From 64610c264b8bcef8ee6a9a15e1717a3f50f68f17 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 15 May 2023 13:39:53 +0600 Subject: [PATCH] Composition aware resources --- nukebuild/RefAssemblyGenerator.cs | 71 ++-- .../Pages/TabControlPage.xaml.cs | 2 +- .../ViewModels/TabControlPageViewModel.cs | 2 +- samples/RenderDemo/Pages/CustomSkiaPage.cs | 6 +- .../RenderDemo/Pages/PathMeasurementPage.cs | 8 +- .../Collections/Pooled/PooledList.cs | 2 +- .../Collections/Pooled/PooledStack.cs | 2 +- src/Avalonia.Base/Input/Cursor.cs | 4 +- src/Avalonia.Base/Media/Brush.cs | 80 ++-- src/Avalonia.Base/Media/ConicGradientBrush.cs | 18 +- src/Avalonia.Base/Media/DashStyle.cs | 4 +- src/Avalonia.Base/Media/Drawing.cs | 4 +- src/Avalonia.Base/Media/DrawingBrush.cs | 61 ++- src/Avalonia.Base/Media/DrawingGroup.cs | 2 +- src/Avalonia.Base/Media/Effects/IEffect.cs | 4 +- src/Avalonia.Base/Media/Geometry.cs | 70 +++- src/Avalonia.Base/Media/GeometryDrawing.cs | 2 +- src/Avalonia.Base/Media/GlyphRun.cs | 5 +- src/Avalonia.Base/Media/GlyphRunDrawing.cs | 2 +- src/Avalonia.Base/Media/GradientBrush.cs | 42 +- src/Avalonia.Base/Media/IAffectsRender.cs | 2 +- src/Avalonia.Base/Media/IImageBrush.cs | 10 +- .../Media/IImmutableGlyphRunReference.cs | 25 ++ src/Avalonia.Base/Media/IMutableBrush.cs | 2 +- .../IMutableExperimentalAcrylicMaterial.cs | 2 +- src/Avalonia.Base/Media/ITransform.cs | 2 + src/Avalonia.Base/Media/ImageBrush.cs | 27 +- src/Avalonia.Base/Media/ImageDrawing.cs | 2 +- src/Avalonia.Base/Media/Imaging/Bitmap.cs | 10 +- src/Avalonia.Base/Media/Imaging/IBitmap.cs | 2 +- .../Media/Imaging/RenderTargetBitmap.cs | 2 +- .../Media/ImmediateDrawingContext.cs | 12 +- .../Media/Immutable/ImmutableImageBrush.cs | 4 +- .../Media/Immutable/ImmutableTileBrush.cs | 2 +- .../Media/LinearGradientBrush.cs | 18 +- src/Avalonia.Base/Media/Pen.cs | 112 +++--- .../Media/PlatformDrawingContext.cs | 6 +- .../Media/RadialGradientBrush.cs | 18 +- src/Avalonia.Base/Media/SolidColorBrush.cs | 20 +- src/Avalonia.Base/Media/TileBrush.cs | 23 +- src/Avalonia.Base/Media/Transform.cs | 20 +- src/Avalonia.Base/Media/VisualBrush.cs | 64 ++- .../Metadata/PrivateApiAttribute.cs | 2 +- src/Avalonia.Base/Platform/IBitmapImpl.cs | 2 +- .../Platform/IDrawingContextImpl.cs | 6 +- .../IDrawingContextWithAcrylicLikeSupport.cs | 2 + src/Avalonia.Base/Reactive/Observable.cs | 12 + src/Avalonia.Base/Rect.cs | 7 + .../Brushes/ServerSimpleCompositionBrush.cs | 61 +++ .../Brushes/ServerSimpleContentBrush.cs | 24 ++ .../Brushes/ServerSimpleImageBrush.cs | 37 ++ .../Composition/CompositingRenderer.cs | 20 +- .../Composition/CompositionDrawListVisual.cs | 14 +- .../Composition/CompositionDrawingSurface.cs | 5 +- .../CompositionExperimentalAcrylicVisual.cs | 11 + .../Composition/CompositionObject.cs | 28 +- .../Composition/CompositionPropertySet.cs | 3 +- .../Composition/CompositionTarget.cs | 2 +- .../Composition/CompositionTransform.cs | 9 + .../Composition/Compositor.Factories.cs | 2 +- .../Rendering/Composition/Compositor.cs | 47 ++- .../Drawing/CompositionDrawList.cs | 129 ------ .../CompositionDrawListSceneBrushContent.cs | 37 -- .../Drawing/CompositionDrawingContext.cs | 371 ------------------ .../Drawing/CompositionRenderData.cs | 68 ++++ .../CompositionRenderDataSceneBrushContent.cs | 47 +++ .../Drawing/CompositorResourceHelpers.cs | 123 ++++++ .../Drawing/ICompositionRenderResource.cs | 12 + .../ImmediateRenderDataSceneBrushContent.cs | 81 ++++ .../Drawing/Nodes/RenderDataBitmapNode.cs | 28 ++ .../Drawing/Nodes/RenderDataEllipseNode.cs | 61 +++ .../Drawing/Nodes/RenderDataGeometryNode.cs | 29 ++ .../Drawing/Nodes/RenderDataGlyphRunNode.cs | 35 ++ .../Drawing/Nodes/RenderDataLineNode.cs | 65 +++ .../Drawing/Nodes/RenderDataNodes.cs | 214 ++++++++++ .../Drawing/Nodes/RenderDataPushMatrixNode.cs | 27 ++ .../Nodes/RenderDataPushOpacityMaskNode.cs | 25 ++ .../Drawing/Nodes/RenderDataRectangleNode.cs | 30 ++ .../Drawing/RenderDataDrawingContext.cs | 349 ++++++++++++++++ .../Drawing/ServerCompositionRenderData.cs | 136 +++++++ .../Drawing/ServerCompositionSimplePen.cs | 12 + .../Drawing/ServerResourceHelperExtensions.cs | 56 +++ .../Composition/ICompositorSerializable.cs | 10 + .../Server/DiagnosticTextRenderer.cs | 2 +- .../Composition/Server/DrawingContextProxy.cs | 9 +- .../Server/ServerCompositionDrawListVisual.cs | 24 +- ...verCompositionExperimentalAcrylicVisual.cs | 26 ++ .../Server/ServerCompositionSurfaceVisual.cs | 2 +- .../Server/ServerCompositionTarget.cs | 2 +- .../ServerCompositor.RenderResources.cs | 22 ++ .../Composition/Server/ServerCompositor.cs | 23 +- .../Composition/Server/ServerList.cs | 11 - .../Composition/Server/ServerObject.cs | 32 +- .../Server/ServerRenderResource.cs | 123 ++++++ .../Composition/Server/SimpleServerObject.cs | 34 ++ .../SceneGraph/BrushDrawOperation.cs | 27 -- .../Rendering/SceneGraph/ClipNode.cs | 87 ---- .../SceneGraph/CustomDrawOperation.cs | 31 -- .../Rendering/SceneGraph/DrawOperation.cs | 56 --- .../Rendering/SceneGraph/EllipseNode.cs | 97 ----- .../SceneGraph/ExperimentalAcrylicNode.cs | 70 ---- .../Rendering/SceneGraph/GeometryClipNode.cs | 74 ---- .../Rendering/SceneGraph/GeometryNode.cs | 73 ---- .../Rendering/SceneGraph/GlyphRunNode.cs | 66 ---- .../Rendering/SceneGraph/IDrawOperation.cs | 41 -- .../Rendering/SceneGraph/ImageNode.cs | 91 ----- .../Rendering/SceneGraph/LineNode.cs | 108 ----- .../Rendering/SceneGraph/OpacityMaskNode.cs | 64 --- .../Rendering/SceneGraph/OpacityNode.cs | 70 ---- .../Rendering/SceneGraph/RectangleNode.cs | 92 ----- src/Avalonia.Base/RoundedRect.cs | 7 + src/Avalonia.Base/Utilities/Polyfills.cs | 43 ++ .../Utilities/PooledInlineList.cs | 221 +++++++++++ src/Avalonia.Base/Utilities/Ref.cs | 34 +- .../Utilities/RefCountingSmallDictionary.cs | 54 +++ .../Utilities/SmallDictionary.cs | 145 ++++++- src/Avalonia.Base/composition-schema.xml | 55 ++- .../ExperimentalAcrylicBorder.cs | 63 ++- src/Avalonia.Controls/NativeMenuItem.cs | 10 +- src/Avalonia.Controls/WindowIcon.cs | 4 +- .../IBitmapToImageConverter.cs | 4 +- .../IBitmapToImageConverter.cs | 2 +- src/Avalonia.X11/X11CursorFactory.cs | 2 +- src/Avalonia.X11/X11IconLoader.cs | 2 +- .../HeadlessPlatformRenderInterface.cs | 6 +- .../AvaloniaXamlIlLanguage.cs | 3 +- .../Converters/IconTypeConverter.cs | 2 +- src/Skia/Avalonia.Skia/DrawingContextImpl.cs | 22 +- .../Media/DrawingContextImpl.cs | 20 +- .../Media/ImageBrushImpl.cs | 2 +- .../Imaging/D2DRenderTargetBitmapImpl.cs | 2 +- .../CompositionGenerator/Config.cs | 10 +- .../Generator.ConfigHelpers.cs | 56 +++ .../CompositionGenerator/Generator.cs | 94 +++-- .../Media/BrushTests.cs | 7 +- .../Media/ImageBrushTests.cs | 15 +- .../Media/LinearGradientBrushTests.cs | 54 +-- .../Avalonia.Base.UnitTests/Media/PenTests.cs | 56 +-- .../Media/RenderResourceTestHelper.cs | 55 +++ .../Media/SolidColorBrushTests.cs | 10 +- .../SceneGraph/DrawOperationTests.cs | 140 ++++--- .../Rendering/SceneGraph/EllipseNodeTests.cs | 17 +- .../Rendering/SceneGraph/LineNodeTests.cs | 15 +- .../BorderTests.cs | 19 +- .../Avalonia.Controls.UnitTests/PanelTests.cs | 19 +- .../ContentPresenterTests_Standalone.cs | 17 - .../Shapes/RectangleTests.cs | 34 -- .../TextBlockTests.cs | 34 -- 148 files changed, 3372 insertions(+), 2355 deletions(-) create mode 100644 src/Avalonia.Base/Media/IImmutableGlyphRunReference.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Brushes/ServerSimpleCompositionBrush.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Brushes/ServerSimpleContentBrush.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Brushes/ServerSimpleImageBrush.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionExperimentalAcrylicVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/CompositionTransform.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawListSceneBrushContent.cs delete mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderData.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderDataSceneBrushContent.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/CompositorResourceHelpers.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/ICompositionRenderResource.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/ImmediateRenderDataSceneBrushContent.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataBitmapNode.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataEllipseNode.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGeometryNode.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGlyphRunNode.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataLineNode.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataNodes.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataPushMatrixNode.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataPushOpacityMaskNode.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataRectangleNode.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataDrawingContext.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionRenderData.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionSimplePen.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Drawing/ServerResourceHelperExtensions.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/ICompositorSerializable.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.RenderResources.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerRenderResource.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/SimpleServerObject.cs delete mode 100644 src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs delete mode 100644 src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs delete mode 100644 src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs delete mode 100644 src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs delete mode 100644 src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs delete mode 100644 src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs delete mode 100644 src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs delete mode 100644 src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs delete mode 100644 src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs delete mode 100644 src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs delete mode 100644 src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs delete mode 100644 src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs delete mode 100644 src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs delete mode 100644 src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs create mode 100644 src/Avalonia.Base/Utilities/Polyfills.cs create mode 100644 src/Avalonia.Base/Utilities/PooledInlineList.cs create mode 100644 src/Avalonia.Base/Utilities/RefCountingSmallDictionary.cs create mode 100644 src/tools/DevGenerators/CompositionGenerator/Generator.ConfigHelpers.cs create mode 100644 tests/Avalonia.Base.UnitTests/Media/RenderResourceTestHelper.cs diff --git a/nukebuild/RefAssemblyGenerator.cs b/nukebuild/RefAssemblyGenerator.cs index f0d5c81a37..e1c19c69a3 100644 --- a/nukebuild/RefAssemblyGenerator.cs +++ b/nukebuild/RefAssemblyGenerator.cs @@ -63,50 +63,51 @@ public class RefAssemblyGenerator }); } + static bool HasPrivateApi(IEnumerable attrs) => attrs.Any(a => + a.AttributeType.FullName == "Avalonia.Metadata.PrivateApiAttribute"); + static void ProcessType(TypeDefinition type, MethodReference obsoleteCtor) { foreach (var nested in type.NestedTypes) ProcessType(nested, obsoleteCtor); - if (type.IsInterface) + + var hideMethods = (type.IsInterface && type.Name.EndsWith("Impl")) + || HasPrivateApi(type.CustomAttributes); + + var injectMethod = hideMethods + || type.CustomAttributes.Any(a => + a.AttributeType.FullName == "Avalonia.Metadata.NotClientImplementableAttribute"); + + + + if (injectMethod) { - var hideMethods = type.Name.EndsWith("Impl") - || (type.HasCustomAttributes && type.CustomAttributes.Any(a => - a.AttributeType.FullName == "Avalonia.Metadata.PrivateApiAttribute")); + type.Methods.Add(new MethodDefinition( + "(This interface or abstract class is -not- implementable by user code !)", + MethodAttributes.Assembly + | MethodAttributes.Abstract + | MethodAttributes.NewSlot + | MethodAttributes.HideBySig, type.Module.TypeSystem.Void)); + } - var injectMethod = hideMethods - || type.CustomAttributes.Any(a => - a.AttributeType.FullName == "Avalonia.Metadata.NotClientImplementableAttribute"); - - if (hideMethods) - { - foreach (var m in type.Methods) - { - var dflags = MethodAttributes.Public | MethodAttributes.Family | MethodAttributes.FamORAssem | - MethodAttributes.FamANDAssem | MethodAttributes.Assembly; - m.Attributes = ((m.Attributes | dflags) ^ dflags) | MethodAttributes.Assembly; - } - } - - if(injectMethod) + var forceUnstable = type.CustomAttributes.FirstOrDefault(a => + a.AttributeType.FullName == "Avalonia.Metadata.UnstableAttribute"); + + foreach (var m in type.Methods) + { + if (hideMethods || HasPrivateApi(m.CustomAttributes)) { - type.Methods.Add(new MethodDefinition("NotClientImplementable", - MethodAttributes.Assembly - | MethodAttributes.Abstract - | MethodAttributes.NewSlot - | MethodAttributes.HideBySig, type.Module.TypeSystem.Void)); + var dflags = MethodAttributes.Public | MethodAttributes.Family | MethodAttributes.FamORAssem | + MethodAttributes.FamANDAssem | MethodAttributes.Assembly; + m.Attributes = ((m.Attributes | dflags) ^ dflags) | MethodAttributes.Assembly; } - - var forceUnstable = type.CustomAttributes.FirstOrDefault(a => - a.AttributeType.FullName == "Avalonia.Metadata.UnstableAttribute"); - - foreach (var m in type.Methods) - MarkAsUnstable(m, obsoleteCtor, forceUnstable); - foreach (var m in type.Properties) - MarkAsUnstable(m, obsoleteCtor, forceUnstable); - foreach (var m in type.Events) - MarkAsUnstable(m, obsoleteCtor, forceUnstable); - + MarkAsUnstable(m, obsoleteCtor, forceUnstable); } + + foreach (var m in type.Properties) + MarkAsUnstable(m, obsoleteCtor, forceUnstable); + foreach (var m in type.Events) + MarkAsUnstable(m, obsoleteCtor, forceUnstable); } static void MarkAsUnstable(IMemberDefinition def, MethodReference obsoleteCtor, ICustomAttribute unstableAttribute) diff --git a/samples/ControlCatalog/Pages/TabControlPage.xaml.cs b/samples/ControlCatalog/Pages/TabControlPage.xaml.cs index f3f4bf6e93..ed48811809 100644 --- a/samples/ControlCatalog/Pages/TabControlPage.xaml.cs +++ b/samples/ControlCatalog/Pages/TabControlPage.xaml.cs @@ -49,7 +49,7 @@ namespace ControlCatalog.Pages AvaloniaXamlLoader.Load(this); } - private static IBitmap LoadBitmap(string uri) + private static Bitmap LoadBitmap(string uri) { var assets = AvaloniaLocator.Current.GetRequiredService(); return new Bitmap(assets.Open(new Uri(uri))); diff --git a/samples/ControlCatalog/ViewModels/TabControlPageViewModel.cs b/samples/ControlCatalog/ViewModels/TabControlPageViewModel.cs index b0e560b001..b2334625ec 100644 --- a/samples/ControlCatalog/ViewModels/TabControlPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/TabControlPageViewModel.cs @@ -21,6 +21,6 @@ public class TabControlPageViewModelItem { public string? Header { get; set; } public string? Text { get; set; } - public IBitmap? Image { get; set; } + public Bitmap? Image { get; set; } public bool IsEnabled { get; set; } = true; } diff --git a/samples/RenderDemo/Pages/CustomSkiaPage.cs b/samples/RenderDemo/Pages/CustomSkiaPage.cs index 2d64ba8f7b..85dc50aadc 100644 --- a/samples/RenderDemo/Pages/CustomSkiaPage.cs +++ b/samples/RenderDemo/Pages/CustomSkiaPage.cs @@ -27,11 +27,11 @@ namespace RenderDemo.Pages class CustomDrawOp : ICustomDrawOperation { - private readonly GlyphRun _noSkia; + private readonly IImmutableGlyphRunReference _noSkia; public CustomDrawOp(Rect bounds, GlyphRun noSkia) { - _noSkia = noSkia; + _noSkia = noSkia.TryCreateImmutableGlyphRunReference(); Bounds = bounds; } @@ -48,7 +48,7 @@ namespace RenderDemo.Pages { var leaseFeature = context.TryGetFeature(); if (leaseFeature == null) - context.DrawGlyphRun(Brushes.Black, _noSkia.PlatformImpl); + context.DrawGlyphRun(Brushes.Black, _noSkia); else { using var lease = leaseFeature.Lease(); diff --git a/samples/RenderDemo/Pages/PathMeasurementPage.cs b/samples/RenderDemo/Pages/PathMeasurementPage.cs index 2fe57165b3..d18c4e6a76 100644 --- a/samples/RenderDemo/Pages/PathMeasurementPage.cs +++ b/samples/RenderDemo/Pages/PathMeasurementPage.cs @@ -53,15 +53,15 @@ namespace RenderDemo.Pages bitmapCtx.DrawGeometry(null, strokePen, basePath); - var length = basePath.PlatformImpl.ContourLength; + var length = basePath.ContourLength; - if (basePath.PlatformImpl.TryGetSegment(length * 0.05, length * 0.2, true, out var dst1)) + if (basePath.TryGetSegment(length * 0.05, length * 0.2, true, out var dst1)) bitmapCtx.DrawGeometry(null, strokePen1, dst1); - if (basePath.PlatformImpl.TryGetSegment(length * 0.2, length * 0.8, true, out var dst2)) + if (basePath.TryGetSegment(length * 0.2, length * 0.8, true, out var dst2)) bitmapCtx.DrawGeometry(null, strokePen2, dst2); - if (basePath.PlatformImpl.TryGetSegment(length * 0.8, length * 0.95, true, out var dst3)) + if (basePath.TryGetSegment(length * 0.8, length * 0.95, true, out var dst3)) bitmapCtx.DrawGeometry(null, strokePen3, dst3); var pathBounds = basePath.GetRenderBounds(strokePen); diff --git a/src/Avalonia.Base/Collections/Pooled/PooledList.cs b/src/Avalonia.Base/Collections/Pooled/PooledList.cs index 267c403ab7..150fe5f7a9 100644 --- a/src/Avalonia.Base/Collections/Pooled/PooledList.cs +++ b/src/Avalonia.Base/Collections/Pooled/PooledList.cs @@ -1423,7 +1423,7 @@ namespace Avalonia.Collections.Pooled private static bool ShouldClear(ClearMode mode) { -#if NETCOREAPP2_1 +#if NETCOREAPP2_1_OR_GREATER return mode == ClearMode.Always || (mode == ClearMode.Auto && RuntimeHelpers.IsReferenceOrContainsReferences()); #else diff --git a/src/Avalonia.Base/Collections/Pooled/PooledStack.cs b/src/Avalonia.Base/Collections/Pooled/PooledStack.cs index f721cb1b62..3d29c43051 100644 --- a/src/Avalonia.Base/Collections/Pooled/PooledStack.cs +++ b/src/Avalonia.Base/Collections/Pooled/PooledStack.cs @@ -596,7 +596,7 @@ namespace Avalonia.Collections.Pooled private static bool ShouldClear(ClearMode mode) { -#if NETCOREAPP2_1 +#if NETCOREAPP2_1_OR_GREATER return mode == ClearMode.Always || (mode == ClearMode.Auto && RuntimeHelpers.IsReferenceOrContainsReferences()); #else diff --git a/src/Avalonia.Base/Input/Cursor.cs b/src/Avalonia.Base/Input/Cursor.cs index 49660e508e..719b64baba 100644 --- a/src/Avalonia.Base/Input/Cursor.cs +++ b/src/Avalonia.Base/Input/Cursor.cs @@ -55,12 +55,12 @@ namespace Avalonia.Input { } - public Cursor(IBitmap cursor, PixelPoint hotSpot) + public Cursor(Bitmap cursor, PixelPoint hotSpot) : this(GetCursorFactory().CreateCursor(cursor.PlatformImpl.Item, hotSpot), "BitmapCursor") { } - public ICursorImpl PlatformImpl { get; } + internal ICursorImpl PlatformImpl { get; } public void Dispose() => PlatformImpl.Dispose(); diff --git a/src/Avalonia.Base/Media/Brush.cs b/src/Avalonia.Base/Media/Brush.cs index accabce145..3c888d13cf 100644 --- a/src/Avalonia.Base/Media/Brush.cs +++ b/src/Avalonia.Base/Media/Brush.cs @@ -4,6 +4,10 @@ using Avalonia.Animation; using Avalonia.Animation.Animators; using Avalonia.Media.Immutable; using Avalonia.Reactive; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; namespace Avalonia.Media { @@ -11,7 +15,7 @@ namespace Avalonia.Media /// Describes how an area is painted. /// [TypeConverter(typeof(BrushConverter))] - public abstract class Brush : Animatable, IBrush + public abstract class Brush : Animatable, IBrush, ICompositionRenderResource, ICompositorSerializable { /// /// Defines the property. @@ -30,14 +34,10 @@ namespace Avalonia.Media /// public static readonly StyledProperty TransformOriginProperty = AvaloniaProperty.Register(nameof(TransformOrigin)); - - /// - public event EventHandler? Invalidated; - + static Brush() { Animation.Animation.RegisterAnimator(prop => typeof(IBrush).IsAssignableFrom(prop.PropertyType)); - AffectsRender(OpacityProperty, TransformProperty); } /// @@ -92,31 +92,57 @@ namespace Avalonia.Media throw new FormatException($"Invalid brush string: '{s}'."); } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if (change.Property == TransformProperty) + _resource.ProcessPropertyChangeNotification(change); - /// - /// Marks a property as affecting the brush's visual representation. - /// - /// The properties. - /// - /// After a call to this method in a brush's static constructor, any change to the - /// property will cause the event to be raised on the brush. - /// - protected static void AffectsRender(params AvaloniaProperty[] properties) - where T : Brush + RegisterForSerialization(); + + base.OnPropertyChanged(change); + } + + private protected void RegisterForSerialization() => + _resource.RegisterForInvalidationOnAllCompositors(this); + + private CompositorResourceHolder _resource; + + IBrush ICompositionRenderResource.GetForCompositor(Compositor c) => _resource.GetForCompositor(c); + + internal abstract Func Factory { get; } + + void ICompositionRenderResource.AddRefOnCompositor(Compositor c) { - var invalidateObserver = new AnonymousObserver( - static e => (e.Sender as T)?.RaiseInvalidated(EventArgs.Empty)); + if (_resource.CreateOrAddRef(c, this, out _, Factory)) + OnReferencedFromCompositor(c); + } - foreach (var property in properties) - { - property.Changed.Subscribe(invalidateObserver); - } + protected virtual void OnReferencedFromCompositor(Compositor c) + { + if (Transform is ICompositionRenderResource resource) + resource.AddRefOnCompositor(c); } - /// - /// Raises the event. - /// - /// The event args. - protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e); + void ICompositionRenderResource.ReleaseOnCompositor(Compositor c) + { + if(_resource.Release(c)) + OnUnreferencedFromCompositor(c); + } + + protected virtual void OnUnreferencedFromCompositor(Compositor c) + { + if (Transform is ICompositionRenderResource resource) + resource.ReleaseOnCompositor(c); + } + + SimpleServerObject? ICompositorSerializable.TryGetServer(Compositor c) => _resource.TryGetForCompositor(c); + + private protected virtual void SerializeChanges(Compositor c, BatchStreamWriter writer) + { + ServerCompositionSimpleBrush.SerializeAllChanges(writer, Opacity, TransformOrigin, Transform.GetServer(c)); + } + + void ICompositorSerializable.SerializeChanges(Compositor c, BatchStreamWriter writer) => SerializeChanges(c, writer); } } diff --git a/src/Avalonia.Base/Media/ConicGradientBrush.cs b/src/Avalonia.Base/Media/ConicGradientBrush.cs index bce7a5af19..f80b731ac1 100644 --- a/src/Avalonia.Base/Media/ConicGradientBrush.cs +++ b/src/Avalonia.Base/Media/ConicGradientBrush.cs @@ -1,4 +1,8 @@ +using System; using Avalonia.Media.Immutable; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; namespace Avalonia.Media { @@ -23,11 +27,6 @@ namespace Avalonia.Media nameof(Angle), 0); - static ConicGradientBrush() - { - AffectsRender(CenterProperty, AngleProperty); - } - /// /// Gets or sets the center point of the gradient. /// @@ -51,5 +50,14 @@ namespace Avalonia.Media { return new ImmutableConicGradientBrush(this); } + + internal override Func Factory => + static c => new ServerCompositionSimpleConicGradientBrush(c.Server); + + private protected override void SerializeChanges(Compositor c, BatchStreamWriter writer) + { + base.SerializeChanges(c, writer); + ServerCompositionSimpleConicGradientBrush.SerializeAllChanges(writer, Angle, Center); + } } } diff --git a/src/Avalonia.Base/Media/DashStyle.cs b/src/Avalonia.Base/Media/DashStyle.cs index 2529b9317d..0efb764029 100644 --- a/src/Avalonia.Base/Media/DashStyle.cs +++ b/src/Avalonia.Base/Media/DashStyle.cs @@ -13,7 +13,7 @@ namespace Avalonia.Media /// /// Represents the sequence of dashes and gaps that will be applied by a . /// - public class DashStyle : Animatable, IDashStyle, IAffectsRender + public class DashStyle : Animatable, IDashStyle { /// /// Defines the property. @@ -104,7 +104,7 @@ namespace Avalonia.Media /// /// Raised when the dash style changes. /// - public event EventHandler? Invalidated; + internal event EventHandler? Invalidated; /// /// Returns an immutable clone of the . diff --git a/src/Avalonia.Base/Media/Drawing.cs b/src/Avalonia.Base/Media/Drawing.cs index 66d05e7e48..867708f78d 100644 --- a/src/Avalonia.Base/Media/Drawing.cs +++ b/src/Avalonia.Base/Media/Drawing.cs @@ -9,7 +9,9 @@ /// Draws this drawing to the given . /// /// The drawing context. - public abstract void Draw(DrawingContext context); + public void Draw(DrawingContext context) => DrawCore(context); + + internal abstract void DrawCore(DrawingContext context); /// /// Gets the drawing's bounding rectangle. diff --git a/src/Avalonia.Base/Media/DrawingBrush.cs b/src/Avalonia.Base/Media/DrawingBrush.cs index 2825628948..89e9b9e075 100644 --- a/src/Avalonia.Base/Media/DrawingBrush.cs +++ b/src/Avalonia.Base/Media/DrawingBrush.cs @@ -1,26 +1,25 @@ +using System; using Avalonia.Media.Immutable; using Avalonia.Rendering; using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; +using Avalonia.Utilities; namespace Avalonia.Media { /// /// Paints an area with an . /// - public class DrawingBrush : TileBrush, ISceneBrush, IAffectsRender + public class DrawingBrush : TileBrush, ISceneBrush { /// /// Defines the property. /// public static readonly StyledProperty DrawingProperty = AvaloniaProperty.Register(nameof(Drawing)); - - static DrawingBrush() - { - AffectsRender(DrawingProperty); - } - + /// /// Initializes a new instance of the class. /// @@ -51,16 +50,52 @@ namespace Avalonia.Media if (Drawing == null) return null; + using var recorder = new RenderDataDrawingContext(null); + Drawing?.Draw(recorder); + return recorder.GetImmediateSceneBrushContent(this, null, true); + } + + internal override Func Factory => + static c => new ServerCompositionSimpleContentBrush(c.Server); + + private InlineDictionary _renderDataDictionary; + + protected override void OnReferencedFromCompositor(Compositor c) + { + _renderDataDictionary.Add(c, CreateServerContent(c)); + base.OnReferencedFromCompositor(c); + } + + protected override void OnUnreferencedFromCompositor(Compositor c) + { + if (_renderDataDictionary.TryGetAndRemoveValue(c, out var content)) + content?.RenderData.Dispose(); + base.OnUnreferencedFromCompositor(c); + } + + private protected override void SerializeChanges(Compositor c, BatchStreamWriter writer) + { + base.SerializeChanges(c, writer); + if (_renderDataDictionary.TryGetValue(c, out var content)) + writer.WriteObject(content); + else + writer.WriteObject(null); + } + + CompositionRenderDataSceneBrushContent? CreateServerContent(Compositor c) + { + if (Drawing == null) + return null; - var recorder = new CompositionDrawingContext(); - recorder.BeginUpdate(null); + using var recorder = new RenderDataDrawingContext(c); Drawing?.Draw(recorder); - var drawList = recorder.EndUpdate(); - if (drawList == null) + var renderData = recorder.GetRenderResults(); + if (renderData == null) return null; - return new CompositionDrawListSceneBrushContent(new ImmutableSceneBrush(this), drawList, - drawList.CalculateBounds(), true); + return new CompositionRenderDataSceneBrushContent( + (ServerCompositionSimpleContentBrush)((ICompositionRenderResource)this).GetForCompositor(c), + renderData, null, true); } } } diff --git a/src/Avalonia.Base/Media/DrawingGroup.cs b/src/Avalonia.Base/Media/DrawingGroup.cs index 5a5bd50c7c..dfcb5c1eb4 100644 --- a/src/Avalonia.Base/Media/DrawingGroup.cs +++ b/src/Avalonia.Base/Media/DrawingGroup.cs @@ -69,7 +69,7 @@ namespace Avalonia.Media public DrawingContext Open() => new DrawingGroupDrawingContext(this); - public override void Draw(DrawingContext context) + internal override void DrawCore(DrawingContext context) { var bounds = GetBounds(); diff --git a/src/Avalonia.Base/Media/Effects/IEffect.cs b/src/Avalonia.Base/Media/Effects/IEffect.cs index 698dccf1dd..23958db756 100644 --- a/src/Avalonia.Base/Media/Effects/IEffect.cs +++ b/src/Avalonia.Base/Media/Effects/IEffect.cs @@ -2,16 +2,18 @@ using System; using System.ComponentModel; +using Avalonia.Metadata; namespace Avalonia.Media; [TypeConverter(typeof(EffectConverter))] +[NotClientImplementable] public interface IEffect { } -public interface IMutableEffect : IEffect, IAffectsRender +public interface IMutableEffect : IEffect { /// /// Creates an immutable clone of the effect. diff --git a/src/Avalonia.Base/Media/Geometry.cs b/src/Avalonia.Base/Media/Geometry.cs index ab1bb3b9fa..0d2311eafc 100644 --- a/src/Avalonia.Base/Media/Geometry.cs +++ b/src/Avalonia.Base/Media/Geometry.cs @@ -1,6 +1,7 @@ using System; using Avalonia.Platform; using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using Avalonia.Reactive; @@ -40,7 +41,7 @@ namespace Avalonia.Media /// /// Gets the platform-specific implementation of the geometry. /// - public IGeometryImpl? PlatformImpl + internal IGeometryImpl? PlatformImpl { get { @@ -192,6 +193,12 @@ namespace Avalonia.Media control?.InvalidateGeometry(); } + /// + /// Gets the geometry's total length as if all its contours are placed + /// in a straight line. + /// + public double ContourLength => PlatformImpl?.ContourLength ?? 0; + /// /// Combines the two geometries using the specified and applies the specified transform to the resulting geometry. /// @@ -204,6 +211,67 @@ namespace Avalonia.Media { return new CombinedGeometry(combineMode, geometry1, geometry2, transform); } + + /// + /// Attempts to get the corresponding point at the + /// specified distance + /// + /// The contour distance to get from. + /// The point in the specified distance. + /// If there's valid point at the specified distance. + public bool TryGetPointAtDistance(double distance, out Point point) + { + if (PlatformImpl == null) + { + point = default; + return false; + } + + return PlatformImpl.TryGetPointAtDistance(distance, out point); + } + + /// + /// Attempts to get the corresponding point and + /// tangent from the specified distance along the + /// contour of the geometry. + /// + /// The contour distance to get from. + /// The point in the specified distance. + /// The tangent in the specified distance. + /// If there's valid point and tangent at the specified distance. + public bool TryGetPointAndTangentAtDistance(double distance, out Point point, out Point tangent) + { + if (PlatformImpl == null) + { + point = tangent = default; + return false; + } + + return PlatformImpl.TryGetPointAndTangentAtDistance(distance, out point, out tangent); + } + + /// + /// Attempts to get the corresponding path segment + /// given by the two distances specified. + /// Imagine it like snipping a part of the current + /// geometry. + /// + /// The contour distance to start snipping from. + /// The contour distance to stop snipping to. + /// If ture, the resulting snipped path will start with a BeginFigure call. + /// The resulting snipped path. + /// If the snipping operation is successful. + public bool TryGetSegment(double startDistance, double stopDistance, bool startOnBeginFigure, + [NotNullWhen(true)] out Geometry? segmentGeometry) + { + segmentGeometry = null; + if (PlatformImpl == null) + return false; + if (!PlatformImpl.TryGetSegment(startDistance, stopDistance, startOnBeginFigure, out var segment)) + return false; + segmentGeometry = new PlatformGeometry(segment); + return true; + } } public class GeometryTypeConverter : TypeConverter diff --git a/src/Avalonia.Base/Media/GeometryDrawing.cs b/src/Avalonia.Base/Media/GeometryDrawing.cs index 75d7e44ab8..dd359c5c64 100644 --- a/src/Avalonia.Base/Media/GeometryDrawing.cs +++ b/src/Avalonia.Base/Media/GeometryDrawing.cs @@ -58,7 +58,7 @@ namespace Avalonia.Media set => SetValue(PenProperty, value); } - public override void Draw(DrawingContext context) + internal override void DrawCore(DrawingContext context) { if (Geometry != null) { diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 20e0f96ff7..612f468bfb 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -214,7 +214,7 @@ namespace Avalonia.Media /// /// The platform implementation of the . /// - public IRef PlatformImpl + internal IRef PlatformImpl => _platformImpl ??= CreateGlyphRunImpl(); /// @@ -851,5 +851,8 @@ namespace Avalonia.Media { return PlatformImpl.Item.GetIntersections(lowerLimit, upperLimit); } + + public IImmutableGlyphRunReference? TryCreateImmutableGlyphRunReference() + => new ImmutableGlyphRunReference(PlatformImpl.Clone()); } } diff --git a/src/Avalonia.Base/Media/GlyphRunDrawing.cs b/src/Avalonia.Base/Media/GlyphRunDrawing.cs index 961203e30e..1bc56c3ff7 100644 --- a/src/Avalonia.Base/Media/GlyphRunDrawing.cs +++ b/src/Avalonia.Base/Media/GlyphRunDrawing.cs @@ -20,7 +20,7 @@ set => SetValue(GlyphRunProperty, value); } - public override void Draw(DrawingContext context) + internal override void DrawCore(DrawingContext context) { if (GlyphRun == null) { diff --git a/src/Avalonia.Base/Media/GradientBrush.cs b/src/Avalonia.Base/Media/GradientBrush.cs index 971d4fdd58..c2068a85ff 100644 --- a/src/Avalonia.Base/Media/GradientBrush.cs +++ b/src/Avalonia.Base/Media/GradientBrush.cs @@ -5,8 +5,11 @@ using System.ComponentModel; using Avalonia.Animation.Animators; using Avalonia.Collections; +using Avalonia.Media.Immutable; using Avalonia.Metadata; using Avalonia.Reactive; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Transport; namespace Avalonia.Media { @@ -29,12 +32,6 @@ namespace Avalonia.Media private IDisposable? _gradientStopsSubscription; - static GradientBrush() - { - GradientStopsProperty.Changed.Subscribe(GradientStopsChanged); - AffectsRender(SpreadMethodProperty); - } - /// /// Initializes a new instance of the class. /// @@ -63,37 +60,46 @@ namespace Avalonia.Media /// IReadOnlyList IGradientBrush.GradientStops => GradientStops; - private static void GradientStopsChanged(AvaloniaPropertyChangedEventArgs e) + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - if (e.Sender is GradientBrush brush) + if (change.Property == GradientStopsProperty) { - var oldValue = (GradientStops?)e.OldValue; - var newValue = (GradientStops?)e.NewValue; + var (oldValue, newValue) = change.GetOldAndNewValue(); if (oldValue != null) { - oldValue.CollectionChanged -= brush.GradientStopsChanged; - brush._gradientStopsSubscription?.Dispose(); + oldValue.CollectionChanged -= GradientStopsChanged; + _gradientStopsSubscription?.Dispose(); } if (newValue != null) { - newValue.CollectionChanged += brush.GradientStopsChanged; - brush._gradientStopsSubscription = newValue.TrackItemPropertyChanged(brush.GradientStopChanged); + newValue.CollectionChanged += GradientStopsChanged; + _gradientStopsSubscription = newValue.TrackItemPropertyChanged(GradientStopChanged); } - - brush.RaiseInvalidated(EventArgs.Empty); } + base.OnPropertyChanged(change); } private void GradientStopsChanged(object? sender, NotifyCollectionChangedEventArgs e) { - RaiseInvalidated(EventArgs.Empty); + RegisterForSerialization(); } private void GradientStopChanged(Tuple e) { - RaiseInvalidated(EventArgs.Empty); + RegisterForSerialization(); + } + + private protected override void SerializeChanges(Compositor c, BatchStreamWriter writer) + { + base.SerializeChanges(c, writer); + writer.Write(SpreadMethod); + writer.Write(GradientStops.Count); + foreach (var stop in GradientStops) + // TODO: Technically it allocates, so it would be better to sync stops individually + writer.WriteObject(new ImmutableGradientStop(stop.Offset, stop.Color)); } public abstract IImmutableBrush ToImmutable(); diff --git a/src/Avalonia.Base/Media/IAffectsRender.cs b/src/Avalonia.Base/Media/IAffectsRender.cs index 0024195ce5..11c7fed2fe 100644 --- a/src/Avalonia.Base/Media/IAffectsRender.cs +++ b/src/Avalonia.Base/Media/IAffectsRender.cs @@ -6,7 +6,7 @@ namespace Avalonia.Media /// Signals to a self-rendering control that changes to the resource should invoke /// . /// - public interface IAffectsRender + internal interface IAffectsRender { /// /// Raised when the resource changes visually. diff --git a/src/Avalonia.Base/Media/IImageBrush.cs b/src/Avalonia.Base/Media/IImageBrush.cs index 07fd2d56fa..439a9401d5 100644 --- a/src/Avalonia.Base/Media/IImageBrush.cs +++ b/src/Avalonia.Base/Media/IImageBrush.cs @@ -1,5 +1,7 @@ using Avalonia.Media.Imaging; using Avalonia.Metadata; +using Avalonia.Platform; +using Avalonia.Utilities; namespace Avalonia.Media { @@ -12,6 +14,12 @@ namespace Avalonia.Media /// /// Gets the image to draw. /// - IBitmap? Source { get; } + IImageBrushSource? Source { get; } + } + + [NotClientImplementable] + public interface IImageBrushSource + { + internal IRef? Bitmap { get; } } } diff --git a/src/Avalonia.Base/Media/IImmutableGlyphRunReference.cs b/src/Avalonia.Base/Media/IImmutableGlyphRunReference.cs new file mode 100644 index 0000000000..5ab2d41783 --- /dev/null +++ b/src/Avalonia.Base/Media/IImmutableGlyphRunReference.cs @@ -0,0 +1,25 @@ +using System; +using Avalonia.Platform; +using Avalonia.Utilities; + +namespace Avalonia.Media; + +public interface IImmutableGlyphRunReference : IDisposable +{ + internal IRef? GlyphRun { get; } +} + +internal class ImmutableGlyphRunReference : IImmutableGlyphRunReference +{ + public ImmutableGlyphRunReference(IRef? glyphRun) + { + GlyphRun = glyphRun; + } + + public IRef? GlyphRun { get; private set; } + public void Dispose() + { + GlyphRun?.Dispose(); + GlyphRun = null; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Media/IMutableBrush.cs b/src/Avalonia.Base/Media/IMutableBrush.cs index f128aba4df..b7eb594147 100644 --- a/src/Avalonia.Base/Media/IMutableBrush.cs +++ b/src/Avalonia.Base/Media/IMutableBrush.cs @@ -7,7 +7,7 @@ namespace Avalonia.Media /// Represents a mutable brush which can return an immutable clone of itself. /// [NotClientImplementable] - internal interface IMutableBrush : IBrush, IAffectsRender + internal interface IMutableBrush : IBrush { /// /// Creates an immutable clone of the brush. diff --git a/src/Avalonia.Base/Media/IMutableExperimentalAcrylicMaterial.cs b/src/Avalonia.Base/Media/IMutableExperimentalAcrylicMaterial.cs index f954a8c52a..2456f1cc84 100644 --- a/src/Avalonia.Base/Media/IMutableExperimentalAcrylicMaterial.cs +++ b/src/Avalonia.Base/Media/IMutableExperimentalAcrylicMaterial.cs @@ -6,7 +6,7 @@ namespace Avalonia.Media /// Represents a mutable brush which can return an immutable clone of itself. /// [NotClientImplementable] - public interface IMutableExperimentalAcrylicMaterial : IExperimentalAcrylicMaterial, IAffectsRender + public interface IMutableExperimentalAcrylicMaterial : IExperimentalAcrylicMaterial { /// /// Creates an immutable clone of the brush. diff --git a/src/Avalonia.Base/Media/ITransform.cs b/src/Avalonia.Base/Media/ITransform.cs index 91577fe38e..3d90a2cb55 100644 --- a/src/Avalonia.Base/Media/ITransform.cs +++ b/src/Avalonia.Base/Media/ITransform.cs @@ -1,8 +1,10 @@ using System.ComponentModel; +using Avalonia.Metadata; namespace Avalonia.Media { [TypeConverter(typeof(TransformConverter))] + [NotClientImplementable] public interface ITransform { Matrix Value { get; } diff --git a/src/Avalonia.Base/Media/ImageBrush.cs b/src/Avalonia.Base/Media/ImageBrush.cs index 718ebf1686..48deffb79c 100644 --- a/src/Avalonia.Base/Media/ImageBrush.cs +++ b/src/Avalonia.Base/Media/ImageBrush.cs @@ -1,5 +1,9 @@ +using System; using Avalonia.Media.Imaging; using Avalonia.Media.Immutable; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; namespace Avalonia.Media { @@ -11,13 +15,8 @@ namespace Avalonia.Media /// /// Defines the property. /// - public static readonly StyledProperty SourceProperty = - AvaloniaProperty.Register(nameof(Source)); - - static ImageBrush() - { - AffectsRender(SourceProperty); - } + public static readonly StyledProperty SourceProperty = + AvaloniaProperty.Register(nameof(Source)); /// /// Initializes a new instance of the class. @@ -30,7 +29,7 @@ namespace Avalonia.Media /// Initializes a new instance of the class. /// /// The image to draw. - public ImageBrush(IBitmap? source) + public ImageBrush(IImageBrushSource? source) { Source = source; } @@ -38,7 +37,7 @@ namespace Avalonia.Media /// /// Gets or sets the image to draw. /// - public IBitmap? Source + public IImageBrushSource? Source { get { return GetValue(SourceProperty); } set { SetValue(SourceProperty, value); } @@ -49,5 +48,15 @@ namespace Avalonia.Media { return new ImmutableImageBrush(this); } + + internal override Func Factory => + static c => new ServerCompositionSimpleImageBrush(c.Server); + + private protected override void SerializeChanges(Compositor c, BatchStreamWriter writer) + { + base.SerializeChanges(c, writer); + var clonedRef = Source?.Bitmap?.Clone(); + writer.WriteObject(clonedRef); + } } } diff --git a/src/Avalonia.Base/Media/ImageDrawing.cs b/src/Avalonia.Base/Media/ImageDrawing.cs index 77b1c52be0..ad5e29e87d 100644 --- a/src/Avalonia.Base/Media/ImageDrawing.cs +++ b/src/Avalonia.Base/Media/ImageDrawing.cs @@ -37,7 +37,7 @@ namespace Avalonia.Media set => SetValue(RectProperty, value); } - public override void Draw(DrawingContext context) + internal override void DrawCore(DrawingContext context) { var imageSource = ImageSource; var rect = Rect; diff --git a/src/Avalonia.Base/Media/Imaging/Bitmap.cs b/src/Avalonia.Base/Media/Imaging/Bitmap.cs index 07bb3db100..fc31077221 100644 --- a/src/Avalonia.Base/Media/Imaging/Bitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/Bitmap.cs @@ -10,7 +10,7 @@ namespace Avalonia.Media.Imaging /// /// Holds a bitmap image. /// - public class Bitmap : IBitmap + public class Bitmap : IBitmap, IImageBrushSource { private bool _isTranscoded; /// @@ -72,7 +72,7 @@ namespace Avalonia.Media.Imaging /// Initializes a new instance of the class. /// /// A platform-specific bitmap implementation. - public Bitmap(IRef impl) + internal Bitmap(IRef impl) { PlatformImpl = impl.Clone(); } @@ -139,7 +139,9 @@ namespace Avalonia.Media.Imaging /// /// Gets the platform-specific bitmap implementation. /// - public IRef PlatformImpl { get; } + internal IRef PlatformImpl { get; } + + IRef IBitmap.PlatformImpl => PlatformImpl; /// /// Saves the bitmap to a file. @@ -237,5 +239,7 @@ namespace Avalonia.Media.Imaging { return AvaloniaLocator.Current.GetRequiredService(); } + + IRef IImageBrushSource.Bitmap => PlatformImpl; } } diff --git a/src/Avalonia.Base/Media/Imaging/IBitmap.cs b/src/Avalonia.Base/Media/Imaging/IBitmap.cs index e7d1862aa2..f2cbc7a11e 100644 --- a/src/Avalonia.Base/Media/Imaging/IBitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/IBitmap.cs @@ -10,7 +10,7 @@ namespace Avalonia.Media.Imaging /// Represents a bitmap image. /// [NotClientImplementable] - public interface IBitmap : IImage, IDisposable + internal interface IBitmap : IImage, IDisposable { /// /// Gets the dots per inch (DPI) of the image. diff --git a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs index 4921e9b756..d638630bb1 100644 --- a/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs +++ b/src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs @@ -38,7 +38,7 @@ namespace Avalonia.Media.Imaging /// /// Gets the platform-specific bitmap implementation. /// - public new IRef PlatformImpl { get; } + internal new IRef PlatformImpl { get; } /// /// Renders a visual to the . diff --git a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs index fdf10596bb..1db53bd576 100644 --- a/src/Avalonia.Base/Media/ImmediateDrawingContext.cs +++ b/src/Avalonia.Base/Media/ImmediateDrawingContext.cs @@ -67,7 +67,7 @@ namespace Avalonia.Media /// /// The bitmap. /// The rect in the output to draw to. - public void DrawBitmap(IBitmap source, Rect rect) + public void DrawBitmap(Bitmap source, Rect rect) { _ = source ?? throw new ArgumentNullException(nameof(source)); DrawBitmap(source, new Rect(source.Size), rect); @@ -79,10 +79,10 @@ namespace Avalonia.Media /// The bitmap. /// The rect in the image to draw. /// The rect in the output to draw to. - public void DrawBitmap(IBitmap source, Rect sourceRect, Rect destRect) + public void DrawBitmap(Bitmap source, Rect sourceRect, Rect destRect) { _ = source ?? throw new ArgumentNullException(nameof(source)); - PlatformImpl.DrawBitmap(source.PlatformImpl, 1, sourceRect, destRect); + PlatformImpl.DrawBitmap(source.PlatformImpl.Item, 1, sourceRect, destRect); } /// @@ -180,11 +180,11 @@ namespace Avalonia.Media /// /// The foreground brush. /// The glyph run. - public void DrawGlyphRun(IImmutableBrush foreground, IRef glyphRun) + public void DrawGlyphRun(IImmutableBrush foreground, IImmutableGlyphRunReference glyphRun) { _ = glyphRun ?? throw new ArgumentNullException(nameof(glyphRun)); - - PlatformImpl.DrawGlyphRun(foreground, glyphRun); + if (glyphRun.GlyphRun != null) + PlatformImpl.DrawGlyphRun(foreground, glyphRun.GlyphRun.Item); } /// diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs index 175038ba75..ce4ba5d140 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs @@ -23,7 +23,7 @@ namespace Avalonia.Media.Immutable /// /// The tile mode. public ImmutableImageBrush( - IBitmap? source, + Bitmap? source, AlignmentX alignmentX = AlignmentX.Center, AlignmentY alignmentY = AlignmentY.Center, RelativeRect? destinationRect = null, @@ -58,6 +58,6 @@ namespace Avalonia.Media.Immutable } /// - public IBitmap? Source { get; } + public IImageBrushSource? Source { get; } } } diff --git a/src/Avalonia.Base/Media/Immutable/ImmutableTileBrush.cs b/src/Avalonia.Base/Media/Immutable/ImmutableTileBrush.cs index 7e139af516..2781141f06 100644 --- a/src/Avalonia.Base/Media/Immutable/ImmutableTileBrush.cs +++ b/src/Avalonia.Base/Media/Immutable/ImmutableTileBrush.cs @@ -21,7 +21,7 @@ namespace Avalonia.Media.Immutable /// How the source rectangle will be stretched to fill the destination rect. /// /// The tile mode. - protected ImmutableTileBrush( + protected internal ImmutableTileBrush( AlignmentX alignmentX, AlignmentY alignmentY, RelativeRect destinationRect, diff --git a/src/Avalonia.Base/Media/LinearGradientBrush.cs b/src/Avalonia.Base/Media/LinearGradientBrush.cs index 9b9ce5e4b7..fc863379a3 100644 --- a/src/Avalonia.Base/Media/LinearGradientBrush.cs +++ b/src/Avalonia.Base/Media/LinearGradientBrush.cs @@ -1,4 +1,8 @@ +using System; using Avalonia.Media.Immutable; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; namespace Avalonia.Media { @@ -23,11 +27,6 @@ namespace Avalonia.Media nameof(EndPoint), RelativePoint.BottomRight); - static LinearGradientBrush() - { - AffectsRender(StartPointProperty, EndPointProperty); - } - /// /// Gets or sets the start point for the gradient. /// @@ -51,5 +50,14 @@ namespace Avalonia.Media { return new ImmutableLinearGradientBrush(this); } + + internal override Func Factory => + static c => new ServerCompositionSimpleLinearGradientBrush(c.Server); + + private protected override void SerializeChanges(Compositor c, BatchStreamWriter writer) + { + base.SerializeChanges(c, writer); + ServerCompositionSimpleLinearGradientBrush.SerializeAllChanges(writer, StartPoint, EndPoint); + } } } diff --git a/src/Avalonia.Base/Media/Pen.cs b/src/Avalonia.Base/Media/Pen.cs index 66632a973b..ad0bc66276 100644 --- a/src/Avalonia.Base/Media/Pen.cs +++ b/src/Avalonia.Base/Media/Pen.cs @@ -1,5 +1,9 @@ using System; using Avalonia.Media.Immutable; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; using Avalonia.Utilities; namespace Avalonia.Media @@ -7,7 +11,7 @@ namespace Avalonia.Media /// /// Describes how a stroke is drawn. /// - public sealed class Pen : AvaloniaObject, IPen + public sealed class Pen : AvaloniaObject, IPen, ICompositionRenderResource, ICompositorSerializable { /// /// Defines the property. @@ -45,9 +49,7 @@ namespace Avalonia.Media public static readonly StyledProperty MiterLimitProperty = AvaloniaProperty.Register(nameof(MiterLimit), 10.0); - private EventHandler? _invalidated; - private IAffectsRender? _subscribedToBrush; - private IAffectsRender? _subscribedToDashes; + private DashStyle? _subscribedToDashes; private TargetWeakEventSubscriber? _weakSubscriber; /// @@ -110,8 +112,8 @@ namespace Avalonia.Media set => SetValue(BrushProperty, value); } - private static readonly WeakEvent InvalidatedWeakEvent = - WeakEvent.Register( + private static readonly WeakEvent InvalidatedWeakEvent = + WeakEvent.Register( (s, h) => s.Invalidated += h, (s, h) => s.Invalidated -= h); @@ -161,23 +163,6 @@ namespace Avalonia.Media set => SetValue(MiterLimitProperty, value); } - /// - /// Raised when the pen changes. - /// - public event EventHandler? Invalidated - { - add - { - _invalidated += value; - UpdateSubscriptions(); - } - remove - { - _invalidated -= value; - UpdateSubscriptions(); - } - } - /// /// Creates an immutable clone of the brush. /// @@ -192,48 +177,79 @@ namespace Avalonia.Media LineJoin, MiterLimit); } + + void RegisterForSerialization() + { + _resource.RegisterForInvalidationOnAllCompositors(this); + } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { - _invalidated?.Invoke(this, EventArgs.Empty); - if(change.Property == BrushProperty) - UpdateSubscription(ref _subscribedToBrush, Brush); + RegisterForSerialization(); + + if (change.Property == BrushProperty) + _resource.ProcessPropertyChangeNotification(change); + if(change.Property == DashStyleProperty) - UpdateSubscription(ref _subscribedToDashes, DashStyle); + UpdateDashStyleSubscription(); base.OnPropertyChanged(change); } - void UpdateSubscription(ref IAffectsRender? field, object? value) + void UpdateDashStyleSubscription() + { + var newValue = _resource.IsAttached ? DashStyle as DashStyle : null; + + if(ReferenceEquals(_subscribedToDashes, newValue)) + return; + + if (_subscribedToDashes != null && _weakSubscriber != null) + { + InvalidatedWeakEvent.Unsubscribe(_subscribedToDashes, _weakSubscriber); + _subscribedToDashes = null; + } + + if (newValue != null) + { + _weakSubscriber ??= new TargetWeakEventSubscriber( + this, static (target, _, ev, _) => + { + if (ev == InvalidatedWeakEvent) + target.RegisterForSerialization(); + }); + InvalidatedWeakEvent.Subscribe(newValue, _weakSubscriber); + _subscribedToDashes = newValue; + } + } + + private CompositorResourceHolder _resource; + + IPen ICompositionRenderResource.GetForCompositor(Compositor c) => _resource.GetForCompositor(c); + + void ICompositionRenderResource.AddRefOnCompositor(Compositor c) { - if ((_invalidated == null || field != value) && field != null) + if (_resource.CreateOrAddRef(c, this, out _, static c => new ServerCompositionSimplePen(c.Server))) { - if (_weakSubscriber != null) - InvalidatedWeakEvent.Unsubscribe(field, _weakSubscriber); - field = null; + (Brush as ICompositionRenderResource)?.AddRefOnCompositor(c); + UpdateDashStyleSubscription(); } + } - if (_invalidated != null && field != value && value is IAffectsRender affectsRender) + void ICompositionRenderResource.ReleaseOnCompositor(Compositor c) + { + if (_resource.Release(c)) { - if (_weakSubscriber == null) - { - _weakSubscriber = new TargetWeakEventSubscriber( - this, static (target, _, ev, _) => - { - if (ev == InvalidatedWeakEvent) - target._invalidated?.Invoke(target, EventArgs.Empty); - }); - } - - InvalidatedWeakEvent.Subscribe(affectsRender, _weakSubscriber); - field = affectsRender; + (Brush as ICompositionRenderResource)?.ReleaseOnCompositor(c); + UpdateDashStyleSubscription(); } } - void UpdateSubscriptions() + SimpleServerObject? ICompositorSerializable.TryGetServer(Compositor c) => _resource.TryGetForCompositor(c); + + void ICompositorSerializable.SerializeChanges(Compositor c, BatchStreamWriter writer) { - UpdateSubscription(ref _subscribedToBrush, Brush); - UpdateSubscription(ref _subscribedToDashes, DashStyle); + ServerCompositionSimplePen.SerializeAllChanges(writer, + Brush.GetServer(c), DashStyle?.ToImmutable(), LineCap, LineJoin, MiterLimit, Thickness); } } } diff --git a/src/Avalonia.Base/Media/PlatformDrawingContext.cs b/src/Avalonia.Base/Media/PlatformDrawingContext.cs index 09c0cd26ac..1c1f202532 100644 --- a/src/Avalonia.Base/Media/PlatformDrawingContext.cs +++ b/src/Avalonia.Base/Media/PlatformDrawingContext.cs @@ -10,7 +10,7 @@ using Avalonia.Utilities; namespace Avalonia.Media; -internal sealed class PlatformDrawingContext : DrawingContext, IDrawingContextWithAcrylicLikeSupport +internal sealed class PlatformDrawingContext : DrawingContext { private readonly IDrawingContextImpl _impl; private readonly bool _ownsImpl; @@ -45,7 +45,7 @@ internal sealed class PlatformDrawingContext : DrawingContext, IDrawingContextWi protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect) => _impl.DrawEllipse(brush, pen, rect); internal override void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect) => - _impl.DrawBitmap(source, opacity, sourceRect, destRect); + _impl.DrawBitmap(source.Item, opacity, sourceRect, destRect); public override void Custom(ICustomDrawOperation custom) { @@ -66,7 +66,7 @@ internal sealed class PlatformDrawingContext : DrawingContext, IDrawingContextWi _ = glyphRun ?? throw new ArgumentNullException(nameof(glyphRun)); if (foreground != null) - _impl.DrawGlyphRun(foreground, glyphRun.PlatformImpl); + _impl.DrawGlyphRun(foreground, glyphRun.PlatformImpl.Item); } protected override void PushClipCore(RoundedRect rect) => _impl.PushClip(rect); diff --git a/src/Avalonia.Base/Media/RadialGradientBrush.cs b/src/Avalonia.Base/Media/RadialGradientBrush.cs index f803051676..334ef328c0 100644 --- a/src/Avalonia.Base/Media/RadialGradientBrush.cs +++ b/src/Avalonia.Base/Media/RadialGradientBrush.cs @@ -1,4 +1,8 @@ +using System; using Avalonia.Media.Immutable; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; namespace Avalonia.Media { @@ -31,11 +35,6 @@ namespace Avalonia.Media nameof(Radius), 0.5); - static RadialGradientBrush() - { - AffectsRender(CenterProperty, GradientOriginProperty, RadiusProperty); - } - /// /// Gets or sets the start point for the gradient. /// @@ -71,5 +70,14 @@ namespace Avalonia.Media { return new ImmutableRadialGradientBrush(this); } + + internal override Func Factory => + static c => new ServerCompositionSimpleRadialGradientBrush(c.Server); + + private protected override void SerializeChanges(Compositor c, BatchStreamWriter writer) + { + base.SerializeChanges(c, writer); + ServerCompositionSimpleRadialGradientBrush.SerializeAllChanges(writer, Center, GradientOrigin, Radius); + } } } diff --git a/src/Avalonia.Base/Media/SolidColorBrush.cs b/src/Avalonia.Base/Media/SolidColorBrush.cs index d8e25fe748..a552e262ae 100644 --- a/src/Avalonia.Base/Media/SolidColorBrush.cs +++ b/src/Avalonia.Base/Media/SolidColorBrush.cs @@ -1,5 +1,9 @@ +using System; using Avalonia.Animation.Animators; using Avalonia.Media.Immutable; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; namespace Avalonia.Media { @@ -13,12 +17,7 @@ namespace Avalonia.Media /// public static readonly StyledProperty ColorProperty = AvaloniaProperty.Register(nameof(Color)); - - static SolidColorBrush() - { - AffectsRender(ColorProperty); - } - + /// /// Initializes a new instance of the class. /// @@ -84,5 +83,14 @@ namespace Avalonia.Media { return new ImmutableSolidColorBrush(this); } + + internal override Func Factory => + static c => new ServerCompositionSimpleSolidColorBrush(c.Server); + + private protected override void SerializeChanges(Compositor c, BatchStreamWriter writer) + { + base.SerializeChanges(c, writer); + ServerCompositionSimpleSolidColorBrush.SerializeAllChanges(writer, Color); + } } } diff --git a/src/Avalonia.Base/Media/TileBrush.cs b/src/Avalonia.Base/Media/TileBrush.cs index d7b818a174..47a617a4fb 100644 --- a/src/Avalonia.Base/Media/TileBrush.cs +++ b/src/Avalonia.Base/Media/TileBrush.cs @@ -1,4 +1,7 @@ using Avalonia.Media.Imaging; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; namespace Avalonia.Media { @@ -73,18 +76,7 @@ namespace Avalonia.Media /// public static readonly StyledProperty TileModeProperty = AvaloniaProperty.Register(nameof(TileMode)); - - static TileBrush() - { - AffectsRender( - AlignmentXProperty, - AlignmentYProperty, - DestinationRectProperty, - SourceRectProperty, - StretchProperty, - TileModeProperty); - } - + /// /// Gets or sets the horizontal alignment of a tile in the destination. /// @@ -139,5 +131,12 @@ namespace Avalonia.Media get { return (TileMode)GetValue(TileModeProperty); } set { SetValue(TileModeProperty, value); } } + + private protected override void SerializeChanges(Compositor c, BatchStreamWriter writer) + { + base.SerializeChanges(c, writer); + ServerCompositionSimpleTileBrush.SerializeAllChanges(writer, AlignmentX, AlignmentY, DestinationRect, SourceRect, + Stretch, TileMode); + } } } diff --git a/src/Avalonia.Base/Media/Transform.cs b/src/Avalonia.Base/Media/Transform.cs index 85393ab189..2934291626 100644 --- a/src/Avalonia.Base/Media/Transform.cs +++ b/src/Avalonia.Base/Media/Transform.cs @@ -2,6 +2,10 @@ using System; using Avalonia.Animation; using Avalonia.Animation.Animators; using Avalonia.Media.Immutable; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; using Avalonia.VisualTree; namespace Avalonia.Media @@ -9,7 +13,7 @@ namespace Avalonia.Media /// /// Represents a transform on an . /// - public abstract class Transform : Animatable, IMutableTransform + public abstract class Transform : Animatable, IMutableTransform, ICompositionRenderResource, ICompositorSerializable { static Transform() { @@ -62,5 +66,19 @@ namespace Avalonia.Media { return Value.ToString(); } + + private CompositorResourceHolder _resource; + ITransform ICompositionRenderResource.GetForCompositor(Compositor c) => _resource.GetForCompositor(c); + SimpleServerObject? ICompositorSerializable.TryGetServer(Compositor c) => _resource.TryGetForCompositor(c); + + void ICompositionRenderResource.AddRefOnCompositor(Compositor c) + { + _resource.CreateOrAddRef(c, this, out _, static (cc) => new ServerCompositionSimpleTransform(cc.Server)); + } + + void ICompositionRenderResource.ReleaseOnCompositor(Compositor c) => _resource.Release(c); + + void ICompositorSerializable.SerializeChanges(Compositor c, BatchStreamWriter writer) => + ServerCompositionSimpleTransform.SerializeAllChanges(writer, Value); } } diff --git a/src/Avalonia.Base/Media/VisualBrush.cs b/src/Avalonia.Base/Media/VisualBrush.cs index 6bfe20271f..a1be9a9202 100644 --- a/src/Avalonia.Base/Media/VisualBrush.cs +++ b/src/Avalonia.Base/Media/VisualBrush.cs @@ -1,25 +1,25 @@ +using System; using Avalonia.Media.Immutable; using Avalonia.Rendering; using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; +using Avalonia.Utilities; namespace Avalonia.Media { /// /// Paints an area with an . /// - public class VisualBrush : TileBrush, ISceneBrush, IAffectsRender + public class VisualBrush : TileBrush, ISceneBrush { /// /// Defines the property. /// public static readonly StyledProperty VisualProperty = AvaloniaProperty.Register(nameof(Visual)); - - static VisualBrush() - { - AffectsRender(VisualProperty); - } + /// /// Initializes a new instance of the class. @@ -54,15 +54,55 @@ namespace Avalonia.Media if (Visual is IVisualBrushInitialize initialize) initialize.EnsureInitialized(); - var recorder = new CompositionDrawingContext(); - recorder.BeginUpdate(null); + using var recorder = new RenderDataDrawingContext(null); ImmediateRenderer.Render(recorder, Visual, Visual.Bounds); - var drawList = recorder.EndUpdate(); - if (drawList == null) + return recorder.GetImmediateSceneBrushContent(this, new(Visual.Bounds.Size), false); + } + + internal override Func Factory => + static c => new ServerCompositionSimpleContentBrush(c.Server); + + private InlineDictionary _renderDataDictionary; + + protected override void OnReferencedFromCompositor(Compositor c) + { + _renderDataDictionary.Add(c, CreateServerContent(c)); + base.OnReferencedFromCompositor(c); + } + + protected override void OnUnreferencedFromCompositor(Compositor c) + { + if (_renderDataDictionary.TryGetAndRemoveValue(c, out var content)) + content?.RenderData.Dispose(); + base.OnUnreferencedFromCompositor(c); + } + + private protected override void SerializeChanges(Compositor c, BatchStreamWriter writer) + { + base.SerializeChanges(c, writer); + if (_renderDataDictionary.TryGetValue(c, out var content)) + writer.WriteObject(content); + else + writer.WriteObject(null); + } + + CompositionRenderDataSceneBrushContent? CreateServerContent(Compositor c) + { + if (Visual == null) return null; - return new CompositionDrawListSceneBrushContent(new ImmutableSceneBrush(this), drawList, - new(Visual.Bounds.Size), false); + if (Visual is IVisualBrushInitialize initialize) + initialize.EnsureInitialized(); + + using var recorder = new RenderDataDrawingContext(c); + ImmediateRenderer.Render(recorder, Visual, Visual.Bounds); + var renderData = recorder.GetRenderResults(); + if (renderData == null) + return null; + + return new CompositionRenderDataSceneBrushContent( + (ServerCompositionSimpleContentBrush)((ICompositionRenderResource)this).GetForCompositor(c), + renderData, new(Visual.Bounds.Size), false); } } } diff --git a/src/Avalonia.Base/Metadata/PrivateApiAttribute.cs b/src/Avalonia.Base/Metadata/PrivateApiAttribute.cs index 3f60940c5e..8848a6b629 100644 --- a/src/Avalonia.Base/Metadata/PrivateApiAttribute.cs +++ b/src/Avalonia.Base/Metadata/PrivateApiAttribute.cs @@ -2,7 +2,7 @@ using System; namespace Avalonia.Metadata; -[AttributeUsage(AttributeTargets.Interface)] +[AttributeUsage(AttributeTargets.Interface | AttributeTargets.Class | AttributeTargets.Constructor | AttributeTargets.Method)] public sealed class PrivateApiAttribute : Attribute { diff --git a/src/Avalonia.Base/Platform/IBitmapImpl.cs b/src/Avalonia.Base/Platform/IBitmapImpl.cs index 299a758961..586ac76a3a 100644 --- a/src/Avalonia.Base/Platform/IBitmapImpl.cs +++ b/src/Avalonia.Base/Platform/IBitmapImpl.cs @@ -7,7 +7,7 @@ namespace Avalonia.Platform /// /// Defines the platform-specific interface for a . /// - [Unstable] + [PrivateApi] public interface IBitmapImpl : IDisposable { /// diff --git a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs index d86519656c..8416354bc2 100644 --- a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs @@ -35,7 +35,7 @@ namespace Avalonia.Platform /// The opacity to draw with. /// The rect in the image to draw. /// The rect in the output to draw to. - void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect); + void DrawBitmap(IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect); /// /// Draws a bitmap image. @@ -44,7 +44,7 @@ namespace Avalonia.Platform /// The opacity mask to draw with. /// The destination rect for the opacity mask. /// The rect in the output to draw to. - void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect); + void DrawBitmap(IBitmapImpl source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect); /// /// Draws a line. @@ -94,7 +94,7 @@ namespace Avalonia.Platform /// /// The foreground. /// The glyph run. - void DrawGlyphRun(IBrush? foreground, IRef glyphRun); + void DrawGlyphRun(IBrush? foreground, IGlyphRunImpl glyphRun); /// /// Creates a new that can be used as a render layer diff --git a/src/Avalonia.Base/Platform/IDrawingContextWithAcrylicLikeSupport.cs b/src/Avalonia.Base/Platform/IDrawingContextWithAcrylicLikeSupport.cs index a3cf0298dd..18afef4ecc 100644 --- a/src/Avalonia.Base/Platform/IDrawingContextWithAcrylicLikeSupport.cs +++ b/src/Avalonia.Base/Platform/IDrawingContextWithAcrylicLikeSupport.cs @@ -1,7 +1,9 @@ using Avalonia.Media; +using Avalonia.Metadata; namespace Avalonia.Platform { + [PrivateApi] public interface IDrawingContextWithAcrylicLikeSupport { void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect); diff --git a/src/Avalonia.Base/Reactive/Observable.cs b/src/Avalonia.Base/Reactive/Observable.cs index 1009829429..4cffdaa685 100644 --- a/src/Avalonia.Base/Reactive/Observable.cs +++ b/src/Avalonia.Base/Reactive/Observable.cs @@ -165,6 +165,18 @@ internal static class Observable }); } + public static IObservable FromEventPattern(Action> addHandler, Action> removeHandler) where T : EventArgs + { + return Create(observer => + { + var handler = new Action(observer.OnNext); + var converted = new EventHandler((_, args) => handler(args)); + addHandler(converted); + + return Disposable.Create(() => removeHandler(converted)); + }); + } + public static IObservable Return(T value) { return new ReturnImpl(value); diff --git a/src/Avalonia.Base/Rect.cs b/src/Avalonia.Base/Rect.cs index fc5d0fc043..d9218ab36e 100644 --- a/src/Avalonia.Base/Rect.cs +++ b/src/Avalonia.Base/Rect.cs @@ -607,5 +607,12 @@ namespace Avalonia ); } } + + /// + /// This method should be used internally to check for the rect emptiness + /// Once we add support for WPF-like empty rects, there will be an actual implementation + /// For now it's internal to keep some loud community members happy about the API being pretty + /// + internal bool IsEmpty() => this == default; } } diff --git a/src/Avalonia.Base/Rendering/Composition/Brushes/ServerSimpleCompositionBrush.cs b/src/Avalonia.Base/Rendering/Composition/Brushes/ServerSimpleCompositionBrush.cs new file mode 100644 index 0000000000..5b05d001fd --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Brushes/ServerSimpleCompositionBrush.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using Avalonia.Media; +using Avalonia.Media.Immutable; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.Composition.Transport; + +// ReSharper disable CheckNamespace + +namespace Avalonia.Rendering.Composition.Server +{ + internal partial class ServerCompositionSimpleBrush : IBrush + { + ITransform? IBrush.Transform => Transform; + } + + class ServerCompositionSimpleGradientBrush : ServerCompositionSimpleBrush, IGradientBrush + { + + internal ServerCompositionSimpleGradientBrush(ServerCompositor compositor) : base(compositor) + { + + } + + private readonly List _gradientStops = new(); + public IReadOnlyList GradientStops => _gradientStops; + public GradientSpreadMethod SpreadMethod { get; private set; } + + protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt) + { + base.DeserializeChangesCore(reader, committedAt); + SpreadMethod = reader.Read(); + _gradientStops.Clear(); + var count = reader.Read(); + for (var c = 0; c < count; c++) + _gradientStops.Add(reader.ReadObject()); + } + } + + partial class ServerCompositionSimpleConicGradientBrush : IConicGradientBrush + { + + } + + partial class ServerCompositionSimpleLinearGradientBrush : ILinearGradientBrush + { + + } + + partial class ServerCompositionSimpleRadialGradientBrush : IRadialGradientBrush + { + + } + + partial class ServerCompositionSimpleSolidColorBrush : ISolidColorBrush + { + + } + + +} diff --git a/src/Avalonia.Base/Rendering/Composition/Brushes/ServerSimpleContentBrush.cs b/src/Avalonia.Base/Rendering/Composition/Brushes/ServerSimpleContentBrush.cs new file mode 100644 index 0000000000..2d4f3cd8a4 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Brushes/ServerSimpleContentBrush.cs @@ -0,0 +1,24 @@ +using System; +using Avalonia.Media; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server; + +internal class ServerCompositionSimpleContentBrush : ServerCompositionSimpleTileBrush, ITileBrush, ISceneBrush +{ + private CompositionRenderDataSceneBrushContent? _content; + + internal ServerCompositionSimpleContentBrush(ServerCompositor compositor) : base(compositor) + { + } + + // TODO: Figure out something about disposable + public ISceneBrushContent? CreateContent() => _content; + + protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt) + { + base.DeserializeChangesCore(reader, committedAt); + _content = reader.ReadObject(); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Brushes/ServerSimpleImageBrush.cs b/src/Avalonia.Base/Rendering/Composition/Brushes/ServerSimpleImageBrush.cs new file mode 100644 index 0000000000..f1405b9eff --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Brushes/ServerSimpleImageBrush.cs @@ -0,0 +1,37 @@ +using System; +using System.Data; +using System.IO; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Transport; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server; + +internal class ServerCompositionSimpleImageBrush : ServerCompositionSimpleTileBrush, + IImageBrush, IImageBrushSource +{ + public IImageBrushSource? Source => this; + public IRef? Bitmap { get; private set; } + + internal ServerCompositionSimpleImageBrush(ServerCompositor compositor) : base(compositor) + { + } + + public override void Dispose() + { + Bitmap?.Dispose(); + Bitmap = null; + base.Dispose(); + } + + + protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt) + { + base.DeserializeChangesCore(reader, committedAt); + Bitmap?.Dispose(); + Bitmap = null; + Bitmap = reader.ReadObject>(); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs index 1bf52729a0..b6fe265531 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Avalonia.Collections; using Avalonia.Collections.Pooled; using Avalonia.Media; +using Avalonia.Rendering.Composition.Drawing; using Avalonia.VisualTree; // Special license applies License.md @@ -20,8 +21,7 @@ public class CompositingRenderer : IRendererWithCompositor { private readonly IRenderRoot _root; private readonly Compositor _compositor; - private readonly CompositionDrawingContext _recorder = new(); - private readonly DrawingContext _recordingContext; + private readonly RenderDataDrawingContext _recorder; private readonly HashSet _dirty = new(); private readonly HashSet _recalculateChildren = new(); private readonly Action _update; @@ -55,7 +55,7 @@ public class CompositingRenderer : IRendererWithCompositor { _root = root; _compositor = compositor; - _recordingContext = _recorder; + _recorder = new(compositor); CompositionTarget = compositor.CreateCompositionTarget(surfaces); CompositionTarget.Root = ((Visual)root).AttachToCompositor(compositor); _update = Update; @@ -276,10 +276,16 @@ public class CompositingRenderer : IRendererWithCompositor comp.TransformMatrix = MatrixUtils.ToMatrix4x4(renderTransform); - _recorder.BeginUpdate(comp.DrawList); - visual.Render(_recordingContext); - comp.DrawList = _recorder.EndUpdate(); - + try + { + visual.Render(_recorder); + comp.DrawList = _recorder.GetRenderResults(); + } + finally + { + _recorder.Reset(); + } + SyncChildren(visual); } foreach(var v in _recalculateChildren) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs index f1905fec08..4ce8a9f9ae 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs @@ -1,9 +1,6 @@ -using System; -using System.Numerics; using Avalonia.Rendering.Composition.Drawing; using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.Composition.Transport; -using Avalonia.VisualTree; // Special license applies License.md @@ -21,12 +18,12 @@ internal class CompositionDrawListVisual : CompositionContainerVisual public Visual Visual { get; } private bool _drawListChanged; - private CompositionDrawList? _drawList; + private CompositionRenderData? _drawList; /// /// The list of drawing commands /// - public CompositionDrawList? DrawList + public CompositionRenderData? DrawList { get => _drawList; set @@ -43,7 +40,7 @@ internal class CompositionDrawListVisual : CompositionContainerVisual writer.Write((byte)(_drawListChanged ? 1 : 0)); if (_drawListChanged) { - writer.WriteObject(DrawList?.Clone()); + writer.WriteObject(DrawList?.Server); _drawListChanged = false; } base.SerializeChangesCore(writer); @@ -64,9 +61,6 @@ internal class CompositionDrawListVisual : CompositionContainerVisual return custom.HitTest(pt); } - foreach (var op in DrawList!) - if (op.Item.HitTest(pt)) - return true; - return false; + return DrawList?.HitTest(pt) ?? false; } } diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs b/src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs index bfe70d593d..b98f556d73 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs @@ -1,10 +1,11 @@ +using System; using System.Threading.Tasks; using Avalonia.Rendering.Composition.Server; using Avalonia.Threading; namespace Avalonia.Rendering.Composition; -public class CompositionDrawingSurface : CompositionSurface +public class CompositionDrawingSurface : CompositionSurface, IDisposable { internal new ServerCompositionDrawingSurface Server => (ServerCompositionDrawingSurface)base.Server!; internal CompositionDrawingSurface(Compositor compositor) : base(compositor, new ServerCompositionDrawingSurface(compositor.Server)) @@ -57,4 +58,6 @@ public class CompositionDrawingSurface : CompositionSurface { Dispatcher.UIThread.Post(Dispose); } + + public new void Dispose() => base.Dispose(); } diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionExperimentalAcrylicVisual.cs b/src/Avalonia.Base/Rendering/Composition/CompositionExperimentalAcrylicVisual.cs new file mode 100644 index 0000000000..e3322c8ca5 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionExperimentalAcrylicVisual.cs @@ -0,0 +1,11 @@ +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Rendering.Composition; + +internal partial class CompositionExperimentalAcrylicVisual +{ + internal CompositionExperimentalAcrylicVisual(Compositor compositor, Visual visual) : base(compositor, + new ServerCompositionExperimentalAcrylicVisual(compositor.Server, visual), visual) + { + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs index 8c21b534db..942b27d84e 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionObject.cs @@ -1,4 +1,6 @@ using System; +using System.Diagnostics; +using System.Runtime.CompilerServices; using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Expressions; using Avalonia.Rendering.Composition.Server; @@ -14,7 +16,7 @@ namespace Avalonia.Rendering.Composition /// Composition objects are the visual tree structure on which all other features of the composition API use and build on. /// The API allows developers to define and create one or many objects each representing a single node in a Visual tree. /// - public abstract class CompositionObject : IDisposable + public abstract class CompositionObject : ICompositorSerializable { /// /// The collection of implicit animations attached to this object. @@ -22,7 +24,7 @@ namespace Avalonia.Rendering.Composition public ImplicitAnimationCollection? ImplicitAnimations { get; set; } private protected InlineDictionary PendingAnimations; - internal CompositionObject(Compositor compositor, ServerObject? server) + internal CompositionObject(Compositor compositor, SimpleServerObject? server) { Compositor = compositor; Server = server; @@ -32,16 +34,25 @@ namespace Avalonia.Rendering.Composition /// The associated Compositor /// public Compositor Compositor { get; } - internal ServerObject? Server { get; } + + SimpleServerObject ICompositorSerializable.TryGetServer(Compositor c) + { + Debug.Assert(c == Compositor); + return Server ?? ThrowInvalidOperation(); + } + + internal SimpleServerObject? Server { get; } public bool IsDisposed { get; private set; } private bool _registeredForSerialization; - private static void ThrowInvalidOperation() => + [MethodImpl(MethodImplOptions.NoInlining)] + private static SimpleServerObject ThrowInvalidOperation() => throw new InvalidOperationException("There is no server-side counterpart for this object"); - public void Dispose() + protected internal void Dispose() { - RegisterForSerialization(); + if (!IsDisposed && Server != null) + Compositor.DisposeOnNextBatch(Server); IsDisposed = true; } @@ -128,16 +139,15 @@ namespace Avalonia.Rendering.Composition Compositor.RegisterForSerialization(this); } - internal void SerializeChanges(BatchStreamWriter writer) + void ICompositorSerializable.SerializeChanges(Compositor c, BatchStreamWriter writer) { + Debug.Assert(c == Compositor); _registeredForSerialization = false; SerializeChangesCore(writer); } private protected virtual void SerializeChangesCore(BatchStreamWriter writer) { - if (Server is IDisposable) - writer.Write((byte)(IsDisposed ? 1 : 0)); } } } diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs index efd89951bb..8052d9c32c 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Numerics; using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Expressions; +using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.Composition.Transport; // Special license applies License.md @@ -130,7 +131,7 @@ namespace Avalonia.Rendering.Composition 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); + dic[o.Key] = new PropertySetSnapshot.Value((ServerObject)o.Value.Server); } foreach (var v in _variants) diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs index f2fa846205..492ea774bc 100644 --- a/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs @@ -11,7 +11,7 @@ namespace Avalonia.Rendering.Composition /// /// Represents the composition output (e. g. a window, embedded control, entire screen) /// - public partial class CompositionTarget + internal partial class CompositionTarget { partial void OnRootChanged() { diff --git a/src/Avalonia.Base/Rendering/Composition/CompositionTransform.cs b/src/Avalonia.Base/Rendering/Composition/CompositionTransform.cs new file mode 100644 index 0000000000..bd7165398a --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/CompositionTransform.cs @@ -0,0 +1,9 @@ +using Avalonia.Media; + +// ReSharper disable CheckNamespace +namespace Avalonia.Rendering.Composition.Server; + +internal partial class ServerCompositionSimpleTransform : ITransform +{ + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs index 801dd32d59..8a75681c68 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs @@ -12,7 +12,7 @@ public partial class Compositor /// /// A factory method to create IRenderTarget to be called from the render thread /// - public CompositionTarget CreateCompositionTarget(Func> surfaces) + internal CompositionTarget CreateCompositionTarget(Func> surfaces) { return new CompositionTarget(this, new ServerCompositionTarget(_server, surfaces, DiagnosticTextRenderer)); } diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 153b32c5f3..a2398fdb61 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -5,6 +5,7 @@ using System.Numerics; using System.Threading.Tasks; using Avalonia.Animation.Easings; using Avalonia.Media; +using Avalonia.Metadata; using Avalonia.Platform; using Avalonia.Rendering.Composition.Animations; using Avalonia.Rendering.Composition.Server; @@ -29,8 +30,10 @@ namespace Avalonia.Rendering.Composition private Action _commit; private BatchStreamObjectPool _batchObjectPool = new(); private BatchStreamMemoryPool _batchMemoryPool = new(); - private List _objectsForSerialization = new(); + private Queue _objectSerializationQueue = new(); + private HashSet _objectSerializationHashSet = new(); private Queue _invokeBeforeCommitWrite = new(), _invokeBeforeCommitRead = new(); + private HashSet _disposeOnNextBatch = new(); internal ServerCompositor Server => _server; private Task? _pendingBatch; private readonly object _pendingBatchLock = new(); @@ -44,11 +47,13 @@ namespace Avalonia.Rendering.Composition internal event Action? AfterCommit; + /// /// Creates a new compositor on a specified render loop that would use a particular GPU /// /// /// + [PrivateApi] public Compositor(IRenderLoop loop, IPlatformGraphics? gpu) { Loop = loop; @@ -112,16 +117,30 @@ namespace Avalonia.Rendering.Composition using (var writer = new BatchStreamWriter(batch.Changes, _batchMemoryPool, _batchObjectPool)) { - foreach (var obj in _objectsForSerialization) + while(_objectSerializationQueue.TryDequeue(out var obj)) { - writer.WriteObject(obj.Server); - obj.SerializeChanges(writer); + var serverObject = obj.TryGetServer(this); + if (serverObject != null) + { + writer.WriteObject(serverObject); + obj.SerializeChanges(this, writer); #if DEBUG_COMPOSITOR_SERIALIZATION - writer.Write(BatchStreamDebugMarkers.ObjectEndMagic); - writer.WriteObject(BatchStreamDebugMarkers.ObjectEndMarker); + writer.Write(BatchStreamDebugMarkers.ObjectEndMagic); + writer.WriteObject(BatchStreamDebugMarkers.ObjectEndMarker); #endif + } + } + _objectSerializationHashSet.Clear(); + + if (_disposeOnNextBatch.Count != 0) + { + writer.WriteObject(ServerCompositor.RenderThreadDisposeStartMarker); + writer.Write(_disposeOnNextBatch.Count); + foreach (var d in _disposeOnNextBatch) + writer.WriteObject(d); + _disposeOnNextBatch.Clear(); } - _objectsForSerialization.Clear(); + if (_pendingServerCompositorJobs.Count > 0) { writer.WriteObject(ServerCompositor.RenderThreadJobsStartMarker); @@ -152,13 +171,20 @@ namespace Avalonia.Rendering.Composition } } - internal void RegisterForSerialization(CompositionObject compositionObject) + internal void RegisterForSerialization(ICompositorSerializable compositionObject) { Dispatcher.UIThread.VerifyAccess(); - _objectsForSerialization.Add(compositionObject); + if(_objectSerializationHashSet.Add(compositionObject)) + _objectSerializationQueue.Enqueue(compositionObject); RequestCommitAsync(); } + internal void DisposeOnNextBatch(SimpleServerObject obj) + { + if (obj is IDisposable disposable && _disposeOnNextBatch.Add(disposable)) + RequestCommitAsync(); + } + /// /// Enqueues a callback to be called before the next scheduled commit. /// If there is no scheduled commit it automatically schedules one @@ -227,5 +253,8 @@ namespace Avalonia.Rendering.Composition return new CompositionInterop(this, feature); } })); + + internal bool UnitTestIsRegisteredForSerialization(ICompositorSerializable serializable) => + _objectSerializationHashSet.Contains(serializable); } } diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs deleted file mode 100644 index 5d45a725c1..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs +++ /dev/null @@ -1,129 +0,0 @@ -using System; -using Avalonia.Collections.Pooled; -using Avalonia.Platform; -using Avalonia.Rendering.Composition.Server; -using Avalonia.Rendering.SceneGraph; -using Avalonia.Utilities; - -// Special license applies License.md - -namespace Avalonia.Rendering.Composition.Drawing; - -/// -/// A list of serialized drawing commands -/// -internal class CompositionDrawList : PooledList> -{ - public CompositionDrawList() - { - - } - - public CompositionDrawList(int capacity) : base(capacity) - { - - } - - public override void Dispose() - { - foreach(var item in this) - item.Dispose(); - base.Dispose(); - } - - public CompositionDrawList Clone() - { - var clone = new CompositionDrawList(Count); - foreach (var r in this) - clone.Add(r.Clone()); - return clone; - } - - public void Render(IDrawingContextImpl canvas) - { - foreach (var cmd in this) - { - if (cmd.Item is IDrawOperationWithTransform hasTransform) - canvas.Transform = hasTransform.Transform; - cmd.Item.Render(canvas); - } - } - - public void Render(IDrawingContextImpl canvas, Matrix transform) - { - foreach (var cmd in this) - { - if (cmd.Item is IDrawOperationWithTransform hasTransform) - canvas.Transform = hasTransform.Transform * transform; - cmd.Item.Render(canvas); - } - } - - - public Rect CalculateBounds() - { - var rect = default(Rect); - foreach (var cmd in this) - rect = rect.Union(cmd.Item.Bounds); - return rect; - } - - public bool HitTest(Point pt) - { - foreach (var op in this) - if (op.Item.HitTest(pt)) - return true; - return false; - } -} - -/// -/// An helper class for building -/// -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); - } -} diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawListSceneBrushContent.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawListSceneBrushContent.cs deleted file mode 100644 index 85bb156475..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawListSceneBrushContent.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Avalonia.Media; -using Avalonia.Media.Immutable; -using Avalonia.Platform; - -namespace Avalonia.Rendering.Composition.Drawing; - -internal class CompositionDrawListSceneBrushContent : ISceneBrushContent -{ - private readonly CompositionDrawList _drawList; - - public CompositionDrawListSceneBrushContent(ImmutableTileBrush brush, CompositionDrawList drawList, Rect rect, bool useScalableRasterization) - { - Brush = brush; - Rect = rect; - UseScalableRasterization = useScalableRasterization; - _drawList = drawList; - } - - public ITileBrush Brush { get; } - public Rect Rect { get; } - - public double Opacity => Brush.Opacity; - public ITransform? Transform => Brush.Transform; - public RelativePoint TransformOrigin => Brush.TransformOrigin; - - public void Dispose() => _drawList.Dispose(); - - public void Render(IDrawingContextImpl context, Matrix? transform) - { - if (transform.HasValue) - _drawList.Render(context, transform.Value); - else - _drawList.Render(context); - } - - public bool UseScalableRasterization { get; } -} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs deleted file mode 100644 index ec419e6313..0000000000 --- a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs +++ /dev/null @@ -1,371 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -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.Threading; - -// Special license applies License.md - -namespace Avalonia.Rendering.Composition; - -/// -/// An IDrawingContextImpl implementation that builds -/// -internal sealed class CompositionDrawingContext : DrawingContext, IDrawingContextWithAcrylicLikeSupport -{ - private CompositionDrawListBuilder _builder = new(); - private int _drawOperationIndex; - - private static ThreadSafeObjectPool> TransformStackPool { get; } = - ThreadSafeObjectPool>.Default; - - private Stack? _transforms; - - private static ThreadSafeObjectPool> OpacityMaskPopStackPool { get; } = - ThreadSafeObjectPool>.Default; - - private Stack? _needsToPopOpacityMask; - - public Matrix Transform { get; set; } = Matrix.Identity; - - public void BeginUpdate(CompositionDrawList? list) - { - _builder.Reset(list); - _drawOperationIndex = 0; - } - - public CompositionDrawList? EndUpdate() - { - // Make sure that any pending pop operations are completed - Dispose(); - - _builder.TrimTo(_drawOperationIndex); - return _builder.DrawOperations; - } - - protected override void DisposeCore() - { - if (_transforms != null) - { - _transforms.Clear(); - TransformStackPool.ReturnAndSetNull(ref _transforms); - } - - if (_needsToPopOpacityMask != null) - { - _needsToPopOpacityMask.Clear(); - _needsToPopOpacityMask = null; - } - } - - protected override void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, brush, pen, geometry)) - { - Add(new GeometryNode(Transform, ConvertBrush(brush), pen, geometry)); - } - else - { - ++_drawOperationIndex; - } - } - - internal override void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect) - { - var next = NextDrawAs(); - - if (next == null || - !next.Item.Equals(Transform, source, opacity, sourceRect, destRect)) - { - Add(new ImageNode(Transform, source, opacity, sourceRect, destRect)); - } - else - { - ++_drawOperationIndex; - } - } - - /// - protected override void DrawLineCore(IPen? pen, Point p1, Point p2) - { - if (pen is null) - { - return; - } - - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, pen, p1, p2)) - { - Add(new LineNode(Transform, pen, p1, p2)); - } - else - { - ++_drawOperationIndex; - } - } - - /// - protected override void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rect, - BoxShadows boxShadows = default) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadows)) - { - Add(new RectangleNode(Transform, ConvertBrush(brush), pen, rect, boxShadows)); - } - else - { - ++_drawOperationIndex; - } - } - - /// - public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, material, rect)) - { - Add(new ExperimentalAcrylicNode(Transform, material, rect)); - } - else - { - ++_drawOperationIndex; - } - } - - protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, brush, pen, rect)) - { - Add(new EllipseNode(Transform, ConvertBrush(brush), pen, rect)); - } - else - { - ++_drawOperationIndex; - } - } - - public override void Custom(ICustomDrawOperation custom) - { - var next = NextDrawAs(); - if (next == null || !next.Item.Equals(Transform, custom)) - Add(new CustomDrawOperation(custom, Transform)); - else - ++_drawOperationIndex; - } - - public override void DrawGlyphRun(IBrush? foreground, GlyphRun glyphRun) - { - if (foreground is null) - { - return; - } - - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, foreground, glyphRun.PlatformImpl)) - { - Add(new GlyphRunNode(Transform, ConvertBrush(foreground)!, glyphRun.PlatformImpl)); - } - - else - { - ++_drawOperationIndex; - } - } - - protected override void PushTransformCore(Matrix matrix) - { - _transforms ??= TransformStackPool.Get(); - _transforms.Push(Transform); - Transform = matrix * Transform; - } - - protected override void PopTransformCore() => - Transform = (_transforms ?? throw new InvalidOperationException()).Pop(); - - protected override void PopClipCore() - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(null)) - { - Add(new ClipNode()); - } - else - { - ++_drawOperationIndex; - } - } - - /// - protected override void PopGeometryClipCore() - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(null)) - { - Add(new GeometryClipNode()); - } - else - { - ++_drawOperationIndex; - } - } - - protected override void PopOpacityCore() - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(null)) - { - Add(new OpacityNode()); - } - else - { - ++_drawOperationIndex; - } - } - - protected override void PopOpacityMaskCore() - { - if (!_needsToPopOpacityMask!.Pop()) - return; - - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(null, null)) - { - Add(new OpacityMaskPopNode()); - } - else - { - ++_drawOperationIndex; - } - } - - - protected override void PushClipCore(Rect clip) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, clip)) - { - Add(new ClipNode(Transform, clip)); - } - else - { - ++_drawOperationIndex; - } - } - - protected override void PushClipCore(RoundedRect clip) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, clip)) - { - Add(new ClipNode(Transform, clip)); - } - else - { - ++_drawOperationIndex; - } - } - - protected override void PushGeometryClipCore(Geometry clip) - { - if (clip.PlatformImpl is null) - return; - - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(Transform, clip.PlatformImpl)) - { - Add(new GeometryClipNode(Transform, clip.PlatformImpl)); - } - else - { - ++_drawOperationIndex; - } - } - - protected override void PushOpacityCore(double opacity, Rect bounds) - { - var next = NextDrawAs(); - - if (next == null || !next.Item.Equals(opacity, bounds)) - { - Add(new OpacityNode(opacity, bounds)); - } - else - { - ++_drawOperationIndex; - } - } - - protected override void PushOpacityMaskCore(IBrush mask, Rect bounds) - { - var next = NextDrawAs(); - - bool needsToPop = true; - if (next == null || !next.Item.Equals(mask, bounds)) - { - var immutableMask = ConvertBrush(mask); - if (immutableMask != null) - Add(new OpacityMaskNode(immutableMask, bounds)); - else - needsToPop = false; - } - else - { - ++_drawOperationIndex; - } - - _needsToPopOpacityMask ??= OpacityMaskPopStackPool.Get(); - _needsToPopOpacityMask.Push(needsToPop); - } - - private void Add(T node) where T : class, IDrawOperation - { - if (_drawOperationIndex < _builder.Count) - { - _builder.ReplaceDrawOperation(_drawOperationIndex, node); - } - else - { - _builder.AddDrawOperation(node); - } - - ++_drawOperationIndex; - } - - private IRef? NextDrawAs() where T : class, IDrawOperation - { - return _drawOperationIndex < _builder.Count - ? _builder.DrawOperations![_drawOperationIndex] as IRef - : null; - } - - private IImmutableBrush? ConvertBrush(IBrush? brush) - { - if (brush is IMutableBrush mutable) - return mutable.ToImmutable(); - if (brush is ISceneBrush sceneBrush) - return sceneBrush.CreateContent(); - return (IImmutableBrush?)brush; - } -} diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderData.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderData.cs new file mode 100644 index 0000000000..0ad70d7102 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderData.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using Avalonia.Media; +using Avalonia.Media.Immutable; +using Avalonia.Rendering.Composition.Drawing.Nodes; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Drawing; + +internal class CompositionRenderData : ICompositorSerializable, IDisposable +{ + private readonly Compositor _compositor; + + public CompositionRenderData(Compositor compositor) + { + _compositor = compositor; + Server = new ServerCompositionRenderData(compositor.Server); + } + + public ServerCompositionRenderData Server { get; } + private PooledInlineList _resources; + private PooledInlineList _items; + private bool _itemsSent; + public void AddResource(ICompositionRenderResource resource) => _resources.Add(resource); + + public void Add(IRenderDataItem item) => _items.Add(item); + + public void Dispose() + { + if (!_itemsSent) + { + foreach(var i in _items) + if (i is IDisposable disp) + disp.Dispose(); + } + + _items.Dispose(); + _itemsSent = false; + foreach(var r in _resources) + r.ReleaseOnCompositor(_compositor); + _resources.Dispose(); + + _compositor.DisposeOnNextBatch(Server); + } + + public SimpleServerObject TryGetServer(Compositor c) => Server; + + public void SerializeChanges(Compositor c, BatchStreamWriter writer) + { + writer.Write(_items.Count); + foreach (var item in _items) + writer.WriteObject(item); + _itemsSent = true; + } + + public bool HitTest(Point pt) + { + foreach (var op in _items) + { + if (op.HitTest(pt)) + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderDataSceneBrushContent.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderDataSceneBrushContent.cs new file mode 100644 index 0000000000..bdeba1ab48 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderDataSceneBrushContent.cs @@ -0,0 +1,47 @@ +using Avalonia.Media; +using Avalonia.Media.Immutable; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Drawing; + +internal class CompositionRenderDataSceneBrushContent : ISceneBrushContent +{ + public CompositionRenderData RenderData { get; } + private Rect? _rect; + + public CompositionRenderDataSceneBrushContent(ITileBrush brush, CompositionRenderData renderData, Rect? rect, + bool useScalableRasterization) + { + Brush = brush; + _rect = rect; + UseScalableRasterization = useScalableRasterization; + RenderData = renderData; + } + + public ITileBrush Brush { get; } + public Rect Rect => _rect ?? (RenderData.Server?.Bounds ?? default); + + public double Opacity => Brush.Opacity; + public ITransform? Transform => Brush.Transform; + public RelativePoint TransformOrigin => Brush.TransformOrigin; + + public void Dispose() + { + // No-op on server + } + + public void Render(IDrawingContextImpl context, Matrix? transform) + { + if (transform.HasValue) + { + var oldTransform = context.Transform; + context.Transform = transform.Value * oldTransform; + RenderData.Server.Render(context); + context.Transform = oldTransform; + } + else + RenderData.Server.Render(context); + } + + public bool UseScalableRasterization { get; } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/CompositorResourceHelpers.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositorResourceHelpers.cs new file mode 100644 index 0000000000..59e0bc848e --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/CompositorResourceHelpers.cs @@ -0,0 +1,123 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Avalonia.Media; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Drawing; + +internal class CompositorRefCountableResource where T : SimpleServerObject +{ + public T Value { get; private set; } + public int RefCount { get; private set; } + + public CompositorRefCountableResource(T value) + { + Value = value; + RefCount = 1; + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void ThrowInvalidOperation() => throw new InvalidOperationException("This resource is disposed"); + + public void AddRef() + { + if (RefCount <= 0) + ThrowInvalidOperation(); + RefCount++; + } + + public bool Release(Compositor c) + { + if (RefCount <= 0) + ThrowInvalidOperation(); + RefCount--; + if (RefCount == 0) + { + c.DisposeOnNextBatch(Value); + return true; + } + + return false; + } +} + +internal struct CompositorResourceHolder where T : SimpleServerObject +{ + private InlineDictionary> _dictionary; + + public bool IsAttached => _dictionary.HasEntries; + + public bool CreateOrAddRef(Compositor compositor, ICompositorSerializable owner, out T resource, Func factory) + { + if (_dictionary.TryGetValue(compositor, out var handle)) + { + handle.AddRef(); + resource = handle.Value; + return false; + } + + resource = factory(compositor); + _dictionary.Add(compositor, new CompositorRefCountableResource(resource)); + compositor.RegisterForSerialization(owner); + return true; + } + + public T? TryGetForCompositor(Compositor compositor) + { + if (_dictionary.TryGetValue(compositor, out var handle)) + return handle.Value; + return default; + } + + public T GetForCompositor(Compositor compositor) + { + if (_dictionary.TryGetValue(compositor, out var handle)) + return handle.Value; + ThrowDoesNotExist(); + return default; + } + + [MethodImpl(MethodImplOptions.NoInlining), DoesNotReturn] + static void ThrowDoesNotExist() => throw new InvalidOperationException("This resource doesn't exist on that compositor"); + + public bool Release(Compositor compositor) + { + if (!_dictionary.TryGetValue(compositor, out var handle)) + ThrowDoesNotExist(); + if (handle.Release(compositor)) + { + _dictionary.Remove(compositor); + return true; + } + + return false; + } + + public void ProcessPropertyChangeNotification(AvaloniaPropertyChangedEventArgs change) + { + if (change.OldValue is ICompositionRenderResource oldResource) + TransitiveReleaseAll(oldResource); + if (change.NewValue is ICompositionRenderResource newResource) + TransitiveAddRefAll(newResource); + } + + public void TransitiveReleaseAll(ICompositionRenderResource oldResource) + { + foreach(var kv in _dictionary) + oldResource.ReleaseOnCompositor(kv.Key); + } + + public void TransitiveAddRefAll(ICompositionRenderResource newResource) + { + foreach (var kv in _dictionary) + newResource.AddRefOnCompositor(kv.Key); + } + + public void RegisterForInvalidationOnAllCompositors(ICompositorSerializable serializable) + { + foreach (var kv in _dictionary) + kv.Key.RegisterForSerialization(serializable); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/ICompositionRenderResource.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/ICompositionRenderResource.cs new file mode 100644 index 0000000000..b061abbc50 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/ICompositionRenderResource.cs @@ -0,0 +1,12 @@ +namespace Avalonia.Rendering.Composition.Drawing; + +internal interface ICompositionRenderResource +{ + void AddRefOnCompositor(Compositor c); + void ReleaseOnCompositor(Compositor c); +} + +internal interface ICompositionRenderResource : ICompositionRenderResource where T : class +{ + T GetForCompositor(Compositor c); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/ImmediateRenderDataSceneBrushContent.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/ImmediateRenderDataSceneBrushContent.cs new file mode 100644 index 0000000000..00f75ec8f8 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/ImmediateRenderDataSceneBrushContent.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Drawing.Nodes; +using Avalonia.Threading; + +namespace Avalonia.Rendering.Composition.Drawing; + +internal class ImmediateRenderDataSceneBrushContent : ISceneBrushContent +{ + private List? _items; + private readonly ThreadSafeObjectPool> _pool; + + public ImmediateRenderDataSceneBrushContent(ITileBrush brush, List items, Rect? rect, + bool useScalableRasterization, ThreadSafeObjectPool> pool) + { + Brush = brush; + _items = items; + _pool = pool; + UseScalableRasterization = useScalableRasterization; + if (rect == null) + { + foreach (var i in _items) + rect = Rect.Union(rect, i.Bounds); + rect = ServerCompositionRenderData.ApplyRenderBoundsRounding(rect); + } + + Rect = rect ?? default; + } + + public ITileBrush Brush { get; } + public Rect Rect { get; } + + public double Opacity => Brush.Opacity; + public ITransform? Transform => Brush.Transform; + public RelativePoint TransformOrigin => Brush.TransformOrigin; + + public void Dispose() + { + if(_items == null) + return; + foreach (var i in _items) + (i as IDisposable)?.Dispose(); + _items.Clear(); + _pool.ReturnAndSetNull(ref _items); + } + + void Render(IDrawingContextImpl context) + { + if (_items == null) + return; + + var ctx = new RenderDataNodeRenderContext(context); + try + { + foreach (var i in _items) + i.Invoke(ref ctx); + } + finally + { + ctx.Dispose(); + } + } + + public void Render(IDrawingContextImpl context, Matrix? transform) + { + if (transform.HasValue) + { + var oldTransform = context.Transform; + context.Transform = transform.Value * oldTransform; + Render(context); + context.Transform = oldTransform; + } + else + Render(context); + } + + public bool UseScalableRasterization { get; } + +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataBitmapNode.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataBitmapNode.cs new file mode 100644 index 0000000000..11a1b87fc9 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataBitmapNode.cs @@ -0,0 +1,28 @@ +using System; +using Avalonia.Platform; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Drawing.Nodes; + +class RenderDataBitmapNode : IRenderDataItem, IDisposable +{ + public IRef? Bitmap { get; set; } + public double Opacity { get; set; } + public Rect SourceRect { get; set; } + public Rect DestRect { get; set; } + + public bool HitTest(Point p) => DestRect.Contains(p); + + public void Invoke(ref RenderDataNodeRenderContext context) + { + if (Bitmap != null) + context.Context.DrawBitmap(Bitmap.Item, Opacity, SourceRect, DestRect); + } + + public Rect? Bounds => DestRect; + public void Dispose() + { + Bitmap?.Dispose(); + Bitmap = null; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataEllipseNode.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataEllipseNode.cs new file mode 100644 index 0000000000..4e77281cc5 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataEllipseNode.cs @@ -0,0 +1,61 @@ +using System; +using Avalonia.Media; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Drawing.Nodes; + +class RenderDataEllipseNode :RenderDataBrushAndPenNode +{ + public Rect Rect { get; set; } + + bool Contains(double dx, double dy, double radiusX, double radiusY) + { + var rx2 = radiusX * radiusX; + var ry2 = radiusY * radiusY; + + var distance = ry2 * dx * dx + rx2 * dy * dy; + + return distance <= rx2 * ry2; + } + + public override bool HitTest(Point p) + { + var center = Rect.Center; + + var strokeThickness = ClientPen?.Thickness ?? 0; + + var rx = Rect.Width / 2 + strokeThickness / 2; + var ry = Rect.Height / 2 + strokeThickness / 2; + + var dx = p.X - center.X; + var dy = p.Y - center.Y; + + if (Math.Abs(dx) > rx || Math.Abs(dy) > ry) + { + return false; + } + + if (ServerBrush != null) + { + return Contains(dx, dy, rx, ry); + } + else if (strokeThickness > 0) + { + bool inStroke = Contains(dx, dy, rx, ry); + + rx = Rect.Width / 2 - strokeThickness / 2; + ry = Rect.Height / 2 - strokeThickness / 2; + + bool inInner = Contains(dx, dy, rx, ry); + + return inStroke && !inInner; + } + + return false; + } + + public override void Invoke(ref RenderDataNodeRenderContext context) => + context.Context.DrawEllipse(ServerBrush, ServerPen, Rect); + + public override Rect? Bounds => Rect.Inflate(ServerPen?.Thickness ?? 0); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGeometryNode.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGeometryNode.cs new file mode 100644 index 0000000000..67231c9012 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGeometryNode.cs @@ -0,0 +1,29 @@ +using System.Diagnostics; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; + +namespace Avalonia.Rendering.Composition.Drawing.Nodes; + +class RenderDataGeometryNode : RenderDataBrushAndPenNode +{ + public IGeometryImpl? Geometry { get; set; } + + public override bool HitTest(Point p) + { + if (Geometry == null) + return false; + + return (ServerBrush != null // null check is safe + && Geometry.FillContains(p)) || + (ClientPen != null && Geometry.StrokeContains(ClientPen, p)); + } + + public override void Invoke(ref RenderDataNodeRenderContext context) + { + Debug.Assert(Geometry != null); + context.Context.DrawGeometry(ServerBrush, ServerPen, Geometry!); + } + + public override Rect? Bounds => Geometry?.GetRenderBounds(ServerPen).CalculateBoundsWithLineCaps(ServerPen) ?? default; +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGlyphRunNode.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGlyphRunNode.cs new file mode 100644 index 0000000000..ca2748cd55 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGlyphRunNode.cs @@ -0,0 +1,35 @@ +using System; +using System.Diagnostics; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Drawing.Nodes; + +class RenderDataGlyphRunNode : IRenderDataItemWithServerResources, IDisposable +{ + public IBrush? ServerBrush { get; set; } + // Dispose only happens once, so it's safe to have one reference + public IRef? GlyphRun { get; set; } + + public bool HitTest(Point p) => GlyphRun?.Item.Bounds.ContainsExclusive(p) ?? false; + + public void Invoke(ref RenderDataNodeRenderContext context) + { + Debug.Assert(GlyphRun!.Item != null); + context.Context.DrawGlyphRun(ServerBrush, GlyphRun.Item); + } + + public Rect? Bounds => GlyphRun?.Item?.Bounds ?? default; + + public void Collect(IRenderDataServerResourcesCollector collector) + { + collector.AddRenderDataServerResource(ServerBrush); + } + + public void Dispose() + { + GlyphRun?.Dispose(); + GlyphRun = null; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataLineNode.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataLineNode.cs new file mode 100644 index 0000000000..6b6b9bdb01 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataLineNode.cs @@ -0,0 +1,65 @@ +using System; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; + +namespace Avalonia.Rendering.Composition.Drawing.Nodes; + +class RenderDataLineNode : IRenderDataItemWithServerResources +{ + public IPen? ServerPen { get; set; } + public IPen? ClientPen { get; set; } + public Point P1 { get; set; } + public Point P2 { get; set; } + + public bool HitTest(Point p) + { + if (ClientPen == null) + return false; + var halfThickness = ClientPen.Thickness / 2; + var minX = Math.Min(P1.X, P2.X) - halfThickness; + var maxX = Math.Max(P1.X, P2.X) + halfThickness; + var minY = Math.Min(P1.Y, P2.Y) - halfThickness; + var maxY = Math.Max(P1.Y, P2.Y) + halfThickness; + + if (p.X < minX || p.X > maxX || p.Y < minY || p.Y > maxY) + return false; + + var a = P1; + var b = P2; + + //If dot1 or dot2 is negative, then the angle between the perpendicular and the segment is obtuse. + //The distance from a point to a straight line is defined as the + //length of the vector formed by the point and the closest point of the segment + + Vector ap = p - a; + var dot1 = Vector.Dot(b - a, ap); + + if (dot1 < 0) + return ap.Length <= ClientPen.Thickness / 2; + + Vector bp = p - b; + var dot2 = Vector.Dot(a - b, bp); + + if (dot2 < 0) + return bp.Length <= halfThickness; + + var bXaX = b.X - a.X; + var bYaY = b.Y - a.Y; + + var distance = (bXaX * (p.Y - a.Y) - bYaY * (p.X - a.X)) / + (Math.Sqrt(bXaX * bXaX + bYaY * bYaY)); + + return Math.Abs(distance) <= halfThickness; + } + + + public void Invoke(ref RenderDataNodeRenderContext context) + => context.Context.DrawLine(ServerPen, P1, P2); + + public Rect? Bounds => LineBoundsHelper.CalculateBounds(P1, P2, ServerPen!); + public void Collect(IRenderDataServerResourcesCollector collector) + { + collector.AddRenderDataServerResource(ServerPen); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataNodes.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataNodes.cs new file mode 100644 index 0000000000..3012432e12 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataNodes.cs @@ -0,0 +1,214 @@ +using System; +using System.Collections.Generic; +using Avalonia.Media; +using Avalonia.Platform; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Threading; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Drawing.Nodes; + +enum RenderDataPopNodeType +{ + Transform, + Clip, + GeometryClip, + Opacity, + OpacityMask +} + +interface IRenderDataServerResourcesCollector +{ + void AddRenderDataServerResource(object? obj); +} + +interface IRenderDataItemWithServerResources : IRenderDataItem +{ + void Collect(IRenderDataServerResourcesCollector collector); +} + +struct RenderDataNodeRenderContext : IDisposable +{ + private Stack? _stack; + private static readonly ThreadSafeObjectPool> s_matrixStackPool = new(); + + public RenderDataNodeRenderContext(IDrawingContextImpl context) + { + Context = context; + } + public IDrawingContextImpl Context { get; } + + public Stack MatrixStack + { + get => _stack ??= s_matrixStackPool.Get(); + } + + public void Dispose() + { + if (_stack != null) + { + _stack.Clear(); + s_matrixStackPool.ReturnAndSetNull(ref _stack); + } + } +} + +interface IRenderDataItem +{ + /// + /// Renders the node to a drawing context. + /// + /// The drawing context. + void Invoke(ref RenderDataNodeRenderContext context); + + /// + /// Gets the bounds of the visible content in the node in global coordinates. + /// + Rect? Bounds { get; } + + /// + /// Hit test the geometry in this node. + /// + /// The point in global coordinates. + /// True if the point hits the node's geometry; otherwise false. + /// + /// This method does not recurse to childs, if you want + /// to hit test children they must be hit tested manually. + /// + bool HitTest(Point p); +} + +class RenderDataCustomNode : IRenderDataItem +{ + public ICustomDrawOperation? Operation { get; set; } + public bool HitTest(Point p) => Operation?.HitTest(p) ?? false; + public void Invoke(ref RenderDataNodeRenderContext context) => Operation?.Render(new(context.Context, false)); + + public Rect? Bounds => Operation?.Bounds; +} + +abstract class RenderDataPushNode : IRenderDataItem, IDisposable +{ + public PooledInlineList Children; + public abstract void Push(ref RenderDataNodeRenderContext context); + public abstract void Pop(ref RenderDataNodeRenderContext context); + public void Invoke(ref RenderDataNodeRenderContext context) + { + if (Children.Count == 0) + return; + Push(ref context); + foreach (var ch in Children) + ch.Invoke(ref context); + Pop(ref context); + } + + public virtual Rect? Bounds + { + get + { + if (Children.Count == 0) + return null; + Rect? union = null; + foreach (var i in Children) + union = Rect.Union(union, i.Bounds); + return union; + } + } + + public virtual bool HitTest(Point p) + { + if (Children.Count == 0) + return false; + foreach(var ch in Children) + if (ch.HitTest(p)) + return true; + return false; + } + + public void Dispose() + { + if (Children.Count > 0) + { + foreach(var ch in Children) + if (ch is RenderDataPushNode node) + node.Dispose(); + Children.Dispose(); + } + } +} + +class RenderDataClipNode : RenderDataPushNode +{ + public RoundedRect Rect { get; set; } + public override void Push(ref RenderDataNodeRenderContext context) => + context.Context.PushClip(Rect); + + public override void Pop(ref RenderDataNodeRenderContext context) => + context.Context.PopClip(); + + public override bool HitTest(Point p) + { + if (!Rect.Rect.Contains(p)) + return false; + return base.HitTest(p); + } +} + +class RenderDataGeometryClipNode : RenderDataPushNode +{ + public IGeometryImpl? Geometry { get; set; } + public bool Contains(Point p) => Geometry?.FillContains(p) ?? false; + + public override void Push(ref RenderDataNodeRenderContext context) + { + if (Geometry != null) + context.Context.PushGeometryClip(Geometry); + } + + public override void Pop(ref RenderDataNodeRenderContext context) + { + if (Geometry != null) + context.Context.PopGeometryClip(); + } + + public override bool HitTest(Point p) + { + if (Geometry != null && !Geometry.FillContains(p)) + return false; + return base.HitTest(p); + } +} + +class RenderDataOpacityNode : RenderDataPushNode +{ + public double Opacity { get; set; } + public Rect BoundsRect { get; set; } + public override void Push(ref RenderDataNodeRenderContext context) + { + if (Opacity != 1) + context.Context.PushOpacity(Opacity, BoundsRect); + } + + public override void Pop(ref RenderDataNodeRenderContext context) + { + if (Opacity != 1) + context.Context.PopOpacity(); + } +} + +abstract class RenderDataBrushAndPenNode : IRenderDataItemWithServerResources +{ + public IBrush? ServerBrush { get; set; } + public IPen? ServerPen { get; set; } + public IPen? ClientPen { get; set; } + + public void Collect(IRenderDataServerResourcesCollector collector) + { + collector.AddRenderDataServerResource(ServerBrush); + collector.AddRenderDataServerResource(ServerPen); + } + + public abstract void Invoke(ref RenderDataNodeRenderContext context); + public abstract Rect? Bounds { get; } + public abstract bool HitTest(Point p); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataPushMatrixNode.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataPushMatrixNode.cs new file mode 100644 index 0000000000..2ccfa0e7a5 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataPushMatrixNode.cs @@ -0,0 +1,27 @@ +namespace Avalonia.Rendering.Composition.Drawing.Nodes; + +class RenderDataPushMatrixNode : RenderDataPushNode +{ + public Matrix Matrix { get; set; } + + public override void Push(ref RenderDataNodeRenderContext context) + { + var current = context.Context.Transform; + context.MatrixStack.Push(current); + context.Context.Transform = Matrix * current; + } + + public override void Pop(ref RenderDataNodeRenderContext context) + { + context.Context.Transform = context.MatrixStack.Pop(); + } + + public override bool HitTest(Point p) + { + if (Matrix.TryInvert(out var inverted)) + return base.HitTest(p.Transform(inverted)); + return false; + } + + public override Rect? Bounds => base.Bounds?.TransformToAABB(Matrix); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataPushOpacityMaskNode.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataPushOpacityMaskNode.cs new file mode 100644 index 0000000000..14607282da --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataPushOpacityMaskNode.cs @@ -0,0 +1,25 @@ +using Avalonia.Media; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Drawing.Nodes; + +class RenderDataOpacityMaskNode : RenderDataPushNode, IRenderDataItemWithServerResources +{ + public IBrush? ServerBrush { get; set; } + + public Rect BoundsRect { get; set; } + + public void Collect(IRenderDataServerResourcesCollector collector) + { + collector.AddRenderDataServerResource(ServerBrush); + } + + public override void Push(ref RenderDataNodeRenderContext context) + { + if (ServerBrush != null) + context.Context.PushOpacityMask(ServerBrush, BoundsRect); + } + + public override void Pop(ref RenderDataNodeRenderContext context) => + context.Context.PopOpacityMask(); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataRectangleNode.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataRectangleNode.cs new file mode 100644 index 0000000000..df6b478e83 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataRectangleNode.cs @@ -0,0 +1,30 @@ +using Avalonia.Media; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Drawing.Nodes; + +class RenderDataRectangleNode : RenderDataBrushAndPenNode +{ + public RoundedRect Rect { get; set; } + public BoxShadows BoxShadows { get; set; } + + public override bool HitTest(Point p) + { + if (ServerBrush != null) // it's safe to check for null + { + var rect = Rect.Rect.Inflate((ClientPen?.Thickness / 2) ?? 0); + return rect.ContainsExclusive(p); + } + else + { + var borderRect = Rect.Rect.Inflate((ClientPen?.Thickness / 2) ?? 0); + var emptyRect = Rect.Rect.Deflate((ClientPen?.Thickness / 2) ?? 0); + return borderRect.ContainsExclusive(p) && !emptyRect.ContainsExclusive(p); + } + } + + public override void Invoke(ref RenderDataNodeRenderContext context) => + context.Context.DrawRectangle(ServerBrush, ServerPen, Rect, BoxShadows); + + public override Rect? Bounds => BoxShadows.TransformBounds(Rect.Rect).Inflate((ServerPen?.Thickness ?? 0) / 2); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataDrawingContext.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataDrawingContext.cs new file mode 100644 index 0000000000..3d5033086e --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataDrawingContext.cs @@ -0,0 +1,349 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Avalonia.Media; +using Avalonia.Media.Immutable; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Drawing.Nodes; +using Avalonia.Rendering.SceneGraph; +using Avalonia.Threading; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Drawing; + +internal class RenderDataDrawingContext : DrawingContext +{ + private readonly Compositor? _compositor; + private CompositionRenderData? _renderData; + private HashSet? _resourcesHashSet; + private static readonly ThreadSafeObjectPool> s_hashSetPool = new(); + private CompositionRenderData RenderData + { + get + { + Debug.Assert(_compositor != null); + return _renderData ??= new(_compositor); + } + } + + struct ParentStackItem + { + public RenderDataPushNode? Node; + public List Items; + } + + private List? _currentItemList; + private static readonly ThreadSafeObjectPool> s_listPool = new(); + + private Stack? _parentNodeStack; + private static readonly ThreadSafeObjectPool> s_parentStackPool = new(); + + public RenderDataDrawingContext(Compositor? compositor) + { + _compositor = compositor; + } + + void Add(IRenderDataItem item) + { + _currentItemList ??= s_listPool.Get(); + _currentItemList.Add(item); + } + + void Push(RenderDataPushNode? node = null) + { + // Push a fake no-op node so something could be popped by the corresponding Pop call + // Since there is no nesting, we don't update the item list + if (node == null) + { + (_parentNodeStack ??= s_parentStackPool.Get()).Push(default); + return; + } + Add(node); + (_parentNodeStack ??= s_parentStackPool.Get()).Push(new ParentStackItem + { + Node = node, + Items = _currentItemList! + }); + _currentItemList = null; + } + + void Pop() where T : IRenderDataItem + { + var parent = _parentNodeStack!.Pop(); + + // No-op node + if (parent.Node == null) + return; + + if (!(parent.Node is T)) + throw new InvalidOperationException("Invalid Pop operation"); + + foreach(var item in _currentItemList!) + parent.Node.Children.Add(item); + _currentItemList.Clear(); + s_listPool.ReturnAndSetNull(ref _currentItemList); + _currentItemList = parent.Items; + } + + void AddResource(object? resource) + { + if (_compositor == null) + return; + + if (resource == null + || resource is IImmutableBrush + || resource is ImmutablePen + || resource is ImmutableTransform) + return; + + if (resource is ICompositionRenderResource renderResource) + { + _resourcesHashSet ??= s_hashSetPool.Get(); + if (!_resourcesHashSet.Add(renderResource)) + return; + + renderResource.AddRefOnCompositor(_compositor); + RenderData.AddResource(renderResource); + return; + } + + throw new InvalidOperationException(resource.GetType().FullName + " can not be used with this DrawingContext"); + } + + protected override void DrawLineCore(IPen? pen, Point p1, Point p2) + { + if(pen == null) + return; + AddResource(pen); + Add(new RenderDataLineNode + { + ClientPen = pen, + ServerPen = pen.GetServer(_compositor), + P1 = p1, + P2 = p2 + }); + } + + protected override void DrawGeometryCore(IBrush? brush, IPen? pen, IGeometryImpl geometry) + { + if (brush == null && pen == null) + return; + AddResource(brush); + AddResource(pen); + Add(new RenderDataGeometryNode + { + ServerBrush = brush.GetServer(_compositor), + ServerPen = pen.GetServer(_compositor), + ClientPen = pen, + Geometry = geometry + }); + } + + protected override void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rrect, BoxShadows boxShadows = default) + { + if (rrect.IsEmpty()) + return; + if(brush == null && pen == null && boxShadows == default) + return; + AddResource(brush); + AddResource(pen); + Add(new RenderDataRectangleNode + { + ServerBrush = brush.GetServer(_compositor), + ServerPen = pen.GetServer(_compositor), + ClientPen = pen, + Rect = rrect, + BoxShadows = boxShadows + }); + } + + protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect) + { + if (rect.IsEmpty()) + return; + + if(brush == null && pen == null) + return; + AddResource(brush); + AddResource(pen); + Add(new RenderDataEllipseNode + { + ServerBrush = brush.GetServer(_compositor), + ServerPen = pen.GetServer(_compositor), + ClientPen = pen, + Rect = rect, + }); + } + + public override void Custom(ICustomDrawOperation custom) => Add(new RenderDataCustomNode()); + + public override void DrawGlyphRun(IBrush? foreground, GlyphRun? glyphRun) + { + if (foreground == null || glyphRun == null) + return; + AddResource(foreground); + Add(new RenderDataGlyphRunNode + { + ServerBrush = foreground.GetServer(_compositor), + GlyphRun = glyphRun.PlatformImpl.Clone() + }); + } + + protected override void PushClipCore(RoundedRect rect) => Push(new RenderDataClipNode + { + Rect = rect + }); + + protected override void PushClipCore(Rect rect) => Push(new RenderDataClipNode + { + Rect = rect + }); + + protected override void PushGeometryClipCore(Geometry? clip) + { + if (clip == null) + Push(); + else + Push(new RenderDataGeometryClipNode + { + Geometry = clip?.PlatformImpl + }); + } + + protected override void PushOpacityCore(double opacity, Rect bounds) + { + if (opacity == 1) + Push(); + else + Push(new RenderDataOpacityNode + { + Opacity = opacity, + BoundsRect = bounds + }); + } + + protected override void PushOpacityMaskCore(IBrush? mask, Rect bounds) + { + if(mask == null) + Push(); + else + { + AddResource(mask); + Push(new RenderDataOpacityMaskNode + { + ServerBrush = mask.GetServer(_compositor), + BoundsRect = bounds + }); + } + } + + protected override void PushTransformCore(Matrix matrix) + { + if (matrix.IsIdentity) + Push(); + else + Push(new RenderDataPushMatrixNode() + { + Matrix = matrix + }); + } + + protected override void PopClipCore() => Pop(); + + protected override void PopGeometryClipCore() => Pop(); + + protected override void PopOpacityCore() => Pop(); + + protected override void PopOpacityMaskCore() => Pop(); + + protected override void PopTransformCore() => Pop(); + + internal override void DrawBitmap(IRef? source, double opacity, Rect sourceRect, Rect destRect) + { + if (source == null || sourceRect.IsEmpty() || destRect.IsEmpty()) + return; + Add(new RenderDataBitmapNode + { + Bitmap = source.Clone(), + Opacity = opacity, + SourceRect = sourceRect, + DestRect = destRect + }); + } + + + void FlushStack() + { + // Flush stack + if (_parentNodeStack != null) + { + // TODO: throw error, unbalanced stack + while (_parentNodeStack.Count > 0) + Pop(); + } + + + } + + public CompositionRenderData? GetRenderResults() + { + Debug.Assert(_compositor != null); + + FlushStack(); + + // Transfer items to RenderData + if (_currentItemList is { Count: > 0 }) + { + foreach (var i in _currentItemList) + RenderData.Add(i); + _currentItemList.Clear(); + } + + var rv = _renderData; + _renderData = null; + _resourcesHashSet?.Clear(); + + if (rv != null) + _compositor.RegisterForSerialization(rv); + return rv; + } + + public ImmediateRenderDataSceneBrushContent? GetImmediateSceneBrushContent(ITileBrush brush, Rect? rect, bool useScalableRasterization) + { + Debug.Assert(_compositor == null); + Debug.Assert(_resourcesHashSet == null); + Debug.Assert(_renderData == null); + + FlushStack(); + if (_currentItemList == null || _currentItemList.Count == 0) + return null; + + var itemList = _currentItemList; + _currentItemList = null; + + return new ImmediateRenderDataSceneBrushContent(brush, itemList, rect, useScalableRasterization, s_listPool); + } + + public void Reset() + { + // This means that render data should be discarded + if (_renderData != null) + { + _renderData.Dispose(); + _renderData = null; + } + + _currentItemList?.Clear(); + _parentNodeStack?.Clear(); + _resourcesHashSet?.Clear(); + } + + protected override void DisposeCore() + { + Reset(); + if (_resourcesHashSet != null) + s_hashSetPool.ReturnAndSetNull(ref _resourcesHashSet); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionRenderData.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionRenderData.cs new file mode 100644 index 0000000000..cb5e21be00 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionRenderData.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Avalonia.Platform; +using Avalonia.Rendering.Composition.Drawing.Nodes; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; +using Avalonia.Threading; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Drawing; + +class ServerCompositionRenderData : SimpleServerRenderResource +{ + private PooledInlineList _items; + private PooledInlineList _referencedResources; + private Rect? _bounds; + private bool _boundsValid; + private static readonly ThreadSafeObjectPool s_resourceHashSetPool = new(); + + public ServerCompositionRenderData(ServerCompositor compositor) : base(compositor) + { + } + + class Collector : IRenderDataServerResourcesCollector + { + public readonly HashSet Resources = new(); + public void AddRenderDataServerResource(object? obj) + { + if (obj is IServerRenderResource res) + Resources.Add(res); + } + } + + protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt) + { + Reset(); + + var count = reader.Read(); + _items.EnsureCapacity(count); + for (var c = 0; c < count; c++) + _items.Add(reader.ReadObject()); + + var collector = s_resourceHashSetPool.Get(); + foreach(var item in _items) + if (item is IRenderDataItemWithServerResources resourceItem) + resourceItem.Collect(collector); + + foreach (var r in collector.Resources) + { + _referencedResources.Add(r); + r.AddObserver(this); + } + + collector.Resources.Clear(); + s_resourceHashSetPool.ReturnAndSetNull(ref collector); + + base.DeserializeChangesCore(reader, committedAt); + } + + public Rect? Bounds + { + get + { + if (!_boundsValid) + { + _bounds = CalculateRenderBounds(); + _boundsValid = true; + } + return _bounds; + } + } + + private Rect? CalculateRenderBounds() + { + Rect? totalBounds = null; + foreach (var item in _items) + totalBounds = Rect.Union(totalBounds, item.Bounds); + + return ApplyRenderBoundsRounding(totalBounds); + } + + public static Rect? ApplyRenderBoundsRounding(Rect? rect) + { + if (rect != null) + { + var r = rect.Value; + // I don't believe that it's correct to do here (rather than in CompositionVisual), + // but it's the old behavior, so I'm keeping it for now + return new Rect( + new Point(Math.Floor(r.X), Math.Floor(r.Y)), + new Point(Math.Ceiling(r.Right), Math.Ceiling(r.Bottom))); + } + + return null; + } + + public override void DependencyQueuedInvalidate(IServerRenderResource sender) + { + _boundsValid = false; + base.DependencyQueuedInvalidate(sender); + } + + public void Render(IDrawingContextImpl context) + { + var ctx = new RenderDataNodeRenderContext(context); + try + { + foreach (var item in _items) + item.Invoke(ref ctx); + } + finally + { + ctx.Dispose(); + } + } + + void Reset() + { + _bounds = null; + _boundsValid = false; + foreach (var r in _referencedResources) + r.RemoveObserver(this); + _referencedResources.Dispose(); + foreach(var i in _items) + if (i is IDisposable disp) + disp.Dispose(); + _items.Dispose(); + } + + public override void Dispose() + { + Reset(); + base.Dispose(); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionSimplePen.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionSimplePen.cs new file mode 100644 index 0000000000..8e99a1cc01 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionSimplePen.cs @@ -0,0 +1,12 @@ +using System; +using Avalonia.Media; +using Avalonia.Media.Immutable; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server; + +internal partial class ServerCompositionSimplePen : IPen +{ + IDashStyle? IPen.DashStyle => DashStyle; +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Drawing/ServerResourceHelperExtensions.cs b/src/Avalonia.Base/Rendering/Composition/Drawing/ServerResourceHelperExtensions.cs new file mode 100644 index 0000000000..63b81a742c --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Drawing/ServerResourceHelperExtensions.cs @@ -0,0 +1,56 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Avalonia.Media; +using Avalonia.Media.Immutable; + +namespace Avalonia.Rendering.Composition.Drawing; + +static class ServerResourceHelperExtensions +{ + public static IBrush? GetServer(this IBrush? brush, Compositor? compositor) + { + if (compositor == null) + return brush; + if (brush == null) + return null; + if (brush is IImmutableBrush immutable) + return immutable; + if (brush is ICompositionRenderResource resource) + return resource.GetForCompositor(compositor); + ThrowNotCompatible(brush); + return null; + } + + public static IPen? GetServer(this IPen? pen, Compositor? compositor) + { + if (compositor == null) + return pen; + if (pen == null) + return null; + if (pen is ImmutablePen immutable) + return immutable; + if (pen is ICompositionRenderResource resource) + return resource.GetForCompositor(compositor); + ThrowNotCompatible(pen); + return null; + } + + [MethodImpl(MethodImplOptions.NoInlining), DoesNotReturn] + static void ThrowNotCompatible(object o) => + throw new InvalidOperationException(o.GetType() + " is not compatible with composition"); + + public static ITransform? GetServer(this ITransform? transform, Compositor? compositor) + { + if (compositor == null) + return transform; + if (transform == null) + return null; + if (transform is ImmutableTransform immutable) + return immutable; + if (transform is ICompositionRenderResource resource) + resource.GetForCompositor(compositor); + ThrowNotCompatible(transform); + return null; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/ICompositorSerializable.cs b/src/Avalonia.Base/Rendering/Composition/ICompositorSerializable.cs new file mode 100644 index 0000000000..03a95f52e7 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/ICompositorSerializable.cs @@ -0,0 +1,10 @@ +using Avalonia.Rendering.Composition.Server; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition; + +internal interface ICompositorSerializable +{ + SimpleServerObject? TryGetServer(Compositor c); + void SerializeChanges(Compositor c, BatchStreamWriter writer); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs b/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs index 04e40e8744..138b791019 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs @@ -68,7 +68,7 @@ namespace Avalonia.Rendering.Composition.Server var effectiveChar = c is >= FirstChar and <= LastChar ? c : ' '; var run = _runs[effectiveChar - FirstChar]; context.Transform = originalTransform * Matrix.CreateTranslation(offset, 0.0); - context.DrawGlyphRun(foreground, run.PlatformImpl); + context.DrawGlyphRun(foreground, run.PlatformImpl.Item); offset += run.Bounds.Width; } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs index 5a4890e568..b735b106d8 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs @@ -2,6 +2,7 @@ using System; using System.Numerics; using Avalonia.Media; using Avalonia.Media.Imaging; +using Avalonia.Media.Immutable; using Avalonia.Platform; using Avalonia.Rendering.Composition.Drawing; using Avalonia.Rendering.SceneGraph; @@ -53,12 +54,12 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, _impl.Clear(color); } - public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect) + public void DrawBitmap(IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect) { _impl.DrawBitmap(source, opacity, sourceRect, destRect); } - public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) + public void DrawBitmap(IBitmapImpl source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) { _impl.DrawBitmap(source, opacityMask, opacityMaskRect, destRect); } @@ -83,7 +84,7 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, _impl.DrawEllipse(brush, pen, rect); } - public void DrawGlyphRun(IBrush? foreground, IRef glyphRun) + public void DrawGlyphRun(IBrush? foreground, IGlyphRunImpl glyphRun) { _impl.DrawGlyphRun(foreground, glyphRun); } @@ -145,6 +146,8 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl, { if (_impl is IDrawingContextWithAcrylicLikeSupport acrylic) acrylic.DrawRectangle(material, rect); + else + _impl.DrawRectangle(new ImmutableSolidColorBrush(material.FallbackColor), null, rect); } public void PushEffect(IEffect effect) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index aebe3a5cdd..763ec3b5f6 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -20,7 +20,7 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua // This is needed for debugging purposes so we could see inspect the associated visual from debugger public readonly Visual UiVisual; #endif - private CompositionDrawList? _renderCommands; + private ServerCompositionRenderData? _renderCommands; public ServerCompositionDrawListVisual(ServerCompositor compositor, Visual v) : base(compositor) { @@ -29,32 +29,14 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua #endif } - Rect? _contentBounds; - - public override Rect OwnContentBounds - { - get - { - if (_contentBounds == null) - { - var rect = default(Rect); - if(_renderCommands!=null) - foreach (var cmd in _renderCommands) - rect = rect.Union(cmd.Item.Bounds); - _contentBounds = rect; - } - - return _contentBounds.Value; - } - } + public override Rect OwnContentBounds => _renderCommands?.Bounds ?? default; protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt) { if (reader.Read() == 1) { _renderCommands?.Dispose(); - _renderCommands = reader.ReadObject(); - _contentBounds = null; + _renderCommands = reader.ReadObject(); } base.DeserializeChangesCore(reader, committedAt); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs new file mode 100644 index 0000000000..f0bcd7bc92 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs @@ -0,0 +1,26 @@ +using Avalonia.Media.Immutable; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server; + +internal partial class ServerCompositionExperimentalAcrylicVisual +{ + protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) + { + var cornerRadius = CornerRadius; + canvas.DrawRectangle( + Material, + new RoundedRect( + new Rect(0, 0, Size.X, Size.Y), + cornerRadius.TopLeft, cornerRadius.TopRight, + cornerRadius.BottomRight, cornerRadius.BottomLeft)); + + base.RenderCore(canvas, currentTransformedClip); + } + + public override Rect OwnContentBounds => new(0, 0, Size.X, Size.Y); + + public ServerCompositionExperimentalAcrylicVisual(ServerCompositor compositor, Visual v) : base(compositor, v) + { + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs index c75ae8e631..e7e6875193 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs @@ -13,7 +13,7 @@ internal partial class ServerCompositionSurfaceVisual var bmp = Surface.Bitmap.Item; //TODO: add a way to always render the whole bitmap instead of just assuming 96 DPI - canvas.DrawBitmap(Surface.Bitmap, 1, new Rect(bmp.PixelSize.ToSize(1)), new Rect( + canvas.DrawBitmap(Surface.Bitmap.Item, 1, new Rect(bmp.PixelSize.ToSize(1)), new Rect( new Size(Size.X, Size.Y))); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 45275bdfe1..4f6564ebcf 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -179,7 +179,7 @@ namespace Avalonia.Rendering.Composition.Server if (_layer.CanBlit) _layer.Blit(targetContext); else - targetContext.DrawBitmap(RefCountable.CreateUnownedNotClonable(_layer), 1, + targetContext.DrawBitmap(_layer, 1, new Rect(_layerSize), new Rect(Size)); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.RenderResources.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.RenderResources.cs new file mode 100644 index 0000000000..bc8f03f365 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.RenderResources.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; + +namespace Avalonia.Rendering.Composition.Server; + +partial class ServerCompositor +{ + private Queue _renderResourcesInvalidationQueue = new(); + private HashSet _renderResourcesInvalidationSet = new(); + + public void ApplyEnqueuedRenderResourceChanges() + { + while (_renderResourcesInvalidationQueue.TryDequeue(out var obj)) + obj.QueuedInvalidate(); + _renderResourcesInvalidationSet.Clear(); + } + + public void EnqueueRenderResourceForInvalidation(IServerRenderResource resource) + { + if (_renderResourcesInvalidationSet.Add(resource)) + _renderResourcesInvalidationQueue.Enqueue(resource); + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index 0492997200..8652ef223c 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -18,7 +18,7 @@ namespace Avalonia.Rendering.Composition.Server /// 2) triggers animation ticks /// 3) asks composition targets to render themselves /// - internal class ServerCompositor : IRenderLoopTask + internal partial class ServerCompositor : IRenderLoopTask { private readonly IRenderLoop _renderLoop; @@ -35,6 +35,7 @@ namespace Avalonia.Rendering.Composition.Server private object _lock = new object(); private Thread? _safeThread; public PlatformRenderInterfaceContextManager RenderInterface { get; } + internal static readonly object RenderThreadDisposeStartMarker = new(); internal static readonly object RenderThreadJobsStartMarker = new(); internal static readonly object RenderThreadJobsEndMarker = new(); @@ -79,8 +80,14 @@ namespace Avalonia.Rendering.Composition.Server ReadServerJobs(stream); continue; } + + if (readObject == RenderThreadDisposeStartMarker) + { + ReadDisposeJobs(stream); + continue; + } - var target = (ServerObject)readObject!; + var target = (SimpleServerObject)readObject!; target.DeserializeChanges(stream, batch); #if DEBUG_COMPOSITOR_SERIALIZATION if (stream.ReadObject() != BatchStreamDebugMarkers.ObjectEndMarker) @@ -105,6 +112,16 @@ namespace Avalonia.Rendering.Composition.Server _receivedJobQueue.Enqueue((Action)readObject!); } + void ReadDisposeJobs(BatchStreamReader reader) + { + var count = reader.Read(); + while (count > 0) + { + (reader.ReadObject() as IDisposable)?.Dispose(); + count--; + } + } + void ExecuteServerJobs() { while(_receivedJobQueue.Count > 0) @@ -160,6 +177,8 @@ namespace Avalonia.Rendering.Composition.Server animation.OnTick(); _clockItemsToUpdate.Clear(); + + ApplyEnqueuedRenderResourceChanges(); try { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs index 2500358866..0e52f256ee 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs @@ -26,17 +26,6 @@ namespace Avalonia.Rendering.Composition.Server base.DeserializeChangesCore(reader, committedAt); } - public override long LastChangedBy - { - get - { - var seq = base.LastChangedBy; - foreach (var i in List) - seq = Math.Max(i.LastChangedBy, seq); - return seq; - } - } - public List.Enumerator GetEnumerator() => List.GetEnumerator(); public ServerList(ServerCompositor compositor) : base(compositor) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs index 31cb16ec20..c7fdd6dea6 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs @@ -15,12 +15,8 @@ namespace Avalonia.Rendering.Composition.Server /// Server-side counterpart. /// Is responsible for animation activation and invalidation /// - internal abstract class ServerObject : IExpressionObject + internal abstract class ServerObject : SimpleServerObject, 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 _subscriptions; @@ -42,9 +38,8 @@ namespace Avalonia.Rendering.Composition.Server } } - public ServerObject(ServerCompositor compositor) + public ServerObject(ServerCompositor compositor) : base(compositor) { - Compositor = compositor; } public virtual ExpressionVariant GetPropertyForAnimation(string name) @@ -90,13 +85,13 @@ namespace Avalonia.Rendering.Composition.Server subs.Invalidate(); } - protected void SetValue(CompositionProperty prop, out T field, T value) + protected new void SetValue(CompositionProperty prop, ref T field, T value) { field = value; InvalidateSubscriptions(prop); } - protected T GetValue(CompositionProperty prop, ref T field) + protected new T GetValue(CompositionProperty prop, ref T field) { if (_subscriptions.TryGetValue(prop, out var subs)) subs.IsValid = true; @@ -143,11 +138,6 @@ namespace Avalonia.Rendering.Composition.Server ValuesInvalidated(); } - protected virtual void ValuesInvalidated() - { - - } - public void SubscribeToInvalidation(CompositionProperty member, IAnimationInstance animation) { if (!_subscriptions.TryGetValue(member, out var store)) @@ -164,19 +154,5 @@ namespace Avalonia.Rendering.Composition.Server } public virtual CompositionProperty? GetCompositionProperty(string fieldName) => null; - - protected virtual void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt) - { - if (this is IDisposable disp - && reader.Read() == 1) - disp.Dispose(); - } - - public void DeserializeChanges(BatchStreamReader reader, Batch batch) - { - DeserializeChangesCore(reader, batch.CommittedAt); - ValuesInvalidated(); - ItselfLastChangedBy = batch.SequenceId; - } } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerRenderResource.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerRenderResource.cs new file mode 100644 index 0000000000..fad1995092 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerRenderResource.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using Avalonia.Rendering.Composition.Transport; +using Avalonia.Utilities; + +namespace Avalonia.Rendering.Composition.Server; + +internal interface IServerRenderResourceObserver +{ + void DependencyQueuedInvalidate(IServerRenderResource sender); +} + +internal interface IServerRenderResource : IServerRenderResourceObserver +{ + void AddObserver(IServerRenderResource observer); + void RemoveObserver(IServerRenderResource observer); + void QueuedInvalidate(); +} + +internal class SimpleServerRenderResource : SimpleServerObject, IServerRenderResource, IDisposable +{ + private bool _pendingInvalidation; + private bool _disposed; + public bool IsDisposed => _disposed; + private RefCountingSmallDictionary _observers; + + public SimpleServerRenderResource(ServerCompositor compositor) : base(compositor) + { + } + + protected new void SetValue(CompositionProperty prop, ref T field, T value) => SetValue(ref field, value); + + protected void SetValue(ref T field, T value) + { + if (EqualityComparer.Default.Equals(field, value)) + return; + + if (_disposed) + { + field = value; + return; + } + + if (field is IServerRenderResource oldChild) + oldChild.RemoveObserver(this); + else if (field is IServerRenderResource[] oldChildren) + { + foreach (var ch in oldChildren) + ch?.RemoveObserver(this); + } + field = value; + if (field is IServerRenderResource newChild) + newChild.AddObserver(this); + else if (field is IServerRenderResource[] newChildren) + { + foreach (var ch in newChildren) + ch.AddObserver(this); + } + Invalidated(); + } + + protected void Invalidated() + { + // This is needed to avoid triggering on multiple property changes + if (!_pendingInvalidation) + { + _pendingInvalidation = true; + Compositor.EnqueueRenderResourceForInvalidation(this); + PropertyChanged(); + } + } + + protected override void ValuesInvalidated() + { + Invalidated(); + base.ValuesInvalidated(); + } + + protected void RemoveObserversFromProperty(ref T field) + { + (field as IServerRenderResource)?.RemoveObserver(this); + } + + public virtual void Dispose() + { + _disposed = true; + // TODO: dispose once we implement pooling + _observers = default; + } + + public virtual void DependencyQueuedInvalidate(IServerRenderResource sender) => + Compositor.EnqueueRenderResourceForInvalidation(this); + + protected virtual void PropertyChanged() + { + + } + + public void AddObserver(IServerRenderResource observer) + { + Debug.Assert(!_disposed); + if(_disposed) + return; + _observers.Add(observer); + } + + public void RemoveObserver(IServerRenderResource observer) + { + if (_disposed) + return; + _observers.Remove(observer); + } + + public virtual void QueuedInvalidate() + { + _pendingInvalidation = false; + + foreach (var observer in _observers) + observer.Key.DependencyQueuedInvalidate(this); + + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/SimpleServerObject.cs b/src/Avalonia.Base/Rendering/Composition/Server/SimpleServerObject.cs new file mode 100644 index 0000000000..daaa309bec --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/SimpleServerObject.cs @@ -0,0 +1,34 @@ +using System; +using Avalonia.Rendering.Composition.Transport; + +namespace Avalonia.Rendering.Composition.Server; + +class SimpleServerObject +{ + public ServerCompositor Compositor { get; } + + public SimpleServerObject(ServerCompositor compositor) + { + Compositor = compositor; + } + + protected virtual void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt) + { + + } + + public void DeserializeChanges(BatchStreamReader reader, Batch batch) + { + DeserializeChangesCore(reader, batch.CommittedAt); + ValuesInvalidated(); + } + + protected virtual void ValuesInvalidated() + { + + } + + protected void SetValue(CompositionProperty prop, ref T field, T value) => field = value; + + protected T GetValue(CompositionProperty prop, ref T field) => field; +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs deleted file mode 100644 index 62fc73db44..0000000000 --- a/src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Media; -using Avalonia.VisualTree; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// Base class for draw operations that can use a brush. - /// - internal abstract class BrushDrawOperation : DrawOperationWithTransform - { - public IImmutableBrush? Brush { get; } - - public BrushDrawOperation(Rect bounds, Matrix transform, IImmutableBrush? brush) - : base(bounds, transform) - { - Brush = brush; - } - - public override void Dispose() - { - (Brush as ISceneBrushContent)?.Dispose(); - base.Dispose(); - } - } -} diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs deleted file mode 100644 index 782e287989..0000000000 --- a/src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs +++ /dev/null @@ -1,87 +0,0 @@ -using Avalonia.Platform; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// A node in the scene graph which represents a clip push or pop. - /// - internal class ClipNode : IDrawOperationWithTransform - { - /// - /// Initializes a new instance of the class that represents a - /// clip push. - /// - /// The current transform. - /// The clip to push. - public ClipNode(Matrix transform, Rect clip) - { - Transform = transform; - Clip = clip; - } - - /// - /// Initializes a new instance of the class that represents a - /// clip push. - /// - /// The current transform. - /// The clip to push. - public ClipNode(Matrix transform, RoundedRect clip) - { - Transform = transform; - Clip = clip; - } - - /// - /// Initializes a new instance of the class that represents a - /// clip pop. - /// - public ClipNode() - { - } - - /// - public Rect Bounds => default; - - /// - /// Gets the clip to be pushed or null if the operation represents a pop. - /// - public RoundedRect? Clip { get; } - - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - - /// - public bool HitTest(Point p) => false; - - /// - /// Determines if this draw operation equals another. - /// - /// The transform of the other draw operation. - /// The clip of the other draw operation. - /// True if the draw operations are the same, otherwise false. - /// - /// The properties of the other draw operation are passed in as arguments to prevent - /// allocation of a not-yet-constructed draw operation object. - /// - public bool Equals(Matrix transform, RoundedRect? clip) => Transform == transform && Clip == clip; - - /// - public void Render(IDrawingContextImpl context) - { - if (Clip.HasValue) - { - context.PushClip(Clip.Value); - } - else - { - context.PopClip(); - } - } - - public void Dispose() - { - } - } -} diff --git a/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs index 7ce9e6a8af..a3ee957bbe 100644 --- a/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs +++ b/src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs @@ -5,37 +5,6 @@ using Avalonia.Platform; namespace Avalonia.Rendering.SceneGraph { - internal sealed class CustomDrawOperation : DrawOperationWithTransform - { - public ICustomDrawOperation Custom { get; } - public CustomDrawOperation(ICustomDrawOperation custom, Matrix transform) - : base(custom.Bounds, transform) - { - Custom = custom; - } - - public override bool HitTestTransformed(Point p) => Custom.HitTest(p); - - public override void Render(IDrawingContextImpl context) - { - using var immediateDrawingContext = new ImmediateDrawingContext(context, false); - try - { - Custom.Render(immediateDrawingContext); - } - catch (Exception e) - { - Logger.TryGet(LogEventLevel.Error, LogArea.Visual) - ?.Log(Custom, $"Exception in {Custom.GetType().Name}.{nameof(ICustomDrawOperation.Render)} {{0}}", e); - } - } - - public override void Dispose() => Custom.Dispose(); - - public bool Equals(Matrix transform, ICustomDrawOperation custom) => - Transform == transform && Custom?.Equals(custom) == true; - } - public interface ICustomDrawOperation : IEquatable, IDisposable { /// diff --git a/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs deleted file mode 100644 index 786ce28d06..0000000000 --- a/src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System; -using Avalonia.Media; -using Avalonia.Platform; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// Base class for draw operations that have bounds. - /// - internal abstract class DrawOperation : IDrawOperation - { - public DrawOperation(Rect bounds, Matrix transform) - { - bounds = bounds.Normalize().TransformToAABB(transform); - - Bounds = new Rect( - new Point(Math.Floor(bounds.X), Math.Floor(bounds.Y)), - new Point(Math.Ceiling(bounds.Right), Math.Ceiling(bounds.Bottom))); - } - - public Rect Bounds { get; } - - public abstract bool HitTest(Point p); - - public abstract void Render(IDrawingContextImpl context); - - public virtual void Dispose() - { - } - } - - internal abstract class DrawOperationWithTransform : DrawOperation, IDrawOperationWithTransform - { - protected DrawOperationWithTransform(Rect bounds, Matrix transform) : base(bounds, transform) - { - Transform = transform; - } - - public Matrix Transform { get; } - - public sealed override bool HitTest(Point p) - { - if (Transform.IsIdentity) - return HitTestTransformed(p); - - if (!Transform.HasInverse) - return false; - - var transformedPoint = Transform.Invert().Transform(p); - - return HitTestTransformed(transformedPoint); - } - - public abstract bool HitTestTransformed(Point p); - } -} diff --git a/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs deleted file mode 100644 index 0a2b74e46a..0000000000 --- a/src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs +++ /dev/null @@ -1,97 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Media; -using Avalonia.Media.Immutable; -using Avalonia.Platform; -using Avalonia.VisualTree; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// A node in the scene graph which represents an ellipse draw. - /// - internal class EllipseNode : BrushDrawOperation - { - public EllipseNode( - Matrix transform, - IImmutableBrush? brush, - IPen? pen, - Rect rect) - : base(rect.Inflate(pen?.Thickness ?? 0), transform, brush) - { - Pen = pen?.ToImmutable(); - Rect = rect; - } - - /// - /// Gets the stroke pen. - /// - public ImmutablePen? Pen { get; } - - /// - /// Gets the rect of the ellipse to draw. - /// - public Rect Rect { get; } - - public bool Equals(Matrix transform, IBrush? brush, IPen? pen, Rect rect) - { - return transform == Transform && - Equals(brush, Brush) && - Equals(Pen, pen) && - rect.Equals(Rect); - } - - public override void Render(IDrawingContextImpl context) => context.DrawEllipse(Brush, Pen, Rect); - - public override bool HitTestTransformed(Point p) - { - var center = Rect.Center; - - var strokeThickness = Pen?.Thickness ?? 0; - - var rx = Rect.Width / 2 + strokeThickness / 2; - var ry = Rect.Height / 2 + strokeThickness / 2; - - var dx = p.X - center.X; - var dy = p.Y - center.Y; - - if (Math.Abs(dx) > rx || Math.Abs(dy) > ry) - { - return false; - } - - if (Brush != null) - { - return Contains(rx, ry); - } - else if (strokeThickness > 0) - { - bool inStroke = Contains(rx, ry); - - rx = Rect.Width / 2 - strokeThickness / 2; - ry = Rect.Height / 2 - strokeThickness / 2; - - bool inInner = Contains(rx, ry); - - return inStroke && !inInner; - } - - bool Contains(double radiusX, double radiusY) - { - var rx2 = radiusX * radiusX; - var ry2 = radiusY * radiusY; - - var distance = ry2 * dx * dx + rx2 * dy * dy; - - return distance <= rx2 * ry2; - } - - return false; - } - - public override void Dispose() - { - (Brush as ISceneBrushContent)?.Dispose(); - } - } -} diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs deleted file mode 100644 index 22fc49d30e..0000000000 --- a/src/Avalonia.Base/Rendering/SceneGraph/ExperimentalAcrylicNode.cs +++ /dev/null @@ -1,70 +0,0 @@ -using System; -using Avalonia.Media; -using Avalonia.Media.Immutable; -using Avalonia.Platform; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// A node in the scene graph which represents a rectangle draw. - /// - internal class ExperimentalAcrylicNode : DrawOperationWithTransform - { - /// - /// Initializes a new instance of the class. - /// - /// The transform. - /// - /// The rectangle to draw. - public ExperimentalAcrylicNode( - Matrix transform, - IExperimentalAcrylicMaterial material, - RoundedRect rect) - : base(rect.Rect, transform) - { - Material = material.ToImmutable(); - Rect = rect; - } - - public IExperimentalAcrylicMaterial Material { get; } - - /// - /// Gets the rectangle to draw. - /// - public RoundedRect Rect { get; } - - /// - /// Determines if this draw operation equals another. - /// - /// The transform of the other draw operation. - /// The fill of the other draw operation. - /// The rectangle of the other draw operation. - /// True if the draw operations are the same, otherwise false. - /// - /// The properties of the other draw operation are passed in as arguments to prevent - /// allocation of a not-yet-constructed draw operation object. - /// - public bool Equals(Matrix transform, IExperimentalAcrylicMaterial material, RoundedRect rect) - { - return transform == Transform && - Equals(material, Material) && - rect.Equals(Rect); - } - - /// - public override void Render(IDrawingContextImpl context) - { - if(context is IDrawingContextWithAcrylicLikeSupport idc) - { - idc.DrawRectangle(Material, Rect); - } - else - { - context.DrawRectangle(new ImmutableSolidColorBrush(Material.FallbackColor), null, Rect); - } - } - - /// - public override bool HitTestTransformed(Point p) => Rect.Rect.ContainsExclusive(p); - } -} diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs deleted file mode 100644 index 8575e61de4..0000000000 --- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryClipNode.cs +++ /dev/null @@ -1,74 +0,0 @@ -using Avalonia.Platform; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// A node in the scene graph which represents a geometry clip push or pop. - /// - internal class GeometryClipNode : IDrawOperationWithTransform - { - /// - /// Initializes a new instance of the class that represents a - /// geometry clip push. - /// - /// The current transform. - /// The clip to push. - public GeometryClipNode(Matrix transform, IGeometryImpl clip) - { - Transform = transform; - Clip = clip; - } - - /// - /// Initializes a new instance of the class that represents a - /// geometry clip pop. - /// - public GeometryClipNode() - { - } - - /// - public Rect Bounds => default; - - /// - /// Gets the clip to be pushed or null if the operation represents a pop. - /// - public IGeometryImpl? Clip { get; } - - /// - /// Gets the transform with which the node will be drawn. - /// - public Matrix Transform { get; } - - /// - public bool HitTest(Point p) => false; - - /// - /// Determines if this draw operation equals another. - /// - /// The transform of the other draw operation. - /// The clip of the other draw operation. - /// True if the draw operations are the same, otherwise false. - /// - /// The properties of the other draw operation are passed in as arguments to prevent - /// allocation of a not-yet-constructed draw operation object. - /// - public bool Equals(Matrix transform, IGeometryImpl clip) => Transform == transform && Clip == clip; - - /// - public void Render(IDrawingContextImpl context) - { - if (Clip != null) - { - context.PushGeometryClip(Clip); - } - else - { - context.PopGeometryClip(); - } - } - public void Dispose() - { - } - } -} diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs deleted file mode 100644 index 48af3b0e6b..0000000000 --- a/src/Avalonia.Base/Rendering/SceneGraph/GeometryNode.cs +++ /dev/null @@ -1,73 +0,0 @@ -using System; -using Avalonia.Media; -using Avalonia.Media.Immutable; -using Avalonia.Platform; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// A node in the scene graph which represents a geometry draw. - /// - internal class GeometryNode : BrushDrawOperation - { - /// - /// Initializes a new instance of the class. - /// - /// The transform. - /// The fill brush. - /// The stroke pen. - /// The geometry. - public GeometryNode(Matrix transform, - IImmutableBrush? brush, - IPen? pen, - IGeometryImpl geometry) - : base(geometry.GetRenderBounds(pen).CalculateBoundsWithLineCaps(pen), transform, brush) - { - Pen = pen?.ToImmutable(); - Geometry = geometry; - } - - /// - /// Gets the stroke pen. - /// - public ImmutablePen? Pen { get; } - - /// - /// Gets the geometry to draw. - /// - public IGeometryImpl Geometry { get; } - - /// - /// Determines if this draw operation equals another. - /// - /// The transform of the other draw operation. - /// The fill of the other draw operation. - /// The stroke of the other draw operation. - /// The geometry of the other draw operation. - /// True if the draw operations are the same, otherwise false. - /// - /// The properties of the other draw operation are passed in as arguments to prevent - /// allocation of a not-yet-constructed draw operation object. - /// - public bool Equals(Matrix transform, IBrush? brush, IPen? pen, IGeometryImpl geometry) - { - return transform == Transform && - Equals(brush, Brush) && - Equals(Pen, pen) && - Equals(geometry, Geometry); - } - - /// - public override void Render(IDrawingContextImpl context) - { - context.DrawGeometry(Brush, Pen, Geometry); - } - - /// - public override bool HitTestTransformed(Point p) - { - return (Brush != null && Geometry.FillContains(p)) || - (Pen != null && Geometry.StrokeContains(Pen, p)); - } - } -} diff --git a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs deleted file mode 100644 index 764c5c65f9..0000000000 --- a/src/Avalonia.Base/Rendering/SceneGraph/GlyphRunNode.cs +++ /dev/null @@ -1,66 +0,0 @@ -using Avalonia.Media; -using Avalonia.Platform; -using Avalonia.Utilities; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// A node in the scene graph which represents a glyph run draw. - /// - internal class GlyphRunNode : BrushDrawOperation - { - /// - /// Initializes a new instance of the class. - /// - /// The transform. - /// The foreground brush. - /// The glyph run to draw. - public GlyphRunNode( - Matrix transform, - IImmutableBrush? foreground, - IRef glyphRun) - : base(glyphRun.Item.Bounds, transform, foreground) - { - GlyphRun = glyphRun.Clone(); - } - - - /// - /// Gets the glyph run to draw. - /// - public IRef GlyphRun { get; } - - /// - public override void Render(IDrawingContextImpl context) => context.DrawGlyphRun(Brush, GlyphRun); - - /// - /// Determines if this draw operation equals another. - /// - /// The transform of the other draw operation. - /// The foreground of the other draw operation. - /// The glyph run of the other draw operation. - /// True if the draw operations are the same, otherwise false. - /// - /// The properties of the other draw operation are passed in as arguments to prevent - /// allocation of a not-yet-constructed draw operation object. - /// - internal bool Equals(Matrix transform, IBrush foreground, IRef glyphRun) - { - return transform == Transform && - Equals(foreground, Brush) && - Equals(glyphRun.Item, GlyphRun.Item); - } - - /// - public override bool HitTestTransformed(Point p) - { - return GlyphRun.Item.Bounds.ContainsExclusive(p); - } - - public override void Dispose() - { - GlyphRun?.Dispose(); - base.Dispose(); - } - } -} diff --git a/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs b/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs deleted file mode 100644 index 6a1aefe6b2..0000000000 --- a/src/Avalonia.Base/Rendering/SceneGraph/IDrawOperation.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System; -using Avalonia.Platform; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// Represents a node in the low-level scene graph that represents geometry. - /// - internal interface IDrawOperation : IDisposable - { - /// - /// Gets the bounds of the visible content in the node in global coordinates. - /// - Rect Bounds { get; } - - /// - /// Hit test the geometry in this node. - /// - /// The point in global coordinates. - /// True if the point hits the node's geometry; otherwise false. - /// - /// This method does not recurse to childs, if you want - /// to hit test children they must be hit tested manually. - /// - bool HitTest(Point p); - - /// - /// Renders the node to a drawing context. - /// - /// The drawing context. - void Render(IDrawingContextImpl context); - } - - internal interface IDrawOperationWithTransform : IDrawOperation - { - /// - /// Gets the transform with which the node will be drawn. - /// - Matrix Transform { get; } - } -} diff --git a/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs deleted file mode 100644 index caf0eee175..0000000000 --- a/src/Avalonia.Base/Rendering/SceneGraph/ImageNode.cs +++ /dev/null @@ -1,91 +0,0 @@ -using Avalonia.Platform; -using Avalonia.Utilities; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// A node in the scene graph which represents an image draw. - /// - internal class ImageNode : DrawOperationWithTransform - { - /// - /// Initializes a new instance of the class. - /// - /// The transform. - /// The image to draw. - /// The draw opacity. - /// The source rect. - /// The destination rect. - public ImageNode(Matrix transform, IRef source, double opacity, Rect sourceRect, Rect destRect) - : base(destRect, transform) - { - Source = source.Clone(); - Opacity = opacity; - SourceRect = sourceRect; - DestRect = destRect; - SourceVersion = Source.Item.Version; - } - - /// - /// Gets the image to draw. - /// - public IRef Source { get; } - - /// - /// Source bitmap Version - /// - public int SourceVersion { get; } - - /// - /// Gets the draw opacity. - /// - public double Opacity { get; } - - /// - /// Gets the source rect. - /// - public Rect SourceRect { get; } - - /// - /// Gets the destination rect. - /// - public Rect DestRect { get; } - - /// - /// Determines if this draw operation equals another. - /// - /// The transform of the other draw operation. - /// The image of the other draw operation. - /// The opacity of the other draw operation. - /// The source rect of the other draw operation. - /// The dest rect of the other draw operation. - /// True if the draw operations are the same, otherwise false. - /// - /// The properties of the other draw operation are passed in as arguments to prevent - /// allocation of a not-yet-constructed draw operation object. - /// - public bool Equals(Matrix transform, IRef source, double opacity, Rect sourceRect, Rect destRect) - { - return transform == Transform && - Equals(source.Item, Source.Item) && - source.Item.Version == SourceVersion && - opacity == Opacity && - sourceRect == SourceRect && - destRect == DestRect; - } - - /// - public override void Render(IDrawingContextImpl context) - { - context.DrawBitmap(Source, Opacity, SourceRect, DestRect); - } - - /// - public override bool HitTestTransformed(Point p) => DestRect.ContainsExclusive(p); - - public override void Dispose() - { - Source?.Dispose(); - } - } -} diff --git a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs deleted file mode 100644 index 1ac6cffe0a..0000000000 --- a/src/Avalonia.Base/Rendering/SceneGraph/LineNode.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; -using Avalonia.Media; -using Avalonia.Media.Immutable; -using Avalonia.Platform; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// A node in the scene graph which represents a line draw. - /// - internal class LineNode : DrawOperationWithTransform - { - /// - /// Initializes a new instance of the class. - /// - /// The transform. - /// The stroke pen. - /// The start point of the line. - /// The end point of the line. - public LineNode( - Matrix transform, - IPen pen, - Point p1, - Point p2) - : base(LineBoundsHelper.CalculateBounds(p1, p2, pen), transform) - { - Pen = pen.ToImmutable(); - P1 = p1; - P2 = p2; - } - - /// - /// Gets the stroke pen. - /// - public ImmutablePen Pen { get; } - - /// - /// Gets the start point of the line. - /// - public Point P1 { get; } - - /// - /// Gets the end point of the line. - /// - public Point P2 { get; } - - /// - /// Determines if this draw operation equals another. - /// - /// The transform of the other draw operation. - /// The stroke of the other draw operation. - /// The start point of the other draw operation. - /// The end point of the other draw operation. - /// True if the draw operations are the same, otherwise false. - /// - /// The properties of the other draw operation are passed in as arguments to prevent - /// allocation of a not-yet-constructed draw operation object. - /// - public bool Equals(Matrix transform, IPen pen, Point p1, Point p2) - { - return transform == Transform && Equals(Pen, pen) && p1 == P1 && p2 == P2; - } - - public override void Render(IDrawingContextImpl context) - { - context.DrawLine(Pen, P1, P2); - } - - public override bool HitTestTransformed(Point p) - { - var halfThickness = Pen.Thickness / 2; - var minX = Math.Min(P1.X, P2.X) - halfThickness; - var maxX = Math.Max(P1.X, P2.X) + halfThickness; - var minY = Math.Min(P1.Y, P2.Y) - halfThickness; - var maxY = Math.Max(P1.Y, P2.Y) + halfThickness; - - if (p.X < minX || p.X > maxX || p.Y < minY || p.Y > maxY) - return false; - - var a = P1; - var b = P2; - - //If dot1 or dot2 is negative, then the angle between the perpendicular and the segment is obtuse. - //The distance from a point to a straight line is defined as the - //length of the vector formed by the point and the closest point of the segment - - Vector ap = p - a; - var dot1 = Vector.Dot(b - a, ap); - - if (dot1 < 0) - return ap.Length <= Pen.Thickness / 2; - - Vector bp = p - b; - var dot2 = Vector.Dot(a - b, bp); - - if (dot2 < 0) - return bp.Length <= halfThickness; - - var bXaX = b.X - a.X; - var bYaY = b.Y - a.Y; - - var distance = (bXaX * (p.Y - a.Y) - bYaY * (p.X - a.X)) / - (Math.Sqrt(bXaX * bXaX + bYaY * bYaY)); - - return Math.Abs(distance) <= halfThickness; - } - } -} diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs deleted file mode 100644 index 1c79a67944..0000000000 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityMaskNode.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Media; -using Avalonia.Platform; -using Avalonia.VisualTree; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// A node in the scene graph which represents an opacity mask push or pop. - /// - internal class OpacityMaskNode : BrushDrawOperation - { - /// - /// Initializes a new instance of the class that represents an - /// opacity mask push. - /// - /// The opacity mask to push. - /// The bounds of the mask. - public OpacityMaskNode(IImmutableBrush mask, Rect bounds) - : base(default, Matrix.Identity, mask) - { - MaskBounds = bounds; - } - - /// - /// Gets the bounds of the opacity mask or null if the operation represents a pop. - /// - public Rect? MaskBounds { get; } - - - /// - public override bool HitTestTransformed(Point p) => false; - - /// - /// Determines if this draw operation equals another. - /// - /// The opacity mask of the other draw operation. - /// The opacity mask bounds of the other draw operation. - /// True if the draw operations are the same, otherwise false. - /// - /// The properties of the other draw operation are passed in as arguments to prevent - /// allocation of a not-yet-constructed draw operation object. - /// - public bool Equals(IBrush? mask, Rect? bounds) => Equals(Brush, mask) && MaskBounds == bounds; - - /// - public override void Render(IDrawingContextImpl context) - { - context.PushOpacityMask(Brush!, MaskBounds!.Value); - } - } - - internal class OpacityMaskPopNode : DrawOperation - { - public OpacityMaskPopNode() : base(default, Matrix.Identity) - { - } - - public override bool HitTest(Point p) => false; - - public override void Render(IDrawingContextImpl context) => context.PopOpacityMask(); - } -} diff --git a/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs deleted file mode 100644 index f76a055934..0000000000 --- a/src/Avalonia.Base/Rendering/SceneGraph/OpacityNode.cs +++ /dev/null @@ -1,70 +0,0 @@ -using Avalonia.Platform; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// A node in the scene graph which represents an opacity push or pop. - /// - internal class OpacityNode : IDrawOperation - { - /// - /// Initializes a new instance of the class that represents an - /// opacity push. - /// - /// The opacity to push. - /// The bounds. - public OpacityNode(double opacity, Rect bounds) - { - Opacity = opacity; - Bounds = bounds; - } - - /// - /// Initializes a new instance of the class that represents an - /// opacity pop. - /// - public OpacityNode() - { - } - - /// - public Rect Bounds { get; } - - /// - /// Gets the opacity to be pushed or null if the operation represents a pop. - /// - public double? Opacity { get; } - - /// - public bool HitTest(Point p) => false; - - /// - /// Determines if this draw operation equals another. - /// - /// The opacity of the other draw operation. - /// The bounds of the other draw operation. - /// True if the draw operations are the same, otherwise false. - /// - /// The properties of the other draw operation are passed in as arguments to prevent - /// allocation of a not-yet-constructed draw operation object. - /// - public bool Equals(double? opacity, Rect bounds) => Opacity == opacity && Bounds == bounds; - - /// - public void Render(IDrawingContextImpl context) - { - if (Opacity.HasValue) - { - context.PushOpacity(Opacity.Value, Bounds); - } - else - { - context.PopOpacity(); - } - } - - public void Dispose() - { - } - } -} diff --git a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs deleted file mode 100644 index e85992be34..0000000000 --- a/src/Avalonia.Base/Rendering/SceneGraph/RectangleNode.cs +++ /dev/null @@ -1,92 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Media; -using Avalonia.Media.Immutable; -using Avalonia.Platform; -using Avalonia.VisualTree; - -namespace Avalonia.Rendering.SceneGraph -{ - /// - /// A node in the scene graph which represents a rectangle draw. - /// - internal class RectangleNode : BrushDrawOperation - { - /// - /// Initializes a new instance of the class. - /// - /// The transform. - /// The fill brush. - /// The stroke pen. - /// The rectangle to draw. - /// The box shadow parameters - public RectangleNode( - Matrix transform, - IImmutableBrush? brush, - IPen? pen, - RoundedRect rect, - BoxShadows boxShadows) - : base(boxShadows.TransformBounds(rect.Rect).Inflate((pen?.Thickness ?? 0) / 2), transform, brush) - { - Pen = pen?.ToImmutable(); - Rect = rect; - BoxShadows = boxShadows; - } - - /// - /// Gets the stroke pen. - /// - public ImmutablePen? Pen { get; } - - /// - /// Gets the rectangle to draw. - /// - public RoundedRect Rect { get; } - - /// - /// The parameters for the box-shadow effect - /// - public BoxShadows BoxShadows { get; } - - /// - /// Determines if this draw operation equals another. - /// - /// The transform of the other draw operation. - /// The fill of the other draw operation. - /// The stroke of the other draw operation. - /// The rectangle of the other draw operation. - /// The box shadow parameters of the other draw operation - /// True if the draw operations are the same, otherwise false. - /// - /// The properties of the other draw operation are passed in as arguments to prevent - /// allocation of a not-yet-constructed draw operation object. - /// - public bool Equals(Matrix transform, IBrush? brush, IPen? pen, RoundedRect rect, BoxShadows boxShadows) - { - return transform == Transform && - Equals(brush, Brush) && - Equals(Pen, pen) && - BoxShadows.Equals(boxShadows) && - rect.Equals(Rect); - } - - /// - public override void Render(IDrawingContextImpl context) => context.DrawRectangle(Brush, Pen, Rect, BoxShadows); - - /// - public override bool HitTestTransformed(Point p) - { - if (Brush != null) - { - var rect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0); - return rect.ContainsExclusive(p); - } - else - { - var borderRect = Rect.Rect.Inflate((Pen?.Thickness / 2) ?? 0); - var emptyRect = Rect.Rect.Deflate((Pen?.Thickness / 2) ?? 0); - return borderRect.ContainsExclusive(p) && !emptyRect.ContainsExclusive(p); - } - } - } -} diff --git a/src/Avalonia.Base/RoundedRect.cs b/src/Avalonia.Base/RoundedRect.cs index 7d17d681ef..4c6f46ffe0 100644 --- a/src/Avalonia.Base/RoundedRect.cs +++ b/src/Avalonia.Base/RoundedRect.cs @@ -143,5 +143,12 @@ namespace Avalonia return new RoundedRect(new Rect(left, top, right - left, bottom - top), radii[0], radii[1], radii[2], radii[3]); } + + /// + /// This method should be used internally to check for the rect emptiness + /// Once we add support for WPF-like empty rects, there will be an actual implementation + /// For now it's internal to keep some loud community members happy about the API being pretty + /// + internal bool IsEmpty() => this == default; } } diff --git a/src/Avalonia.Base/Utilities/Polyfills.cs b/src/Avalonia.Base/Utilities/Polyfills.cs new file mode 100644 index 0000000000..6a30bfcf93 --- /dev/null +++ b/src/Avalonia.Base/Utilities/Polyfills.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +internal static class Polyfills +{ + #if !NET6_0_OR_GREATER + + public static bool TryDequeue(this Queue queue, [MaybeNullWhen(false)]out T item) + { + if (queue.Count == 0) + { + item = default; + return false; + } + + item = queue.Dequeue(); + return true; + } + + #endif +} + +#if !NET7_0_OR_GREATER + +namespace System.Diagnostics.CodeAnalysis +{ + [System.AttributeUsage( + System.AttributeTargets.Method | System.AttributeTargets.Parameter | System.AttributeTargets.Property, + AllowMultiple = false, Inherited = false)] + internal sealed class UnscopedRefAttribute : Attribute + { + } + + struct S + { + int field; + + // Okay: `field` has the ref-safe-to-escape of `this` which is *calling method* because + // it is a `ref` + [UnscopedRef] ref int Prop1 => ref field; + } +} +#endif \ No newline at end of file diff --git a/src/Avalonia.Base/Utilities/PooledInlineList.cs b/src/Avalonia.Base/Utilities/PooledInlineList.cs new file mode 100644 index 0000000000..f7801525fe --- /dev/null +++ b/src/Avalonia.Base/Utilities/PooledInlineList.cs @@ -0,0 +1,221 @@ +using System; +using System.Buffers; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using Avalonia.Rendering.Composition.Server; + +namespace Avalonia.Utilities; + +internal struct PooledInlineList : IDisposable, IEnumerable where T : class +{ + private object? _item; + + public PooledInlineList() + { + + } + + public void Add(T item) + { + if (_item == null) + _item = item; + else if (_item is SimplePooledList list) + list.Add(item); + else + { + ConvertToList(); + Add(item); + } + } + + public bool Remove(T item) + { + if (item == _item) + { + _item = null; + return true; + } + + if (item is SimplePooledList list) + return list.Remove(item); + + return false; + } + + void ConvertToList() + { + if (_item is SimplePooledList) + return; + var list = new SimplePooledList(); + if (_item != null) + list.Add((T)_item); + _item = list; + } + + public void EnsureCapacity(int count) + { + if (count < 2) + return; + ConvertToList(); + ((SimplePooledList)_item!).EnsureCapacity(count); + } + + public void Dispose() + { + if (_item is SimplePooledList list) + list.Dispose(); + _item = null; + } + + public int Count => _item == null ? 0 : _item is SimplePooledList list ? list.Count : 1; + + class SimplePooledList : IDisposable + { + public int Count; + public T[]? Items; + + public void Add(T item) + { + if (Items == null) + Items = ArrayPool.Shared.Rent(4); + else if (Count == Items.Length) + GrowItems(Count * 2); + + Items[Count] = item; + Count++; + } + + private void ReturnToPool(T[] items) + { +#if NETCOREAPP2_1_OR_GREATER + if (RuntimeHelpers.IsReferenceOrContainsReferences()) +#endif + { + Array.Clear(items, 0, Count); + } + ArrayPool.Shared.Return(items); + } + + void GrowItems(int count) + { + if (count < Count) + return; + var newArr = ArrayPool.Shared.Rent(count); + Array.Copy(Items!, newArr, Count); + ReturnToPool(Items!); + Items = newArr; + } + + public void EnsureCapacity(int count) + { + if (Items == null) + Items = ArrayPool.Shared.Rent(count); + else if (Items.Length < count) GrowItems(count); + } + + public void Dispose() + { + if(Items == null) + return; + + ReturnToPool(Items); + + Items = null!; + Count = 0; + } + + public bool Remove(T item) + { + for (var c = 0; c < Count; c++) + { + if (item == Items![c]) + { + Items[c] = null!; + Count--; + if (c < Count) + Array.Copy(Items, c + 1, Items, c, Count - c); + return true; + } + } + + return false; + } + } + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public Enumerator GetEnumerator() => new(_item); + + public struct Enumerator : IEnumerator + { + private T? _singleItem; + private int _index; + private SimplePooledList? _list; + + public Enumerator(object? item) + { + _index = -1; + _list = item as SimplePooledList; + if (_list == null) + _singleItem = (T?)item; + } + + public bool MoveNext() + { + if (_singleItem != null) + { + if (_index >= 0) + return false; + _index = 0; + return true; + } + + if (_list != null) + { + if (_index >= _list.Count - 1) + return false; + _index++; + return true; + } + + return false; + } + + public void Reset() => throw new NotSupportedException(); + object IEnumerator.Current => Current; + + public T Current + { + get + { + if (_list != null) + return _list.Items![_index]; + return _singleItem!; + } + } + + public void Dispose() + { + } + } + + /// + /// For compositor serialization purposes only, takes the ownership of previously transferred state + /// + public PooledInlineList(object? rawState) + { + _item = rawState; + } + + /// + /// For compositor serialization purposes only, gives up the ownership of the internal state and returns it + /// + public object? TransferRawState() + { + var rv = _item; + _item = null; + return rv; + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Utilities/Ref.cs b/src/Avalonia.Base/Utilities/Ref.cs index 95a1c23883..2d67bfbea6 100644 --- a/src/Avalonia.Base/Utilities/Ref.cs +++ b/src/Avalonia.Base/Utilities/Ref.cs @@ -8,7 +8,7 @@ namespace Avalonia.Utilities /// A ref-counted wrapper for a disposable object. /// /// - public interface IRef : IDisposable where T : class + internal interface IRef : IDisposable where T : class { /// /// The item that is being ref-counted. @@ -37,7 +37,7 @@ namespace Avalonia.Utilities - public static class RefCountable + internal static class RefCountable { /// /// Create a reference counted object wrapping the given item. @@ -49,37 +49,7 @@ namespace Avalonia.Utilities { return new Ref(item, new RefCounter(item)); } - - /// - /// Create an non-owning non-clonable reference to an item. - /// - /// The type of item. - /// The item. - /// A temporary reference that cannot be cloned that doesn't own the element. - public static IRef CreateUnownedNotClonable(T item) where T : class - => new TempRef(item); - - class TempRef : IRef where T : class - { - public void Dispose() - { - - } - - public TempRef(T item) - { - Item = item; - } - - public T Item { get; } - public IRef Clone() => throw new NotSupportedException(); - public IRef CloneAs() where TResult : class - => throw new NotSupportedException(); - - public int RefCount => 1; - } - class RefCounter { private IDisposable? _item; diff --git a/src/Avalonia.Base/Utilities/RefCountingSmallDictionary.cs b/src/Avalonia.Base/Utilities/RefCountingSmallDictionary.cs new file mode 100644 index 0000000000..86c9fd7ba1 --- /dev/null +++ b/src/Avalonia.Base/Utilities/RefCountingSmallDictionary.cs @@ -0,0 +1,54 @@ +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Avalonia.Utilities; + +internal struct RefCountingSmallDictionary : IEnumerable> where TKey : class +{ + private InlineDictionary _counts; + + public bool Add(TKey key) + { +#if NET6_0_OR_GREATER + ref var cnt = ref _counts.GetValueRefOrAddDefault(key, out bool exists); + cnt++; +#else + var exists = _counts.TryGetValue(key, out var cnt); + cnt++; + _counts[key] = cnt; +#endif + return !exists; + } + + public bool Remove(TKey key) + { +#if NET6_0_OR_GREATER + ref var cnt = ref _counts.GetValueRefOrNullRef(key); + cnt--; + if (cnt == 0) + { + _counts.Remove(key); + return true; + } +#else + var cnt = _counts[key]; + cnt--; + if (cnt == 0) + { + _counts.Remove(key); + return true; + } + + _counts[key] = cnt; +#endif + + return false; + } + + public InlineDictionary.Enumerator GetEnumerator() => _counts.GetEnumerator(); + + IEnumerator> IEnumerable>.GetEnumerator() => GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} \ No newline at end of file diff --git a/src/Avalonia.Base/Utilities/SmallDictionary.cs b/src/Avalonia.Base/Utilities/SmallDictionary.cs index 8aaf2200df..b6e62411da 100644 --- a/src/Avalonia.Base/Utilities/SmallDictionary.cs +++ b/src/Avalonia.Base/Utilities/SmallDictionary.cs @@ -2,14 +2,30 @@ using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace Avalonia.Utilities; -public record struct InlineDictionary : IEnumerable> where TKey : class where TValue : class +internal struct InlineDictionary : IEnumerable> where TKey : class { object? _data; TValue? _value; + struct KeyValuePair + { + public TKey? Key; + public TValue? Value; + + public KeyValuePair(TKey key, TValue? value) + { + Key = key; + Value = value; + } + + public static implicit operator KeyValuePair(KeyValuePair kvp) => new(kvp.Key, kvp.Value); + } + void SetCore(TKey key, TValue value, bool overwrite) { if (key == null) @@ -19,7 +35,7 @@ public record struct InlineDictionary : IEnumerable[] arr) + else if (_data is KeyValuePair[] arr) { var free = -1; for (var c = 0; c < arr.Length; c++) @@ -41,7 +57,7 @@ public record struct InlineDictionary : IEnumerable(key, value); + arr[free] = new KeyValuePair(key, value); return; } @@ -62,14 +78,14 @@ public record struct InlineDictionary : IEnumerable[6]; - arr[0] = new KeyValuePair((TKey)_data, _value); - arr[1] = new KeyValuePair(key, value); + arr = new KeyValuePair[6]; + arr[0] = new KeyValuePair((TKey)_data, _value); + arr[1] = new KeyValuePair(key, value); _data = arr; - _value = null; + _value = default; } } - + public void Add(TKey key, TValue value) => SetCore(key, value, false); public void Set(TKey key, TValue value) => SetCore(key, value, true); @@ -89,10 +105,10 @@ public record struct InlineDictionary : IEnumerable[] arr) + else if (_data is KeyValuePair[] arr) { for (var c = 0; c < arr.Length; c++) { @@ -111,6 +127,8 @@ public record struct InlineDictionary : IEnumerable _data != null; + public bool TryGetValue(TKey key, [MaybeNullWhen(false)]out TValue value) { if (_data == key) @@ -118,7 +136,7 @@ public record struct InlineDictionary : IEnumerable[] arr) + else if (_data is KeyValuePair[] arr) { for (var c = 0; c < arr.Length; c++) { @@ -129,27 +147,115 @@ public record struct InlineDictionary : IEnumerable dic) return dic.TryGetValue(key, out value); - value = null; + value = default; return false; } + +#if NET6_0_OR_GREATER + [UnscopedRef] + public ref TValue GetValueRefOrNullRef(TKey key) + { + if (_data == key) + { + return ref (_value!); + } + else if (_data is KeyValuePair[] arr) + { + for (var c = 0; c < arr.Length; c++) + { + if (arr[c].Key == key) + return ref arr[c].Value!; + } + + return ref Unsafe.NullRef(); + } + if (_data is Dictionary dic) + return ref CollectionsMarshal.GetValueRefOrNullRef(dic, key)!; + return ref Unsafe.NullRef(); + } + [UnscopedRef] + public ref TValue GetValueRefOrAddDefault(TKey key, [UnscopedRef] out bool exists) + { + if (_data == null) + { + exists = false; + _data = key; + return ref _value!; + } + + // Single element + if (_data == key) + { + exists = true; + return ref (_value!); + } + + // Small array + if (_data is KeyValuePair[] arr) + { + // Try to find the element and look for the first free slot while we are at it + int free = -1; + for (var c = 0; c < arr.Length; c++) + { + if (arr[c].Key == key) + { + exists = true; + return ref arr[c].Value!; + } + else if (free == -1 && arr[c].Key == null) + free = c; + } + + // There is a free slot, use it + if (free != -1) + { + arr[free] = new(key, default); + exists = false; + return ref arr[free].Value!; + } + + // Upgrade to dictionary + var newDic = new Dictionary(); + foreach (var kvp in arr) + newDic.Add(kvp.Key!, kvp.Value!); + _data = newDic; + return ref CollectionsMarshal.GetValueRefOrAddDefault(newDic, key, out exists)!; + } + + if (_data is Dictionary dic) + return ref CollectionsMarshal.GetValueRefOrAddDefault(dic, key, out exists)!; + + // This is a second element, convert to array + // We have a single element, upgrade to array + arr = new KeyValuePair[6]; + arr[0] = new KeyValuePair((TKey)_data, _value); + arr[1] = new KeyValuePair(key, default); + _data = arr; + _value = default; + exists = false; + return ref arr[1].Value!; + + } +#endif + public bool TryGetAndRemoveValue(TKey key, [MaybeNullWhen(false)]out TValue value) { if (_data == key) { value = _value!; - _value = null; + _value = default; _data = null; return true; } - else if (_data is KeyValuePair[] arr) + else if (_data is KeyValuePair[] arr) { for (var c = 0; c < arr.Length; c++) { @@ -161,7 +267,7 @@ public record struct InlineDictionary : IEnumerable dic) @@ -171,7 +277,7 @@ public record struct InlineDictionary : IEnumerable : IEnumerable> { private Dictionary.Enumerator _inner; - private readonly KeyValuePair[]? _arr; + private readonly KeyValuePair[]? _arr; private KeyValuePair _first; private int _index; private Type _type; @@ -205,7 +311,7 @@ public record struct InlineDictionary : IEnumerable[] arr) + else if (parent._data is KeyValuePair[] arr) { _type = Type.Array; _arr = arr; @@ -227,6 +333,7 @@ public record struct InlineDictionary : IEnumerableAvalonia.Rendering.Composition.Animations + + + - - + + @@ -39,8 +42,13 @@ + + + + + - + @@ -55,4 +63,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs index dbffb803a3..bc17fe7237 100644 --- a/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs +++ b/src/Avalonia.Controls/ExperimentalAcrylicBorder.cs @@ -5,6 +5,9 @@ using Avalonia.Platform; using System; using Avalonia.Reactive; using Avalonia.Media.Immutable; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Server; +using Avalonia.Utilities; namespace Avalonia.Controls { @@ -16,9 +19,8 @@ namespace Avalonia.Controls public static readonly StyledProperty MaterialProperty = AvaloniaProperty.Register(nameof(Material)); - private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper(); - private IDisposable? _subscription; + private IDisposable? _materialSubscription; static ExperimentalAcrylicBorder() { @@ -71,32 +73,55 @@ namespace Avalonia.Controls break; } }); + UpdateMaterialSubscription(); + } + + void UpdateMaterialSubscription() + { + _materialSubscription?.Dispose(); + _materialSubscription = null; + if (CompositionVisual == null) + return; + if (Material == null!) + return; + _materialSubscription = Observable.FromEventPattern( + h => Material.PropertyChanged += h, + h => Material.PropertyChanged -= h) + .Subscribe(_ => UpdateMaterialSubscription()); + SyncMaterial(CompositionVisual); + } + + private void SyncMaterial(CompositionVisual? visual) + { + if (visual is CompositionExperimentalAcrylicVisual v) + { + v.CornerRadius = CornerRadius; + v.Material = (ImmutableExperimentalAcrylicMaterial)Material.ToImmutable(); + } + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if(change.Property == MaterialProperty) + UpdateMaterialSubscription(); + if(change.Property == CornerRadiusProperty) + SyncMaterial(CompositionVisual); + base.OnPropertyChanged(change); } protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) { base.OnDetachedFromVisualTree(e); - + UpdateMaterialSubscription(); _subscription?.Dispose(); } - public sealed override void Render(DrawingContext context) + private protected override CompositionDrawListVisual CreateCompositionVisual(Compositor compositor) { - if (context is IDrawingContextWithAcrylicLikeSupport idc) - { - var cornerRadius = CornerRadius; - - idc.DrawRectangle( - Material, - new RoundedRect( - new Rect(Bounds.Size), - cornerRadius.TopLeft, cornerRadius.TopRight, - cornerRadius.BottomRight, cornerRadius.BottomLeft)); - } - else - { - _borderRenderHelper.Render(context, Bounds.Size, new Thickness(), CornerRadius, new ImmutableSolidColorBrush(Material.FallbackColor), null, default); - } + var v = new CompositionExperimentalAcrylicVisual(compositor, this); + SyncMaterial(v); + + return v; } /// diff --git a/src/Avalonia.Controls/NativeMenuItem.cs b/src/Avalonia.Controls/NativeMenuItem.cs index 9b5f756887..91e1ba159a 100644 --- a/src/Avalonia.Controls/NativeMenuItem.cs +++ b/src/Avalonia.Controls/NativeMenuItem.cs @@ -54,14 +54,14 @@ namespace Avalonia.Controls return value; } - public static readonly StyledProperty IconProperty = - AvaloniaProperty.Register(nameof(Icon)); + public static readonly StyledProperty IconProperty = + AvaloniaProperty.Register(nameof(Icon)); - public IBitmap? Icon + public Bitmap? Icon { get => GetValue(IconProperty); set => SetValue(IconProperty, value); - } + } public static readonly StyledProperty HeaderProperty = AvaloniaProperty.Register(nameof(Header)); @@ -175,7 +175,7 @@ namespace Avalonia.Controls } } } - + public enum NativeMenuItemToggleType { None, diff --git a/src/Avalonia.Controls/WindowIcon.cs b/src/Avalonia.Controls/WindowIcon.cs index 20ea872cad..7f1993b209 100644 --- a/src/Avalonia.Controls/WindowIcon.cs +++ b/src/Avalonia.Controls/WindowIcon.cs @@ -9,7 +9,7 @@ namespace Avalonia.Controls /// public class WindowIcon { - public WindowIcon(IBitmap bitmap) + public WindowIcon(Bitmap bitmap) { PlatformImpl = AvaloniaLocator.Current.GetRequiredService().LoadIcon(bitmap.PlatformImpl.Item); } @@ -24,7 +24,7 @@ namespace Avalonia.Controls PlatformImpl = AvaloniaLocator.Current.GetRequiredService().LoadIcon(stream); } - public IWindowIconImpl PlatformImpl { get; } + internal IWindowIconImpl PlatformImpl { get; } public void Save(Stream stream) => PlatformImpl.Save(stream); } diff --git a/src/Avalonia.Themes.Fluent/IBitmapToImageConverter.cs b/src/Avalonia.Themes.Fluent/IBitmapToImageConverter.cs index e6c46122fc..06b27669c2 100644 --- a/src/Avalonia.Themes.Fluent/IBitmapToImageConverter.cs +++ b/src/Avalonia.Themes.Fluent/IBitmapToImageConverter.cs @@ -10,8 +10,8 @@ namespace Avalonia.Themes.Fluent { public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - if (value != null && value is IBitmap bm) - return new Image { Source=bm }; + if (value != null && value is Bitmap bm) + return new Image { Source = bm }; return null; } diff --git a/src/Avalonia.Themes.Simple/IBitmapToImageConverter.cs b/src/Avalonia.Themes.Simple/IBitmapToImageConverter.cs index d5bf003288..4bb8e2e286 100644 --- a/src/Avalonia.Themes.Simple/IBitmapToImageConverter.cs +++ b/src/Avalonia.Themes.Simple/IBitmapToImageConverter.cs @@ -14,7 +14,7 @@ namespace Avalonia.Themes.Simple { public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - if (value != null && value is IBitmap bm) + if (value != null && value is Bitmap bm) return new Image { Source=bm }; return null; diff --git a/src/Avalonia.X11/X11CursorFactory.cs b/src/Avalonia.X11/X11CursorFactory.cs index 13068832fb..9818d3f86a 100644 --- a/src/Avalonia.X11/X11CursorFactory.cs +++ b/src/Avalonia.X11/X11CursorFactory.cs @@ -118,7 +118,7 @@ namespace Avalonia.X11 using (var ctx = renderTarget.CreateDrawingContext()) { var r = new Rect(_pixelSize.ToSize(1)); - ctx.DrawBitmap(RefCountable.CreateUnownedNotClonable(bitmap), 1, r, r); + ctx.DrawBitmap(bitmap, 1, r, r); } Handle = XLib.XcursorImageLoadCursor(display, _blob.Address); diff --git a/src/Avalonia.X11/X11IconLoader.cs b/src/Avalonia.X11/X11IconLoader.cs index 84a1d35712..bede875e89 100644 --- a/src/Avalonia.X11/X11IconLoader.cs +++ b/src/Avalonia.X11/X11IconLoader.cs @@ -44,7 +44,7 @@ namespace Avalonia.X11 using(var cpuContext = AvaloniaLocator.Current.GetRequiredService().CreateBackendContext(null)) using(var rt = cpuContext.CreateRenderTarget(new[]{this})) using (var ctx = rt.CreateDrawingContext()) - ctx.DrawBitmap(bitmap.PlatformImpl, 1, new Rect(bitmap.Size), + ctx.DrawBitmap(bitmap.PlatformImpl.Item, 1, new Rect(bitmap.Size), new Rect(0, 0, _width, _height)); Data = new UIntPtr[_width * _height + 2]; Data[0] = new UIntPtr((uint)_width); diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index ab157f8062..d677bd1586 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -517,12 +517,12 @@ namespace Avalonia.Headless { } - public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect) + public void DrawBitmap(IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect) { } - public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) + public void DrawBitmap(IBitmapImpl source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) { } @@ -536,7 +536,7 @@ namespace Avalonia.Headless { } - public void DrawGlyphRun(IBrush? foreground, IRef glyphRun) + public void DrawGlyphRun(IBrush? foreground, IGlyphRunImpl glyphRun) { } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs index e9d812a465..ded5768648 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs @@ -125,7 +125,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions => AddType(typeSystem.GetType(type), typeSystem.GetType(conv)); Add("Avalonia.Media.IImage","Avalonia.Markup.Xaml.Converters.BitmapTypeConverter"); - Add("Avalonia.Media.Imaging.IBitmap","Avalonia.Markup.Xaml.Converters.BitmapTypeConverter"); + Add("Avalonia.Media.Imaging.Bitmap","Avalonia.Markup.Xaml.Converters.BitmapTypeConverter"); + Add("Avalonia.Media.IImageBrushSource","Avalonia.Markup.Xaml.Converters.BitmapTypeConverter"); var ilist = typeSystem.GetType("System.Collections.Generic.IList`1"); AddType(ilist.MakeGenericType(typeSystem.GetType("Avalonia.Point")), typeSystem.GetType("Avalonia.Markup.Xaml.Converters.PointsListTypeConverter")); diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/IconTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/IconTypeConverter.cs index c706fba2e7..e84bd09e05 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/IconTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/IconTypeConverter.cs @@ -24,7 +24,7 @@ namespace Avalonia.Markup.Xaml.Converters return CreateIconFromPath(context, path); } - var bitmap = value as IBitmap; + var bitmap = value as Bitmap; if (bitmap != null) { return new WindowIcon(bitmap); diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index 753fa0203c..18cb87dfd1 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -182,10 +182,10 @@ namespace Avalonia.Skia } /// - public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect) + public void DrawBitmap(IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect) { CheckLease(); - var drawableImage = (IDrawableBitmapImpl)source.Item; + var drawableImage = (IDrawableBitmapImpl)source; var s = sourceRect.ToSKRect(); var d = destRect.ToSKRect(); @@ -199,11 +199,11 @@ namespace Avalonia.Skia } /// - public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) + public void DrawBitmap(IBitmapImpl source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) { CheckLease(); PushOpacityMask(opacityMask, opacityMaskRect); - DrawBitmap(source, 1, new Rect(0, 0, source.Item.PixelSize.Width, source.Item.PixelSize.Height), destRect); + DrawBitmap(source, 1, new Rect(0, 0, source.PixelSize.Width, source.PixelSize.Height), destRect); PopOpacityMask(); } @@ -506,7 +506,7 @@ namespace Avalonia.Skia } /// - public void DrawGlyphRun(IBrush? foreground, IRef glyphRun) + public void DrawGlyphRun(IBrush? foreground, IGlyphRunImpl glyphRun) { CheckLease(); @@ -515,14 +515,14 @@ namespace Avalonia.Skia return; } - using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Item.Bounds.Size)) + using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Bounds.Size)) { - var glyphRunImpl = (GlyphRunImpl)glyphRun.Item; + var glyphRunImpl = (GlyphRunImpl)glyphRun; var textBlob = glyphRunImpl.GetTextBlob(RenderOptions); - Canvas.DrawText(textBlob, (float)glyphRun.Item.BaselineOrigin.X, - (float)glyphRun.Item.BaselineOrigin.Y, paintWrapper.Paint); + Canvas.DrawText(textBlob, (float)glyphRun.BaselineOrigin.X, + (float)glyphRun.BaselineOrigin.Y, paintWrapper.Paint); } } @@ -895,7 +895,7 @@ namespace Avalonia.Skia context.RenderOptions = RenderOptions; context.DrawBitmap( - RefCountable.CreateUnownedNotClonable(tileBrushImage), + tileBrushImage, 1, sourceRect, targetRect); @@ -1175,7 +1175,7 @@ namespace Avalonia.Skia } else { - tileBrushImage = (tileBrush as IImageBrush)?.Source?.PlatformImpl.Item as IDrawableBitmapImpl; + tileBrushImage = (tileBrush as IImageBrush)?.Source?.Bitmap?.Item as IDrawableBitmapImpl; } if (tileBrush != null && tileBrushImage != null) diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index e13e699bec..8ac1c4669a 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs @@ -127,9 +127,9 @@ namespace Avalonia.Direct2D1.Media /// The opacity to draw with. /// The rect in the image to draw. /// The rect in the output to draw to. - public void DrawBitmap(IRef source, double opacity, Rect sourceRect, Rect destRect) + public void DrawBitmap(IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect) { - using (var d2d = ((BitmapImpl)source.Item).GetDirect2DBitmap(_deviceContext)) + using (var d2d = ((BitmapImpl)source).GetDirect2DBitmap(_deviceContext)) { var interpolationMode = GetInterpolationMode(RenderOptions.BitmapInterpolationMode); @@ -200,11 +200,11 @@ namespace Avalonia.Direct2D1.Media /// The opacity mask to draw with. /// The destination rect for the opacity mask. /// The rect in the output to draw to. - public void DrawBitmap(IRef source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) + public void DrawBitmap(IBitmapImpl source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect) { var interpolationMode = GetInterpolationMode(RenderOptions.BitmapInterpolationMode); - using (var d2dSource = ((BitmapImpl)source.Item).GetDirect2DBitmap(_deviceContext)) + using (var d2dSource = ((BitmapImpl)source).GetDirect2DBitmap(_deviceContext)) using (var sourceBrush = new BitmapBrush1(_deviceContext, d2dSource.Value, new BitmapBrushProperties1 { InterpolationMode = interpolationMode })) using (var d2dOpacityMask = CreateBrush(opacityMask, opacityMaskRect.Size)) using (var geometry = new SharpDX.Direct2D1.RectangleGeometry(Direct2D1Platform.Direct2D1Factory, destRect.ToDirect2D())) @@ -395,15 +395,15 @@ namespace Avalonia.Direct2D1.Media /// /// The foreground. /// The glyph run. - public void DrawGlyphRun(IBrush foreground, IRef glyphRun) + public void DrawGlyphRun(IBrush foreground, IGlyphRunImpl glyphRun) { - using (var brush = CreateBrush(foreground, glyphRun.Item.Bounds.Size)) + using (var brush = CreateBrush(foreground, glyphRun.Bounds.Size)) { - var immutableGlyphRun = (GlyphRunImpl)glyphRun.Item; + var immutableGlyphRun = (GlyphRunImpl)glyphRun; var dxGlyphRun = immutableGlyphRun.GlyphRun; - _renderTarget.DrawGlyphRun(glyphRun.Item.BaselineOrigin.ToSharpDX(), dxGlyphRun, + _renderTarget.DrawGlyphRun(glyphRun.BaselineOrigin.ToSharpDX(), dxGlyphRun, brush.PlatformBrush, MeasuringMode.Natural); } } @@ -522,12 +522,12 @@ namespace Avalonia.Direct2D1.Media // there is no Direct2D implementation of Conic Gradients so use Radial as a stand-in return new SolidColorBrushImpl(conicGradientBrush, _deviceContext); } - else if (imageBrush?.Source != null) + else if (imageBrush?.Source?.Bitmap != null) { return new ImageBrushImpl( imageBrush, _deviceContext, - (BitmapImpl)imageBrush.Source.PlatformImpl.Item, + (BitmapImpl)imageBrush.Source.Bitmap.Item, destinationSize); } else if (sceneBrush != null || sceneBrushContent != null) diff --git a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs index e0611b5870..2424feb4f5 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs @@ -99,7 +99,7 @@ namespace Avalonia.Direct2D1.Media context.Clear(Colors.Transparent); context.PushClip(calc.IntermediateClip); context.Transform = calc.IntermediateTransform; - context.DrawBitmap(RefCountable.CreateUnownedNotClonable(bitmap), 1, rect, rect); + context.DrawBitmap(bitmap, 1, rect, rect); context.PopClip(); } diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs index 2a3bf617f7..5d38751bde 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs @@ -63,7 +63,7 @@ namespace Avalonia.Direct2D1.Media.Imaging using (var dc = wic.CreateDrawingContext(null)) { dc.DrawBitmap( - RefCountable.CreateUnownedNotClonable(this), + this, 1, new Rect(PixelSize.ToSizeWithDpi(Dpi.X)), new Rect(PixelSize.ToSizeWithDpi(Dpi.X))); diff --git a/src/tools/DevGenerators/CompositionGenerator/Config.cs b/src/tools/DevGenerators/CompositionGenerator/Config.cs index d1fc691a8b..72c7486dd9 100644 --- a/src/tools/DevGenerators/CompositionGenerator/Config.cs +++ b/src/tools/DevGenerators/CompositionGenerator/Config.cs @@ -4,8 +4,6 @@ using System.Xml.Serialization; namespace Avalonia.SourceGenerator.CompositionGenerator { - - [XmlRoot("NComposition")] public class GConfig { @@ -55,7 +53,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator { [XmlAttribute] public string Name { get; set; } - + [XmlAttribute] public string Inherits { get; set; } @@ -71,6 +69,12 @@ namespace Avalonia.SourceGenerator.CompositionGenerator [XmlAttribute] public bool CustomServerCtor { get; set; } + [XmlAttribute] + public bool Internal { get; set; } + + [XmlAttribute] + public bool ServerOnly { get; set; } + [XmlElement(typeof(GImplements), ElementName = "Implements")] public List Implements { get; set; } = new List(); diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.ConfigHelpers.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.ConfigHelpers.cs new file mode 100644 index 0000000000..5d59af6951 --- /dev/null +++ b/src/tools/DevGenerators/CompositionGenerator/Generator.ConfigHelpers.cs @@ -0,0 +1,56 @@ + +using System.Collections.Generic; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +using static Avalonia.SourceGenerator.CompositionGenerator.Extensions; +namespace Avalonia.SourceGenerator.CompositionGenerator; + +public partial class Generator +{ + class GeneratorTypeInfo + { + public TypeSyntax RoslynType { get; set; } + public string FilteredTypeName { get; set; } + public bool IsObject { get; set; } + public bool IsPassthrough { get; set; } + public string ServerType { get; set; } + public bool IsNullable { get; set; } + } + + private Dictionary _typeInfoCache = new(); + + private GeneratorTypeInfo GetTypeInfo(string type) + { + if (_typeInfoCache.TryGetValue(type, out var cached)) + return cached; + + var propType = ParseTypeName(type); + var filteredType = type.TrimEnd('?'); + var isObject = _objects.Contains(filteredType); + var isNullable = type.EndsWith("?"); + bool isPassthrough = false; + + var serverType = ((isObject ? "Server" : "") + type); + if (_manuals.TryGetValue(filteredType, out var manual)) + { + if (manual.Passthrough) + { + isPassthrough = true; + serverType = type; + } + + if (manual.ServerName != null) + serverType = manual.ServerName + (isNullable ? "?" : ""); + } + + return _typeInfoCache[type] = new GeneratorTypeInfo + { + RoslynType = propType, + FilteredTypeName = filteredType, + IsObject = isObject, + IsPassthrough = isPassthrough, + ServerType = serverType, + IsNullable = isNullable + }; + } +} \ No newline at end of file diff --git a/src/tools/DevGenerators/CompositionGenerator/Generator.cs b/src/tools/DevGenerators/CompositionGenerator/Generator.cs index ced2cb369c..db8658c7f1 100644 --- a/src/tools/DevGenerators/CompositionGenerator/Generator.cs +++ b/src/tools/DevGenerators/CompositionGenerator/Generator.cs @@ -44,7 +44,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator static ExpressionSyntax ClientProperty(GClass c, GProperty p) => MemberAccess(ServerName(c.Name), CompositionPropertyField(p)); - + void GenerateClass(GClass cl) { var list = cl as GList; @@ -57,10 +57,11 @@ namespace Avalonia.SourceGenerator.CompositionGenerator var inherits = cl.Inherits ?? "CompositionObject"; var abstractModifier = cl.Abstract ? new[] {SyntaxKind.AbstractKeyword} : null; + var visibilityModifier = cl.Internal ? SyntaxKind.InternalKeyword : SyntaxKind.PublicKeyword; var client = ClassDeclaration(cl.Name) .AddModifiers(abstractModifier) - .AddModifiers(SyntaxKind.PublicKeyword, SyntaxKind.UnsafeKeyword, SyntaxKind.PartialKeyword) + .AddModifiers(visibilityModifier, SyntaxKind.UnsafeKeyword, SyntaxKind.PartialKeyword) .WithBaseType(inherits); var serverName = ServerName(cl.Name); @@ -88,7 +89,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator .AddModifiers(SyntaxKind.InternalKeyword, SyntaxKind.NewKeyword) .AddAccessorListAccessors(AccessorDeclaration(SyntaxKind.GetAccessorDeclaration) .WithSemicolonToken(Semicolon()))); - client = client.AddMembers( + client = client.AddMembers( ConstructorDeclaration(cl.Name) .AddModifiers(SyntaxKind.InternalKeyword) .WithParameterList(ParameterList(SeparatedList(new[] @@ -148,28 +149,16 @@ namespace Avalonia.SourceGenerator.CompositionGenerator foreach (var prop in cl.Properties) { var fieldName = PropertyBackingFieldName(prop); - var propType = ParseTypeName(prop.Type); - var filteredPropertyType = prop.Type.TrimEnd('?'); - var isObject = _objects.Contains(filteredPropertyType); - var isNullable = prop.Type.EndsWith("?"); - bool isPassthrough = false; - - client = GenerateClientProperty(client, cl, prop, propType, isObject, isNullable); - + var typeInfo = GetTypeInfo(prop.Type); + var (propType, filteredPropertyType, isObject, isNullable, isPassthrough, + serverPropertyType) = (typeInfo.RoslynType, + typeInfo.FilteredTypeName, + typeInfo.IsObject, typeInfo.IsNullable, typeInfo.IsPassthrough, typeInfo.ServerType); + var animatedServer = prop.Animated; - var serverPropertyType = ((isObject ? "Server" : "") + prop.Type); - if (_manuals.TryGetValue(filteredPropertyType, out var manual)) - { - if (manual.Passthrough) - { - isPassthrough = true; - serverPropertyType = prop.Type; - } - - if (manual.ServerName != null) - serverPropertyType = manual.ServerName + (isNullable ? "?" : ""); - } + client = GenerateClientProperty(client, cl, prop, propType, isObject, isNullable); + if (animatedServer) server = server.AddMembers( DeclareField(serverPropertyType, fieldName), @@ -218,7 +207,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator ExpressionStatement(InvocationExpression(IdentifierName("SetValue"), ArgumentList(SeparatedList(new[]{ Argument(IdentifierName(CompositionPropertyField(prop))), - Argument(null, Token(SyntaxKind.OutKeyword), IdentifierName(fieldName)), + Argument(null, Token(SyntaxKind.RefKeyword), IdentifierName(fieldName)), Argument(IdentifierName("value")) } )))), @@ -234,7 +223,7 @@ namespace Avalonia.SourceGenerator.CompositionGenerator resetBody = resetBody.AddStatements( ExpressionStatement(InvocationExpression(MemberAccess(prop.Name, "Reset")))); - serializeMethodBody = ApplySerializeField(serializeMethodBody,cl, prop, isObject, isPassthrough); + serializeMethodBody = ApplySerializeField(serializeMethodBody, cl, prop, isObject, isPassthrough); deserializeMethodBody = ApplyDeserializeField(deserializeMethodBody,cl, prop, serverPropertyType, isObject); if (animatedServer) @@ -290,9 +279,15 @@ namespace Avalonia.SourceGenerator.CompositionGenerator if (startAnimationBody.Statements.Count != 0) client = WithStartAnimation(client, startAnimationBody); - - server = WithGetPropertyForAnimation(server, serverGetPropertyBody); - server = WithGetCompositionProperty(server, serverGetCompositionPropertyBody); + + if (!cl.ServerOnly) + { + server = WithGetPropertyForAnimation(server, serverGetPropertyBody); + server = WithGetCompositionProperty(server, serverGetCompositionPropertyBody); + } + + if (cl.ServerOnly) + server = server.AddMembers(GenerateSerializeAllMethod(cl)); if(cl.Implements.Count > 0) foreach (var impl in cl.Implements) @@ -309,9 +304,11 @@ namespace Avalonia.SourceGenerator.CompositionGenerator SaveTo(unit.AddMembers(GenerateChangedFieldsEnum(cl)), "Transport", ChangedFieldsTypeName(cl) + ".generated.cs"); + + if (!cl.ServerOnly) + SaveTo(unit.AddMembers(clientNs.AddMembers(client)), + cl.Name + ".generated.cs"); - SaveTo(unit.AddMembers(clientNs.AddMembers(client)), - cl.Name + ".generated.cs"); SaveTo(unit.AddMembers(serverNs.AddMembers(server)), "Server", "Server" + cl.Name + ".generated.cs"); } @@ -459,6 +456,29 @@ return; ); } + private MethodDeclarationSyntax GenerateSerializeAllMethod(GClass cl) + { + var declaration = (MethodDeclarationSyntax)ParseMemberDeclaration( + $"internal static void SerializeAllChanges(BatchStreamWriter writer){{}}")!; + declaration = declaration.AddParameterListParameters(cl.Properties.Select(prop => + { + var type = GetTypeInfo(prop.Type); + return Parameter(Identifier(prop.Name.WithLowerFirst())).WithType(ParseTypeName(type.ServerType)); + }).ToArray()); + + var changedName = ChangedFieldsTypeName(cl); + var bits = cl.Properties.Select(p => changedName + "." + p.Name); + var body = Block().AddStatements(ParseStatement($"writer.Write({string.Join("|", bits)});")); + foreach (var prop in cl.Properties) + { + var type = GetTypeInfo(prop.Type); + body = body.AddStatements( + ParseStatement($"writer.Write{(type.IsObject ? "Object" : "")}({prop.Name.WithLowerFirst()});")); + } + + return declaration.WithBody(body); + } + private static BlockSyntax SerializeChangesEpilogue(GClass cl) => Block(ParseStatement(ChangedFieldsFieldName(cl) + " = default;")); @@ -481,15 +501,15 @@ return; writer.Write{(isObject ? "Object" : "")}({PropertyBackingFieldName(prop)}{(isObject && !isPassthrough ? "?.Server!":"")}); "; return body.AddStatements(ParseStatement(code)); - } - + } + private static BlockSyntax DeserializeChangesPrologue(GClass cl) { - return Block(ParseStatement($@" -base.DeserializeChangesCore(reader, committedAt); -DeserializeChangesExtra(reader); -var changed = reader.Read<{ChangedFieldsTypeName(cl)}>(); -")); + return Block( + ParseStatement("base.DeserializeChangesCore(reader, committedAt);"), + ParseStatement("DeserializeChangesExtra(reader);"), + ParseStatement($"var changed = reader.Read<{ChangedFieldsTypeName(cl)}>();") + ); } private static BlockSyntax ApplyDeserializeChangesEpilogue(BlockSyntax body, GClass cl) diff --git a/tests/Avalonia.Base.UnitTests/Media/BrushTests.cs b/tests/Avalonia.Base.UnitTests/Media/BrushTests.cs index 6edce79de1..6b103ca684 100644 --- a/tests/Avalonia.Base.UnitTests/Media/BrushTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/BrushTests.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Media; +using Avalonia.Rendering.Composition.Drawing; using Xunit; namespace Avalonia.Base.UnitTests.Media @@ -82,12 +83,8 @@ namespace Avalonia.Base.UnitTests.Media public void Changing_Opacity_Raises_Invalidated() { var target = new SolidColorBrush(); - var raised = false; - target.Invalidated += (s, e) => raised = true; - target.Opacity = 0.5; - - Assert.True(raised); + RenderResourceTestHelper.AssertResourceInvalidation(target, () => { target.Opacity = 0.5; }); } } } diff --git a/tests/Avalonia.Base.UnitTests/Media/ImageBrushTests.cs b/tests/Avalonia.Base.UnitTests/Media/ImageBrushTests.cs index 2e872e7f7f..d45ca918be 100644 --- a/tests/Avalonia.Base.UnitTests/Media/ImageBrushTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/ImageBrushTests.cs @@ -10,15 +10,14 @@ namespace Avalonia.Base.UnitTests.Media [Fact] public void Changing_Source_Raises_Invalidated() { - var bitmap1 = Mock.Of(); - var bitmap2 = Mock.Of(); + var bitmap1 = Mock.Of(); + var bitmap2 = Mock.Of(); var target = new ImageBrush(bitmap1); - var raised = false; - - target.Invalidated += (s, e) => raised = true; - target.Source = bitmap2; - - Assert.True(raised); + + RenderResourceTestHelper.AssertResourceInvalidation(target, () => + { + target.Source = bitmap2; + }); } } } diff --git a/tests/Avalonia.Base.UnitTests/Media/LinearGradientBrushTests.cs b/tests/Avalonia.Base.UnitTests/Media/LinearGradientBrushTests.cs index 736bafba66..4413147a1f 100644 --- a/tests/Avalonia.Base.UnitTests/Media/LinearGradientBrushTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/LinearGradientBrushTests.cs @@ -10,76 +10,62 @@ namespace Avalonia.Base.UnitTests.Media [Fact] public void Changing_StartPoint_Raises_Invalidated() { - var bitmap1 = Mock.Of(); - var bitmap2 = Mock.Of(); var target = new LinearGradientBrush(); - var raised = false; target.StartPoint = new RelativePoint(); - target.Invalidated += (s, e) => raised = true; - target.StartPoint = new RelativePoint(10, 10, RelativeUnit.Absolute); - Assert.True(raised); + RenderResourceTestHelper.AssertResourceInvalidation(target, () => + { + target.StartPoint = new RelativePoint(10, 10, RelativeUnit.Absolute); + }); } [Fact] public void Changing_EndPoint_Raises_Invalidated() { - var bitmap1 = Mock.Of(); - var bitmap2 = Mock.Of(); var target = new LinearGradientBrush(); - var raised = false; target.EndPoint = new RelativePoint(); - target.Invalidated += (s, e) => raised = true; - target.EndPoint = new RelativePoint(10, 10, RelativeUnit.Absolute); - - Assert.True(raised); + RenderResourceTestHelper.AssertResourceInvalidation(target, () => + { + target.EndPoint = new RelativePoint(10, 10, RelativeUnit.Absolute); + }); } [Fact] public void Changing_GradientStops_Raises_Invalidated() { - var bitmap1 = Mock.Of(); - var bitmap2 = Mock.Of(); var target = new LinearGradientBrush(); - var raised = false; target.GradientStops = new GradientStops { new GradientStop(Colors.Red, 0) }; - target.Invalidated += (s, e) => raised = true; - target.GradientStops = new GradientStops { new GradientStop(Colors.Green, 0) }; - - Assert.True(raised); + RenderResourceTestHelper.AssertResourceInvalidation(target, () => + { + target.GradientStops = new GradientStops { new GradientStop(Colors.Green, 0) }; + }); } [Fact] public void Adding_GradientStop_Raises_Invalidated() { - var bitmap1 = Mock.Of(); - var bitmap2 = Mock.Of(); var target = new LinearGradientBrush(); - var raised = false; target.GradientStops = new GradientStops { new GradientStop(Colors.Red, 0) }; - target.Invalidated += (s, e) => raised = true; - target.GradientStops.Add(new GradientStop(Colors.Green, 1)); - - Assert.True(raised); + RenderResourceTestHelper.AssertResourceInvalidation(target, () => + { + target.GradientStops.Add(new GradientStop(Colors.Green, 1)); + }); } [Fact] public void Changing_GradientStop_Offset_Raises_Invalidated() { - var bitmap1 = Mock.Of(); - var bitmap2 = Mock.Of(); var target = new LinearGradientBrush(); - var raised = false; target.GradientStops = new GradientStops { new GradientStop(Colors.Red, 0) }; - target.Invalidated += (s, e) => raised = true; - target.GradientStops[0].Offset = 0.5; - - Assert.True(raised); + RenderResourceTestHelper.AssertResourceInvalidation(target, () => + { + target.GradientStops[0].Offset = 0.5; + }); } } } diff --git a/tests/Avalonia.Base.UnitTests/Media/PenTests.cs b/tests/Avalonia.Base.UnitTests/Media/PenTests.cs index 6016978fde..156a25c678 100644 --- a/tests/Avalonia.Base.UnitTests/Media/PenTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/PenTests.cs @@ -1,4 +1,5 @@ -using Avalonia.Collections; +using System; +using Avalonia.Collections; using Avalonia.Media; using Avalonia.Media.Immutable; using Xunit; @@ -11,25 +12,24 @@ namespace Avalonia.Base.UnitTests.Media public void Changing_Thickness_Raises_Invalidated() { var target = new Pen(); - var raised = false; - target.Invalidated += (s, e) => raised = true; - target.Thickness = 18; - - Assert.True(raised); + RenderResourceTestHelper.AssertResourceInvalidation(target, () => + { + target.Thickness = 18; + }); } [Fact] - public void Changing_Brush_Color_Raises_Invalidated() + public void Brush_Is_Added_To_The_Same_Compositor() { var brush = new SolidColorBrush(Colors.Red); var target = new Pen { Brush = brush }; - var raised = false; - target.Invalidated += (s, e) => raised = true; - brush.Color = Colors.Green; - - Assert.True(raised); + using var helper = new RenderResourceTestHelper(); + helper.AssertExistsOnCompositor(brush, false); + helper.AddToCompositor(target); + helper.AssertExistsOnCompositor(brush); + Assert.True(helper.IsInvalidated(brush)); } [Fact] @@ -37,12 +37,11 @@ namespace Avalonia.Base.UnitTests.Media { var dashes = new DashStyle(); var target = new Pen { DashStyle = dashes }; - var raised = false; - - target.Invalidated += (s, e) => raised = true; - dashes.Dashes = new AvaloniaList { 0.1, 0.2 }; - Assert.True(raised); + RenderResourceTestHelper.AssertResourceInvalidation(target, () => + { + dashes.Dashes = new AvaloniaList { 0.1, 0.2 }; + }); } [Fact] @@ -50,15 +49,28 @@ namespace Avalonia.Base.UnitTests.Media { var dashes = new DashStyle(); var target = new Pen { DashStyle = dashes }; - var raised = false; - - target.Invalidated += (s, e) => raised = true; + RenderResourceTestHelper.AssertResourceInvalidation(target, () => + { + dashes.Dashes = new AvaloniaList + { + 0.3 + }; + }); + } + + [Fact] + public void Adding_DashStyle_Dash_Raises_Invalidated() + { + var dashes = new DashStyle(); + var target = new Pen { DashStyle = dashes }; dashes.Dashes = new AvaloniaList { 0.3 }; - - Assert.True(raised); + RenderResourceTestHelper.AssertResourceInvalidation(target, () => + { + dashes.Dashes.AddRange(new[] { 1.0, 2 }); + }); } [Fact] diff --git a/tests/Avalonia.Base.UnitTests/Media/RenderResourceTestHelper.cs b/tests/Avalonia.Base.UnitTests/Media/RenderResourceTestHelper.cs new file mode 100644 index 0000000000..ffb07da2f5 --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Media/RenderResourceTestHelper.cs @@ -0,0 +1,55 @@ +using System; +using Avalonia.Rendering; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Threading; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Base.UnitTests.Media; + +internal class RenderResourceTestHelper : IDisposable +{ + public CompositorTestServices Services { get; } = new(); + public Compositor Compositor => Services.Compositor; + + public void AddToCompositor(ICompositionRenderResource resource) => resource.AddRefOnCompositor(Compositor); + + public bool IsInvalidated(ICompositorSerializable resource) => + Compositor.UnitTestIsRegisteredForSerialization(resource); + + public void AssertExistsOnCompositor(ICompositorSerializable resource, bool exists = true) + { + var server = resource.TryGetServer(Compositor); + if (exists) + Assert.NotNull(server); + else + Assert.Null(server); + } + + public static void AssertResourceInvalidation(T resource, Action cb) + where T : ICompositionRenderResource, ICompositorSerializable + { + using var helper = new RenderResourceTestHelper(); + helper.AssertInvalidation(resource, cb); + } + + public void AssertInvalidation(T resource, Action cb) + where T : ICompositionRenderResource, ICompositorSerializable + { + resource.AddRefOnCompositor(Compositor); + Assert.NotNull(resource.TryGetServer(Compositor)); + Assert.True(Compositor.UnitTestIsRegisteredForSerialization(resource)); + + Compositor.Commit(); + Compositor.Server.Render(); + + Assert.False(Compositor.UnitTestIsRegisteredForSerialization(resource)); + cb(); + Assert.True(Compositor.UnitTestIsRegisteredForSerialization(resource)); + resource.ReleaseOnCompositor(Compositor); + Assert.Null(resource.TryGetServer(Compositor)); + } + + public void Dispose() => Services.Dispose(); +} \ No newline at end of file diff --git a/tests/Avalonia.Base.UnitTests/Media/SolidColorBrushTests.cs b/tests/Avalonia.Base.UnitTests/Media/SolidColorBrushTests.cs index c982d8c1c8..d4bd2ec3ac 100644 --- a/tests/Avalonia.Base.UnitTests/Media/SolidColorBrushTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/SolidColorBrushTests.cs @@ -9,12 +9,10 @@ namespace Avalonia.Base.UnitTests.Media public void Changing_Color_Raises_Invalidated() { var target = new SolidColorBrush(Colors.Red); - var raised = false; - - target.Invalidated += (s, e) => raised = true; - target.Color = Colors.Green; - - Assert.True(raised); + RenderResourceTestHelper.AssertResourceInvalidation(target, () => + { + target.Color = Colors.Green; + }); } } } diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs index 86f461673e..1a5ce0ef94 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/DrawOperationTests.cs @@ -3,6 +3,12 @@ using Avalonia.Platform; using Avalonia.Rendering.SceneGraph; using Avalonia.Utilities; using Avalonia.Media.Imaging; +using Avalonia.Media.Immutable; +using Avalonia.Rendering; +using Avalonia.Rendering.Composition; +using Avalonia.Rendering.Composition.Drawing; +using Avalonia.Threading; +using Avalonia.UnitTests; using Moq; using Xunit; @@ -10,12 +16,43 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph { public class DrawOperationTests { + + class TestContext + { + private readonly Compositor _compositor; + public RenderDataDrawingContext Context { get; } + + public TestContext() + { + _compositor = new Compositor(new RenderLoop( + new CompositorTestServices.ManualRenderTimer(), + new Dispatcher(new NullDispatcherImpl())), null); + Context = new RenderDataDrawingContext(_compositor); + } + + public void ForceRender() + { + _compositor.Commit(); + _compositor.Server.Render(); + } + + public Rect? GetBounds() + { + var renderData = Context.GetRenderResults(); + if (renderData == null) + return null; + ForceRender(); + return renderData.Server.Bounds; + } + } + [Fact] public void Empty_Bounds_Remain_Empty() { - var target = new TestDrawOperation(default, Matrix.Identity, null); - - Assert.Equal(default, target.Bounds); + var ctx = new TestContext(); + ctx.Context.DrawRectangle(Brushes.Black, null, default); + + Assert.Null(ctx.GetBounds()); } [Theory] @@ -35,83 +72,60 @@ namespace Avalonia.Base.UnitTests.Rendering.SceneGraph double expectedWidth, double expectedHeight) { - var target = new TestRectangleDrawOperation( - new Rect(x, y, width, height), - Matrix.CreateScale(scaleX, scaleY), - new Pen(Brushes.Black, penThickness)); - Assert.Equal(new Rect(expectedX, expectedY, expectedWidth, expectedHeight), target.Bounds); + var ctx = new TestContext(); + using (ctx.Context.PushTransform(Matrix.CreateScale(scaleX, scaleY))) + ctx.Context.DrawRectangle(null, new ImmutablePen(Brushes.Black, penThickness), new Rect(x, y, width, height)); + + Assert.Equal(new Rect(expectedX, expectedY, expectedWidth, expectedHeight), ctx.GetBounds().Value); } - [Fact] - public void Image_Node_Releases_Reference_To_Bitmap_On_Dispose() + [Theory, InlineData(false), InlineData(true)] + public void Image_Node_Releases_Reference_To_Bitmap_On_Dispose(bool disposeBeforeCommit) { var bitmap = RefCountable.Create(Mock.Of()); - var imageNode = new ImageNode( - Matrix.Identity, - bitmap, - 1, - new Rect(1, 1, 1, 1), - new Rect(1, 1, 1, 1)); + var ctx = new TestContext(); + ctx.Context.DrawBitmap(bitmap, 1, new Rect(1, 1, 1, 1), new Rect(1, 1, 1, 1)); + var renderData = ctx.Context.GetRenderResults()!; Assert.Equal(2, bitmap.RefCount); - - imageNode.Dispose(); - - Assert.Equal(1, bitmap.RefCount); + if (disposeBeforeCommit) + { + renderData.Dispose(); + Assert.Equal(1, bitmap.RefCount); + ctx.ForceRender(); + Assert.Equal(1, bitmap.RefCount); + } + else + { + ctx.ForceRender(); + Assert.Equal(2, bitmap.RefCount); + + // Refs ownership is transferred to server-side render data + renderData.Dispose(); + Assert.Equal(2, bitmap.RefCount); + + ctx.ForceRender(); + Assert.Equal(1, bitmap.RefCount); + } } [Fact] public void HitTest_On_Geometry_Node_With_Zero_Transform_Does_Not_Throw() { - var geometry = Mock.Of(); - var geometryNode = new GeometryNode( - new Matrix(), - Brushes.Black, - null, - geometry); - - geometryNode.HitTest(new Point()); + var ctx = new TestContext(); + using (ctx.Context.PushTransform(new Matrix())) + ctx.Context.DrawGeometry(Brushes.Black, null, Mock.Of()); + Assert.False(ctx.Context.GetRenderResults()!.HitTest(default)); } [Fact] public void HitTest_RectangleNode_With_Transform_Hits() { - var geometry = Mock.Of(); - var geometryNode = new RectangleNode( - Matrix.CreateTranslation(20,20), - Brushes.Black, - null, - new RoundedRect(new Rect(0,0,10,10)), - default); - - var actual = geometryNode.HitTest(new Point(25,25)); - - Assert.True(actual); - } - - private class TestRectangleDrawOperation : RectangleNode - { - public TestRectangleDrawOperation(Rect bounds, Matrix transform, Pen pen) - : base(transform, pen.Brush?.ToImmutable(), pen, bounds, new BoxShadows()) - { - - } - - public override bool HitTestTransformed(Point p) => false; - - public override void Render(IDrawingContextImpl context) { } - } - - private class TestDrawOperation : DrawOperation - { - public TestDrawOperation(Rect bounds, Matrix transform, Pen pen) - :base(bounds, transform) - { - } - - public override bool HitTest(Point p) => false; + var ctx = new TestContext(); + using (ctx.Context.PushTransform(Matrix.CreateTranslation(20, 20))) + ctx.Context.DrawRectangle(Brushes.Black, null, new RoundedRect(new Rect(0, 0, 10, 10))); - public override void Render(IDrawingContextImpl context) { } + Assert.True(ctx.Context.GetRenderResults()!.HitTest(new Point(25, 25))); } } } diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs index a2e438e3e0..89cc63edec 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/EllipseNodeTests.cs @@ -1,5 +1,6 @@ using Avalonia.Media; using Avalonia.Media.Immutable; +using Avalonia.Rendering.Composition.Drawing.Nodes; using Avalonia.Rendering.SceneGraph; using Xunit; @@ -18,8 +19,12 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph [InlineData(0, 101, false)] public void FillOnly_HitTest(double x, double y, bool inside) { - var ellipseNode = new EllipseNode(Matrix.Identity, Brushes.Black, null, new Rect(0,0, 100, 100)); - + var ellipseNode = new RenderDataEllipseNode() + { + Rect = new Rect(0, 0, 100, 100), + ServerBrush = Brushes.Black + }; + var point = new Point(x, y); Assert.True(ellipseNode.HitTest(point) == inside); @@ -37,7 +42,13 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph [InlineData(0, 101, false)] public void StrokeOnly_HitTest(double x, double y, bool inside) { - var ellipseNode = new EllipseNode(Matrix.Identity, null, new ImmutablePen(Brushes.Black, 2), new Rect(0, 0, 100, 100)); + var pen = new ImmutablePen(Brushes.Black, 2); + var ellipseNode = new RenderDataEllipseNode() + { + Rect = new Rect(0, 0, 100, 100), + ServerPen = pen, + ClientPen = pen + }; var point = new Point(x, y); diff --git a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/LineNodeTests.cs b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/LineNodeTests.cs index 56f2b03932..2e36bd6392 100644 --- a/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/LineNodeTests.cs +++ b/tests/Avalonia.Base.UnitTests/Rendering/SceneGraph/LineNodeTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Avalonia.Media; +using Avalonia.Rendering.Composition.Drawing.Nodes; using Avalonia.Rendering.SceneGraph; using Xunit; @@ -7,11 +8,18 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph { public class LineNodeTests { + static RenderDataLineNode LineNode(IPen pen, Point p1, Point p2) => new RenderDataLineNode + { + P1 = p1, + P2 = p2, + ServerPen = pen, + ClientPen = pen + }; + [Fact] public void HitTest_Should_Be_True() { - var lineNode = new LineNode( - Matrix.Identity, + var lineNode = LineNode( new Pen(Brushes.Black, 3), new Point(15, 10), new Point(150, 73)); @@ -35,8 +43,7 @@ namespace Avalonia.Visuals.UnitTests.Rendering.SceneGraph [Fact] public void HitTest_Should_Be_False() { - var lineNode = new LineNode( - Matrix.Identity, + var lineNode = LineNode( new Pen(Brushes.Black, 3), new Point(15, 10), new Point(150, 73)); diff --git a/tests/Avalonia.Controls.UnitTests/BorderTests.cs b/tests/Avalonia.Controls.UnitTests/BorderTests.cs index e9ddc8e8a3..dcfa9931c4 100644 --- a/tests/Avalonia.Controls.UnitTests/BorderTests.cs +++ b/tests/Avalonia.Controls.UnitTests/BorderTests.cs @@ -45,24 +45,7 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Rect(6, 6, 0, 0), content.Bounds); } - - [Fact] - public void Changing_Background_Brush_Color_Should_Invalidate_Visual() - { - var target = new Border() - { - Background = new SolidColorBrush(Colors.Red), - }; - - var root = new TestRoot(target); - var renderer = Mock.Get(root.Renderer); - renderer.Invocations.Clear(); - - ((SolidColorBrush)target.Background).Color = Colors.Green; - - renderer.Verify(x => x.AddDirty(target), Times.Once); - } - + public class UseLayoutRounding { [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/PanelTests.cs b/tests/Avalonia.Controls.UnitTests/PanelTests.cs index 82f133d533..313473a76a 100644 --- a/tests/Avalonia.Controls.UnitTests/PanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/PanelTests.cs @@ -117,24 +117,7 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new[] { child2, child1 }, panel.GetLogicalChildren()); Assert.Equal(new[] { child2, child1 }, panel.GetVisualChildren()); } - - [Fact] - public void Changing_Background_Brush_Color_Should_Invalidate_Visual() - { - var target = new Panel() - { - Background = new SolidColorBrush(Colors.Red), - }; - - var root = new TestRoot(target); - var renderer = Mock.Get(root.Renderer); - renderer.Invocations.Clear(); - - ((SolidColorBrush)target.Background).Color = Colors.Green; - - renderer.Verify(x => x.AddDirty(target), Times.Once); - } - + [Fact] public void Adding_Null_Child_Should_Throw() { diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs index b3257d2d01..c31b85ddf7 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs @@ -202,23 +202,6 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.NotEqual(foo, logicalChildren.First()); } - [Fact] - public void Changing_Background_Brush_Color_Should_Invalidate_Visual() - { - var target = new ContentPresenter() - { - Background = new SolidColorBrush(Colors.Red), - }; - - var root = new TestRoot(target); - var renderer = Mock.Get(root.Renderer); - renderer.Invocations.Clear(); - - ((SolidColorBrush)target.Background).Color = Colors.Green; - - renderer.Verify(x => x.AddDirty(target), Times.Once); - } - [Fact] public void Should_Not_Bind_Old_Child_To_New_DataContext() { diff --git a/tests/Avalonia.Controls.UnitTests/Shapes/RectangleTests.cs b/tests/Avalonia.Controls.UnitTests/Shapes/RectangleTests.cs index 067d709ae1..65be7fadaf 100644 --- a/tests/Avalonia.Controls.UnitTests/Shapes/RectangleTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Shapes/RectangleTests.cs @@ -54,39 +54,5 @@ namespace Avalonia.Controls.UnitTests.Shapes geometry = Assert.IsType(target.RenderedGeometry); Assert.Equal(new Rect(0, 0, 200, 200), geometry.Rect); } - - [Fact] - public void Changing_Fill_Brush_Color_Should_Invalidate_Visual() - { - var target = new Rectangle() - { - Fill = new SolidColorBrush(Colors.Red), - }; - - var root = new TestRoot(target); - var renderer = Mock.Get(root.Renderer); - renderer.Invocations.Clear(); - - ((SolidColorBrush)target.Fill).Color = Colors.Green; - - renderer.Verify(x => x.AddDirty(target), Times.Once); - } - - [Fact] - public void Changing_Stroke_Brush_Color_Should_Invalidate_Visual() - { - var target = new Rectangle() - { - Stroke = new SolidColorBrush(Colors.Red), - }; - - var root = new TestRoot(target); - var renderer = Mock.Get(root.Renderer); - renderer.Invocations.Clear(); - - ((SolidColorBrush)target.Stroke).Color = Colors.Green; - - renderer.Verify(x => x.AddDirty(target), Times.Once); - } } } diff --git a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs index d8da198f5b..e9250788c0 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs @@ -29,40 +29,6 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(null, textBlock.Text); } - [Fact] - public void Changing_Background_Brush_Color_Should_Invalidate_Visual() - { - var target = new TextBlock() - { - Background = new SolidColorBrush(Colors.Red), - }; - - var root = new TestRoot(target); - var renderer = Mock.Get(root.Renderer); - renderer.Invocations.Clear(); - - ((SolidColorBrush)target.Background).Color = Colors.Green; - - renderer.Verify(x => x.AddDirty(target), Times.Once); - } - - [Fact] - public void Changing_Foreground_Brush_Color_Should_Invalidate_Visual() - { - var target = new TextBlock() - { - Foreground = new SolidColorBrush(Colors.Red), - }; - - var root = new TestRoot(target); - var renderer = Mock.Get(root.Renderer); - renderer.Invocations.Clear(); - - ((SolidColorBrush)target.Foreground).Color = Colors.Green; - - renderer.Verify(x => x.AddDirty(target), Times.Once); - } - [Fact] public void Changing_InlinesCollection_Should_Invalidate_Measure() {