Browse Source

Composition aware resources

pull/11340/head
Nikita Tsukanov 3 years ago
parent
commit
64610c264b
  1. 71
      nukebuild/RefAssemblyGenerator.cs
  2. 2
      samples/ControlCatalog/Pages/TabControlPage.xaml.cs
  3. 2
      samples/ControlCatalog/ViewModels/TabControlPageViewModel.cs
  4. 6
      samples/RenderDemo/Pages/CustomSkiaPage.cs
  5. 8
      samples/RenderDemo/Pages/PathMeasurementPage.cs
  6. 2
      src/Avalonia.Base/Collections/Pooled/PooledList.cs
  7. 2
      src/Avalonia.Base/Collections/Pooled/PooledStack.cs
  8. 4
      src/Avalonia.Base/Input/Cursor.cs
  9. 80
      src/Avalonia.Base/Media/Brush.cs
  10. 18
      src/Avalonia.Base/Media/ConicGradientBrush.cs
  11. 4
      src/Avalonia.Base/Media/DashStyle.cs
  12. 4
      src/Avalonia.Base/Media/Drawing.cs
  13. 61
      src/Avalonia.Base/Media/DrawingBrush.cs
  14. 2
      src/Avalonia.Base/Media/DrawingGroup.cs
  15. 4
      src/Avalonia.Base/Media/Effects/IEffect.cs
  16. 70
      src/Avalonia.Base/Media/Geometry.cs
  17. 2
      src/Avalonia.Base/Media/GeometryDrawing.cs
  18. 5
      src/Avalonia.Base/Media/GlyphRun.cs
  19. 2
      src/Avalonia.Base/Media/GlyphRunDrawing.cs
  20. 42
      src/Avalonia.Base/Media/GradientBrush.cs
  21. 2
      src/Avalonia.Base/Media/IAffectsRender.cs
  22. 10
      src/Avalonia.Base/Media/IImageBrush.cs
  23. 25
      src/Avalonia.Base/Media/IImmutableGlyphRunReference.cs
  24. 2
      src/Avalonia.Base/Media/IMutableBrush.cs
  25. 2
      src/Avalonia.Base/Media/IMutableExperimentalAcrylicMaterial.cs
  26. 2
      src/Avalonia.Base/Media/ITransform.cs
  27. 27
      src/Avalonia.Base/Media/ImageBrush.cs
  28. 2
      src/Avalonia.Base/Media/ImageDrawing.cs
  29. 10
      src/Avalonia.Base/Media/Imaging/Bitmap.cs
  30. 2
      src/Avalonia.Base/Media/Imaging/IBitmap.cs
  31. 2
      src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs
  32. 12
      src/Avalonia.Base/Media/ImmediateDrawingContext.cs
  33. 4
      src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs
  34. 2
      src/Avalonia.Base/Media/Immutable/ImmutableTileBrush.cs
  35. 18
      src/Avalonia.Base/Media/LinearGradientBrush.cs
  36. 112
      src/Avalonia.Base/Media/Pen.cs
  37. 6
      src/Avalonia.Base/Media/PlatformDrawingContext.cs
  38. 18
      src/Avalonia.Base/Media/RadialGradientBrush.cs
  39. 20
      src/Avalonia.Base/Media/SolidColorBrush.cs
  40. 23
      src/Avalonia.Base/Media/TileBrush.cs
  41. 20
      src/Avalonia.Base/Media/Transform.cs
  42. 64
      src/Avalonia.Base/Media/VisualBrush.cs
  43. 2
      src/Avalonia.Base/Metadata/PrivateApiAttribute.cs
  44. 2
      src/Avalonia.Base/Platform/IBitmapImpl.cs
  45. 6
      src/Avalonia.Base/Platform/IDrawingContextImpl.cs
  46. 2
      src/Avalonia.Base/Platform/IDrawingContextWithAcrylicLikeSupport.cs
  47. 12
      src/Avalonia.Base/Reactive/Observable.cs
  48. 7
      src/Avalonia.Base/Rect.cs
  49. 61
      src/Avalonia.Base/Rendering/Composition/Brushes/ServerSimpleCompositionBrush.cs
  50. 24
      src/Avalonia.Base/Rendering/Composition/Brushes/ServerSimpleContentBrush.cs
  51. 37
      src/Avalonia.Base/Rendering/Composition/Brushes/ServerSimpleImageBrush.cs
  52. 20
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  53. 14
      src/Avalonia.Base/Rendering/Composition/CompositionDrawListVisual.cs
  54. 5
      src/Avalonia.Base/Rendering/Composition/CompositionDrawingSurface.cs
  55. 11
      src/Avalonia.Base/Rendering/Composition/CompositionExperimentalAcrylicVisual.cs
  56. 28
      src/Avalonia.Base/Rendering/Composition/CompositionObject.cs
  57. 3
      src/Avalonia.Base/Rendering/Composition/CompositionPropertySet.cs
  58. 2
      src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
  59. 9
      src/Avalonia.Base/Rendering/Composition/CompositionTransform.cs
  60. 2
      src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs
  61. 47
      src/Avalonia.Base/Rendering/Composition/Compositor.cs
  62. 129
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs
  63. 37
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawListSceneBrushContent.cs
  64. 371
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs
  65. 68
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderData.cs
  66. 47
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositionRenderDataSceneBrushContent.cs
  67. 123
      src/Avalonia.Base/Rendering/Composition/Drawing/CompositorResourceHelpers.cs
  68. 12
      src/Avalonia.Base/Rendering/Composition/Drawing/ICompositionRenderResource.cs
  69. 81
      src/Avalonia.Base/Rendering/Composition/Drawing/ImmediateRenderDataSceneBrushContent.cs
  70. 28
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataBitmapNode.cs
  71. 61
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataEllipseNode.cs
  72. 29
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGeometryNode.cs
  73. 35
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGlyphRunNode.cs
  74. 65
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataLineNode.cs
  75. 214
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataNodes.cs
  76. 27
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataPushMatrixNode.cs
  77. 25
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataPushOpacityMaskNode.cs
  78. 30
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataRectangleNode.cs
  79. 349
      src/Avalonia.Base/Rendering/Composition/Drawing/RenderDataDrawingContext.cs
  80. 136
      src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionRenderData.cs
  81. 12
      src/Avalonia.Base/Rendering/Composition/Drawing/ServerCompositionSimplePen.cs
  82. 56
      src/Avalonia.Base/Rendering/Composition/Drawing/ServerResourceHelperExtensions.cs
  83. 10
      src/Avalonia.Base/Rendering/Composition/ICompositorSerializable.cs
  84. 2
      src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs
  85. 9
      src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs
  86. 24
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs
  87. 26
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs
  88. 2
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs
  89. 2
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  90. 22
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.RenderResources.cs
  91. 23
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs
  92. 11
      src/Avalonia.Base/Rendering/Composition/Server/ServerList.cs
  93. 32
      src/Avalonia.Base/Rendering/Composition/Server/ServerObject.cs
  94. 123
      src/Avalonia.Base/Rendering/Composition/Server/ServerRenderResource.cs
  95. 34
      src/Avalonia.Base/Rendering/Composition/Server/SimpleServerObject.cs
  96. 27
      src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs
  97. 87
      src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs
  98. 31
      src/Avalonia.Base/Rendering/SceneGraph/CustomDrawOperation.cs
  99. 56
      src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs
  100. 97
      src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs

71
nukebuild/RefAssemblyGenerator.cs

@ -63,50 +63,51 @@ public class RefAssemblyGenerator
});
}
static bool HasPrivateApi(IEnumerable<CustomAttribute> 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)

2
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<IAssetLoader>();
return new Bitmap(assets.Open(new Uri(uri)));

2
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;
}

6
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<ISkiaSharpApiLeaseFeature>();
if (leaseFeature == null)
context.DrawGlyphRun(Brushes.Black, _noSkia.PlatformImpl);
context.DrawGlyphRun(Brushes.Black, _noSkia);
else
{
using var lease = leaseFeature.Lease();

8
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);

2
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<T>());
#else

2
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<T>());
#else

4
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();

80
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.
/// </summary>
[TypeConverter(typeof(BrushConverter))]
public abstract class Brush : Animatable, IBrush
public abstract class Brush : Animatable, IBrush, ICompositionRenderResource<IBrush>, ICompositorSerializable
{
/// <summary>
/// Defines the <see cref="Opacity"/> property.
@ -30,14 +34,10 @@ namespace Avalonia.Media
/// </summary>
public static readonly StyledProperty<RelativePoint> TransformOriginProperty =
AvaloniaProperty.Register<Brush, RelativePoint>(nameof(TransformOrigin));
/// <inheritdoc/>
public event EventHandler? Invalidated;
static Brush()
{
Animation.Animation.RegisterAnimator<BaseBrushAnimator>(prop => typeof(IBrush).IsAssignableFrom(prop.PropertyType));
AffectsRender<Brush>(OpacityProperty, TransformProperty);
}
/// <summary>
@ -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);
/// <summary>
/// Marks a property as affecting the brush's visual representation.
/// </summary>
/// <param name="properties">The properties.</param>
/// <remarks>
/// After a call to this method in a brush's static constructor, any change to the
/// property will cause the <see cref="Invalidated"/> event to be raised on the brush.
/// </remarks>
protected static void AffectsRender<T>(params AvaloniaProperty[] properties)
where T : Brush
RegisterForSerialization();
base.OnPropertyChanged(change);
}
private protected void RegisterForSerialization() =>
_resource.RegisterForInvalidationOnAllCompositors(this);
private CompositorResourceHolder<ServerCompositionSimpleBrush> _resource;
IBrush ICompositionRenderResource<IBrush>.GetForCompositor(Compositor c) => _resource.GetForCompositor(c);
internal abstract Func<Compositor, ServerCompositionSimpleBrush> Factory { get; }
void ICompositionRenderResource.AddRefOnCompositor(Compositor c)
{
var invalidateObserver = new AnonymousObserver<AvaloniaPropertyChangedEventArgs>(
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<ITransform> resource)
resource.AddRefOnCompositor(c);
}
/// <summary>
/// Raises the <see cref="Invalidated"/> event.
/// </summary>
/// <param name="e">The event args.</param>
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<ITransform> 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);
}
}

18
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<ConicGradientBrush>(CenterProperty, AngleProperty);
}
/// <summary>
/// Gets or sets the center point of the gradient.
/// </summary>
@ -51,5 +50,14 @@ namespace Avalonia.Media
{
return new ImmutableConicGradientBrush(this);
}
internal override Func<Compositor, ServerCompositionSimpleBrush> 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);
}
}
}

4
src/Avalonia.Base/Media/DashStyle.cs

@ -13,7 +13,7 @@ namespace Avalonia.Media
/// <summary>
/// Represents the sequence of dashes and gaps that will be applied by a <see cref="Pen"/>.
/// </summary>
public class DashStyle : Animatable, IDashStyle, IAffectsRender
public class DashStyle : Animatable, IDashStyle
{
/// <summary>
/// Defines the <see cref="Dashes"/> property.
@ -104,7 +104,7 @@ namespace Avalonia.Media
/// <summary>
/// Raised when the dash style changes.
/// </summary>
public event EventHandler? Invalidated;
internal event EventHandler? Invalidated;
/// <summary>
/// Returns an immutable clone of the <see cref="DashStyle"/>.

4
src/Avalonia.Base/Media/Drawing.cs

@ -9,7 +9,9 @@
/// Draws this drawing to the given <see cref="DrawingContext"/>.
/// </summary>
/// <param name="context">The drawing context.</param>
public abstract void Draw(DrawingContext context);
public void Draw(DrawingContext context) => DrawCore(context);
internal abstract void DrawCore(DrawingContext context);
/// <summary>
/// Gets the drawing's bounding rectangle.

61
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
{
/// <summary>
/// Paints an area with an <see cref="Drawing"/>.
/// </summary>
public class DrawingBrush : TileBrush, ISceneBrush, IAffectsRender
public class DrawingBrush : TileBrush, ISceneBrush
{
/// <summary>
/// Defines the <see cref="Drawing"/> property.
/// </summary>
public static readonly StyledProperty<Drawing?> DrawingProperty =
AvaloniaProperty.Register<DrawingBrush, Drawing?>(nameof(Drawing));
static DrawingBrush()
{
AffectsRender<DrawingBrush>(DrawingProperty);
}
/// <summary>
/// Initializes a new instance of the <see cref="DrawingBrush"/> class.
/// </summary>
@ -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<Compositor, ServerCompositionSimpleBrush> Factory =>
static c => new ServerCompositionSimpleContentBrush(c.Server);
private InlineDictionary<Compositor, CompositionRenderDataSceneBrushContent?> _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<IBrush>)this).GetForCompositor(c),
renderData, null, true);
}
}
}

2
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();

4
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
{
/// <summary>
/// Creates an immutable clone of the effect.

70
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
/// <summary>
/// Gets the platform-specific implementation of the geometry.
/// </summary>
public IGeometryImpl? PlatformImpl
internal IGeometryImpl? PlatformImpl
{
get
{
@ -192,6 +193,12 @@ namespace Avalonia.Media
control?.InvalidateGeometry();
}
/// <summary>
/// Gets the geometry's total length as if all its contours are placed
/// in a straight line.
/// </summary>
public double ContourLength => PlatformImpl?.ContourLength ?? 0;
/// <summary>
/// Combines the two geometries using the specified <see cref="GeometryCombineMode"/> and applies the specified transform to the resulting geometry.
/// </summary>
@ -204,6 +211,67 @@ namespace Avalonia.Media
{
return new CombinedGeometry(combineMode, geometry1, geometry2, transform);
}
/// <summary>
/// Attempts to get the corresponding point at the
/// specified distance
/// </summary>
/// <param name="distance">The contour distance to get from.</param>
/// <param name="point">The point in the specified distance.</param>
/// <returns>If there's valid point at the specified distance.</returns>
public bool TryGetPointAtDistance(double distance, out Point point)
{
if (PlatformImpl == null)
{
point = default;
return false;
}
return PlatformImpl.TryGetPointAtDistance(distance, out point);
}
/// <summary>
/// Attempts to get the corresponding point and
/// tangent from the specified distance along the
/// contour of the geometry.
/// </summary>
/// <param name="distance">The contour distance to get from.</param>
/// <param name="point">The point in the specified distance.</param>
/// <param name="tangent">The tangent in the specified distance.</param>
/// <returns>If there's valid point and tangent at the specified distance.</returns>
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);
}
/// <summary>
/// Attempts to get the corresponding path segment
/// given by the two distances specified.
/// Imagine it like snipping a part of the current
/// geometry.
/// </summary>
/// <param name="startDistance">The contour distance to start snipping from.</param>
/// <param name="stopDistance">The contour distance to stop snipping to.</param>
/// <param name="startOnBeginFigure">If ture, the resulting snipped path will start with a BeginFigure call.</param>
/// <param name="segmentGeometry">The resulting snipped path.</param>
/// <returns>If the snipping operation is successful.</returns>
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

2
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)
{

5
src/Avalonia.Base/Media/GlyphRun.cs

@ -214,7 +214,7 @@ namespace Avalonia.Media
/// <summary>
/// The platform implementation of the <see cref="GlyphRun"/>.
/// </summary>
public IRef<IGlyphRunImpl> PlatformImpl
internal IRef<IGlyphRunImpl> PlatformImpl
=> _platformImpl ??= CreateGlyphRunImpl();
/// <summary>
@ -851,5 +851,8 @@ namespace Avalonia.Media
{
return PlatformImpl.Item.GetIntersections(lowerLimit, upperLimit);
}
public IImmutableGlyphRunReference? TryCreateImmutableGlyphRunReference()
=> new ImmutableGlyphRunReference(PlatformImpl.Clone());
}
}

2
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)
{

42
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<LinearGradientBrush>(SpreadMethodProperty);
}
/// <summary>
/// Initializes a new instance of the <see cref="GradientBrush"/> class.
/// </summary>
@ -63,37 +60,46 @@ namespace Avalonia.Media
/// <inheritdoc/>
IReadOnlyList<IGradientStop> 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<GradientStops?>();
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<object?, PropertyChangedEventArgs> 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();

2
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
/// <see cref="Visual.InvalidateVisual"/>.
/// </summary>
public interface IAffectsRender
internal interface IAffectsRender
{
/// <summary>
/// Raised when the resource changes visually.

10
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
/// <summary>
/// Gets the image to draw.
/// </summary>
IBitmap? Source { get; }
IImageBrushSource? Source { get; }
}
[NotClientImplementable]
public interface IImageBrushSource
{
internal IRef<IBitmapImpl>? Bitmap { get; }
}
}

25
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<IGlyphRunImpl>? GlyphRun { get; }
}
internal class ImmutableGlyphRunReference : IImmutableGlyphRunReference
{
public ImmutableGlyphRunReference(IRef<IGlyphRunImpl>? glyphRun)
{
GlyphRun = glyphRun;
}
public IRef<IGlyphRunImpl>? GlyphRun { get; private set; }
public void Dispose()
{
GlyphRun?.Dispose();
GlyphRun = null;
}
}

2
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.
/// </summary>
[NotClientImplementable]
internal interface IMutableBrush : IBrush, IAffectsRender
internal interface IMutableBrush : IBrush
{
/// <summary>
/// Creates an immutable clone of the brush.

2
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.
/// </summary>
[NotClientImplementable]
public interface IMutableExperimentalAcrylicMaterial : IExperimentalAcrylicMaterial, IAffectsRender
public interface IMutableExperimentalAcrylicMaterial : IExperimentalAcrylicMaterial
{
/// <summary>
/// Creates an immutable clone of the brush.

2
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; }

27
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
/// <summary>
/// Defines the <see cref="Visual"/> property.
/// </summary>
public static readonly StyledProperty<IBitmap?> SourceProperty =
AvaloniaProperty.Register<ImageBrush, IBitmap?>(nameof(Source));
static ImageBrush()
{
AffectsRender<ImageBrush>(SourceProperty);
}
public static readonly StyledProperty<IImageBrushSource?> SourceProperty =
AvaloniaProperty.Register<ImageBrush, IImageBrushSource?>(nameof(Source));
/// <summary>
/// Initializes a new instance of the <see cref="ImageBrush"/> class.
@ -30,7 +29,7 @@ namespace Avalonia.Media
/// Initializes a new instance of the <see cref="ImageBrush"/> class.
/// </summary>
/// <param name="source">The image to draw.</param>
public ImageBrush(IBitmap? source)
public ImageBrush(IImageBrushSource? source)
{
Source = source;
}
@ -38,7 +37,7 @@ namespace Avalonia.Media
/// <summary>
/// Gets or sets the image to draw.
/// </summary>
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<Compositor, ServerCompositionSimpleBrush> 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);
}
}
}

2
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;

10
src/Avalonia.Base/Media/Imaging/Bitmap.cs

@ -10,7 +10,7 @@ namespace Avalonia.Media.Imaging
/// <summary>
/// Holds a bitmap image.
/// </summary>
public class Bitmap : IBitmap
public class Bitmap : IBitmap, IImageBrushSource
{
private bool _isTranscoded;
/// <summary>
@ -72,7 +72,7 @@ namespace Avalonia.Media.Imaging
/// Initializes a new instance of the <see cref="Bitmap"/> class.
/// </summary>
/// <param name="impl">A platform-specific bitmap implementation.</param>
public Bitmap(IRef<IBitmapImpl> impl)
internal Bitmap(IRef<IBitmapImpl> impl)
{
PlatformImpl = impl.Clone();
}
@ -139,7 +139,9 @@ namespace Avalonia.Media.Imaging
/// <summary>
/// Gets the platform-specific bitmap implementation.
/// </summary>
public IRef<IBitmapImpl> PlatformImpl { get; }
internal IRef<IBitmapImpl> PlatformImpl { get; }
IRef<IBitmapImpl> IBitmap.PlatformImpl => PlatformImpl;
/// <summary>
/// Saves the bitmap to a file.
@ -237,5 +239,7 @@ namespace Avalonia.Media.Imaging
{
return AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
}
IRef<IBitmapImpl> IImageBrushSource.Bitmap => PlatformImpl;
}
}

2
src/Avalonia.Base/Media/Imaging/IBitmap.cs

@ -10,7 +10,7 @@ namespace Avalonia.Media.Imaging
/// Represents a bitmap image.
/// </summary>
[NotClientImplementable]
public interface IBitmap : IImage, IDisposable
internal interface IBitmap : IImage, IDisposable
{
/// <summary>
/// Gets the dots per inch (DPI) of the image.

2
src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs

@ -38,7 +38,7 @@ namespace Avalonia.Media.Imaging
/// <summary>
/// Gets the platform-specific bitmap implementation.
/// </summary>
public new IRef<IRenderTargetBitmapImpl> PlatformImpl { get; }
internal new IRef<IRenderTargetBitmapImpl> PlatformImpl { get; }
/// <summary>
/// Renders a visual to the <see cref="RenderTargetBitmap"/>.

12
src/Avalonia.Base/Media/ImmediateDrawingContext.cs

@ -67,7 +67,7 @@ namespace Avalonia.Media
/// </summary>
/// <param name="source">The bitmap.</param>
/// <param name="rect">The rect in the output to draw to.</param>
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
/// <param name="source">The bitmap.</param>
/// <param name="sourceRect">The rect in the image to draw.</param>
/// <param name="destRect">The rect in the output to draw to.</param>
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);
}
/// <summary>
@ -180,11 +180,11 @@ namespace Avalonia.Media
/// </summary>
/// <param name="foreground">The foreground brush.</param>
/// <param name="glyphRun">The glyph run.</param>
public void DrawGlyphRun(IImmutableBrush foreground, IRef<IGlyphRunImpl> 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);
}
/// <summary>

4
src/Avalonia.Base/Media/Immutable/ImmutableImageBrush.cs

@ -23,7 +23,7 @@ namespace Avalonia.Media.Immutable
/// </param>
/// <param name="tileMode">The tile mode.</param>
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
}
/// <inheritdoc/>
public IBitmap? Source { get; }
public IImageBrushSource? Source { get; }
}
}

2
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.
/// </param>
/// <param name="tileMode">The tile mode.</param>
protected ImmutableTileBrush(
protected internal ImmutableTileBrush(
AlignmentX alignmentX,
AlignmentY alignmentY,
RelativeRect destinationRect,

18
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<LinearGradientBrush>(StartPointProperty, EndPointProperty);
}
/// <summary>
/// Gets or sets the start point for the gradient.
/// </summary>
@ -51,5 +50,14 @@ namespace Avalonia.Media
{
return new ImmutableLinearGradientBrush(this);
}
internal override Func<Compositor, ServerCompositionSimpleBrush> 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);
}
}
}

112
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
/// <summary>
/// Describes how a stroke is drawn.
/// </summary>
public sealed class Pen : AvaloniaObject, IPen
public sealed class Pen : AvaloniaObject, IPen, ICompositionRenderResource<IPen>, ICompositorSerializable
{
/// <summary>
/// Defines the <see cref="Brush"/> property.
@ -45,9 +49,7 @@ namespace Avalonia.Media
public static readonly StyledProperty<double> MiterLimitProperty =
AvaloniaProperty.Register<Pen, double>(nameof(MiterLimit), 10.0);
private EventHandler? _invalidated;
private IAffectsRender? _subscribedToBrush;
private IAffectsRender? _subscribedToDashes;
private DashStyle? _subscribedToDashes;
private TargetWeakEventSubscriber<Pen, EventArgs>? _weakSubscriber;
/// <summary>
@ -110,8 +112,8 @@ namespace Avalonia.Media
set => SetValue(BrushProperty, value);
}
private static readonly WeakEvent<IAffectsRender, EventArgs> InvalidatedWeakEvent =
WeakEvent.Register<IAffectsRender>(
private static readonly WeakEvent<DashStyle, EventArgs> InvalidatedWeakEvent =
WeakEvent.Register<DashStyle>(
(s, h) => s.Invalidated += h,
(s, h) => s.Invalidated -= h);
@ -161,23 +163,6 @@ namespace Avalonia.Media
set => SetValue(MiterLimitProperty, value);
}
/// <summary>
/// Raised when the pen changes.
/// </summary>
public event EventHandler? Invalidated
{
add
{
_invalidated += value;
UpdateSubscriptions();
}
remove
{
_invalidated -= value;
UpdateSubscriptions();
}
}
/// <summary>
/// Creates an immutable clone of the brush.
/// </summary>
@ -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<Pen, EventArgs>(
this, static (target, _, ev, _) =>
{
if (ev == InvalidatedWeakEvent)
target.RegisterForSerialization();
});
InvalidatedWeakEvent.Subscribe(newValue, _weakSubscriber);
_subscribedToDashes = newValue;
}
}
private CompositorResourceHolder<ServerCompositionSimplePen> _resource;
IPen ICompositionRenderResource<IPen>.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<Pen, EventArgs>(
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);
}
}
}

6
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<IBitmapImpl> 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);

18
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<RadialGradientBrush>(CenterProperty, GradientOriginProperty, RadiusProperty);
}
/// <summary>
/// Gets or sets the start point for the gradient.
/// </summary>
@ -71,5 +70,14 @@ namespace Avalonia.Media
{
return new ImmutableRadialGradientBrush(this);
}
internal override Func<Compositor, ServerCompositionSimpleBrush> 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);
}
}
}

20
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
/// </summary>
public static readonly StyledProperty<Color> ColorProperty =
AvaloniaProperty.Register<SolidColorBrush, Color>(nameof(Color));
static SolidColorBrush()
{
AffectsRender<SolidColorBrush>(ColorProperty);
}
/// <summary>
/// Initializes a new instance of the <see cref="SolidColorBrush"/> class.
/// </summary>
@ -84,5 +83,14 @@ namespace Avalonia.Media
{
return new ImmutableSolidColorBrush(this);
}
internal override Func<Compositor, ServerCompositionSimpleBrush> Factory =>
static c => new ServerCompositionSimpleSolidColorBrush(c.Server);
private protected override void SerializeChanges(Compositor c, BatchStreamWriter writer)
{
base.SerializeChanges(c, writer);
ServerCompositionSimpleSolidColorBrush.SerializeAllChanges(writer, Color);
}
}
}

23
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
/// </summary>
public static readonly StyledProperty<TileMode> TileModeProperty =
AvaloniaProperty.Register<TileBrush, TileMode>(nameof(TileMode));
static TileBrush()
{
AffectsRender<TileBrush>(
AlignmentXProperty,
AlignmentYProperty,
DestinationRectProperty,
SourceRectProperty,
StretchProperty,
TileModeProperty);
}
/// <summary>
/// Gets or sets the horizontal alignment of a tile in the destination.
/// </summary>
@ -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);
}
}
}

20
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
/// <summary>
/// Represents a transform on an <see cref="Visual"/>.
/// </summary>
public abstract class Transform : Animatable, IMutableTransform
public abstract class Transform : Animatable, IMutableTransform, ICompositionRenderResource<ITransform>, ICompositorSerializable
{
static Transform()
{
@ -62,5 +66,19 @@ namespace Avalonia.Media
{
return Value.ToString();
}
private CompositorResourceHolder<ServerCompositionSimpleTransform> _resource;
ITransform ICompositionRenderResource<ITransform>.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);
}
}

64
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
{
/// <summary>
/// Paints an area with an <see cref="Visual"/>.
/// </summary>
public class VisualBrush : TileBrush, ISceneBrush, IAffectsRender
public class VisualBrush : TileBrush, ISceneBrush
{
/// <summary>
/// Defines the <see cref="Visual"/> property.
/// </summary>
public static readonly StyledProperty<Visual?> VisualProperty =
AvaloniaProperty.Register<VisualBrush, Visual?>(nameof(Visual));
static VisualBrush()
{
AffectsRender<VisualBrush>(VisualProperty);
}
/// <summary>
/// Initializes a new instance of the <see cref="VisualBrush"/> 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<Compositor, ServerCompositionSimpleBrush> Factory =>
static c => new ServerCompositionSimpleContentBrush(c.Server);
private InlineDictionary<Compositor, CompositionRenderDataSceneBrushContent?> _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<IBrush>)this).GetForCompositor(c),
renderData, new(Visual.Bounds.Size), false);
}
}
}

2
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
{

2
src/Avalonia.Base/Platform/IBitmapImpl.cs

@ -7,7 +7,7 @@ namespace Avalonia.Platform
/// <summary>
/// Defines the platform-specific interface for a <see cref="Avalonia.Media.Imaging.Bitmap"/>.
/// </summary>
[Unstable]
[PrivateApi]
public interface IBitmapImpl : IDisposable
{
/// <summary>

6
src/Avalonia.Base/Platform/IDrawingContextImpl.cs

@ -35,7 +35,7 @@ namespace Avalonia.Platform
/// <param name="opacity">The opacity to draw with.</param>
/// <param name="sourceRect">The rect in the image to draw.</param>
/// <param name="destRect">The rect in the output to draw to.</param>
void DrawBitmap(IRef<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect);
void DrawBitmap(IBitmapImpl source, double opacity, Rect sourceRect, Rect destRect);
/// <summary>
/// Draws a bitmap image.
@ -44,7 +44,7 @@ namespace Avalonia.Platform
/// <param name="opacityMask">The opacity mask to draw with.</param>
/// <param name="opacityMaskRect">The destination rect for the opacity mask.</param>
/// <param name="destRect">The rect in the output to draw to.</param>
void DrawBitmap(IRef<IBitmapImpl> source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect);
void DrawBitmap(IBitmapImpl source, IBrush opacityMask, Rect opacityMaskRect, Rect destRect);
/// <summary>
/// Draws a line.
@ -94,7 +94,7 @@ namespace Avalonia.Platform
/// </summary>
/// <param name="foreground">The foreground.</param>
/// <param name="glyphRun">The glyph run.</param>
void DrawGlyphRun(IBrush? foreground, IRef<IGlyphRunImpl> glyphRun);
void DrawGlyphRun(IBrush? foreground, IGlyphRunImpl glyphRun);
/// <summary>
/// Creates a new <see cref="IRenderTargetBitmapImpl"/> that can be used as a render layer

2
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);

12
src/Avalonia.Base/Reactive/Observable.cs

@ -165,6 +165,18 @@ internal static class Observable
});
}
public static IObservable<T> FromEventPattern<T>(Action<EventHandler<T>> addHandler, Action<EventHandler<T>> removeHandler) where T : EventArgs
{
return Create<T>(observer =>
{
var handler = new Action<T>(observer.OnNext);
var converted = new EventHandler<T>((_, args) => handler(args));
addHandler(converted);
return Disposable.Create(() => removeHandler(converted));
});
}
public static IObservable<T> Return<T>(T value)
{
return new ReturnImpl<T>(value);

7
src/Avalonia.Base/Rect.cs

@ -607,5 +607,12 @@ namespace Avalonia
);
}
}
/// <summary>
/// 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
/// </summary>
internal bool IsEmpty() => this == default;
}
}

61
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<IGradientStop> _gradientStops = new();
public IReadOnlyList<IGradientStop> GradientStops => _gradientStops;
public GradientSpreadMethod SpreadMethod { get; private set; }
protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt)
{
base.DeserializeChangesCore(reader, committedAt);
SpreadMethod = reader.Read<GradientSpreadMethod>();
_gradientStops.Clear();
var count = reader.Read<int>();
for (var c = 0; c < count; c++)
_gradientStops.Add(reader.ReadObject<ImmutableGradientStop>());
}
}
partial class ServerCompositionSimpleConicGradientBrush : IConicGradientBrush
{
}
partial class ServerCompositionSimpleLinearGradientBrush : ILinearGradientBrush
{
}
partial class ServerCompositionSimpleRadialGradientBrush : IRadialGradientBrush
{
}
partial class ServerCompositionSimpleSolidColorBrush : ISolidColorBrush
{
}
}

24
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<CompositionRenderDataSceneBrushContent?>();
}
}

37
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<IBitmapImpl>? 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<IRef<IBitmapImpl>>();
}
}

20
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 <see href="https://raw.githubusercontent.com/AvaloniaUI/Avalonia/master/src/Avalonia.Base/Rendering/Composition/License.md">License.md</see>
@ -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<Visual> _dirty = new();
private readonly HashSet<Visual> _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)

14
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 <see href="https://raw.githubusercontent.com/AvaloniaUI/Avalonia/master/src/Avalonia.Base/Rendering/Composition/License.md">License.md</see>
@ -21,12 +18,12 @@ internal class CompositionDrawListVisual : CompositionContainerVisual
public Visual Visual { get; }
private bool _drawListChanged;
private CompositionDrawList? _drawList;
private CompositionRenderData? _drawList;
/// <summary>
/// The list of drawing commands
/// </summary>
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;
}
}

5
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();
}

11
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)
{
}
}

28
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 <see cref="CompositionVisual" /> objects each representing a single node in a Visual tree.
/// </summary>
public abstract class CompositionObject : IDisposable
public abstract class CompositionObject : ICompositorSerializable
{
/// <summary>
/// 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<CompositionProperty, IAnimationInstance> 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
/// </summary>
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));
}
}
}

3
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 <see href="https://raw.githubusercontent.com/AvaloniaUI/Avalonia/master/src/Avalonia.Base/Rendering/Composition/License.md">License.md</see>
@ -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)

2
src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs

@ -11,7 +11,7 @@ namespace Avalonia.Rendering.Composition
/// <summary>
/// Represents the composition output (e. g. a window, embedded control, entire screen)
/// </summary>
public partial class CompositionTarget
internal partial class CompositionTarget
{
partial void OnRootChanged()
{

9
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
{
}

2
src/Avalonia.Base/Rendering/Composition/Compositor.Factories.cs

@ -12,7 +12,7 @@ public partial class Compositor
/// </summary>
/// <param name="surfaces">A factory method to create IRenderTarget to be called from the render thread</param>
/// <returns></returns>
public CompositionTarget CreateCompositionTarget(Func<IEnumerable<object>> surfaces)
internal CompositionTarget CreateCompositionTarget(Func<IEnumerable<object>> surfaces)
{
return new CompositionTarget(this, new ServerCompositionTarget(_server, surfaces, DiagnosticTextRenderer));
}

47
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<object?> _batchObjectPool = new();
private BatchStreamMemoryPool _batchMemoryPool = new();
private List<CompositionObject> _objectsForSerialization = new();
private Queue<ICompositorSerializable> _objectSerializationQueue = new();
private HashSet<ICompositorSerializable> _objectSerializationHashSet = new();
private Queue<Action> _invokeBeforeCommitWrite = new(), _invokeBeforeCommitRead = new();
private HashSet<IDisposable> _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;
/// <summary>
/// Creates a new compositor on a specified render loop that would use a particular GPU
/// </summary>
/// <param name="loop"></param>
/// <param name="gpu"></param>
[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();
}
/// <summary>
/// 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);
}
}

129
src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawList.cs

@ -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 <see href="https://raw.githubusercontent.com/AvaloniaUI/Avalonia/master/src/Avalonia.Base/Rendering/Composition/License.md">License.md</see>
namespace Avalonia.Rendering.Composition.Drawing;
/// <summary>
/// A list of serialized drawing commands
/// </summary>
internal class CompositionDrawList : PooledList<IRef<IDrawOperation>>
{
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;
}
}
/// <summary>
/// An helper class for building <see cref="CompositionDrawList"/>
/// </summary>
internal class CompositionDrawListBuilder
{
private CompositionDrawList? _operations;
private bool _owns;
public void Reset(CompositionDrawList? previousOperations)
{
_operations = previousOperations;
_owns = false;
}
public int Count => _operations?.Count ?? 0;
public CompositionDrawList? DrawOperations => _operations;
void MakeWritable(int atIndex)
{
if(_owns)
return;
_owns = true;
var newOps = new CompositionDrawList(_operations?.Count ?? Math.Max(1, atIndex));
if (_operations != null)
{
for (var c = 0; c < atIndex; c++)
newOps.Add(_operations[c].Clone());
}
_operations = newOps;
}
public void ReplaceDrawOperation(int index, IDrawOperation node)
{
MakeWritable(index);
DrawOperations!.Add(RefCountable.Create(node));
}
public void AddDrawOperation(IDrawOperation node)
{
MakeWritable(Count);
DrawOperations!.Add(RefCountable.Create(node));
}
public void TrimTo(int count)
{
if (count < Count)
_operations!.RemoveRange(count, _operations.Count - count);
}
}

37
src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawListSceneBrushContent.cs

@ -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; }
}

371
src/Avalonia.Base/Rendering/Composition/Drawing/CompositionDrawingContext.cs

@ -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 <see href="https://raw.githubusercontent.com/AvaloniaUI/Avalonia/master/src/Avalonia.Base/Rendering/Composition/License.md">License.md</see>
namespace Avalonia.Rendering.Composition;
/// <summary>
/// An IDrawingContextImpl implementation that builds <see cref="CompositionDrawList"/>
/// </summary>
internal sealed class CompositionDrawingContext : DrawingContext, IDrawingContextWithAcrylicLikeSupport
{
private CompositionDrawListBuilder _builder = new();
private int _drawOperationIndex;
private static ThreadSafeObjectPool<Stack<Matrix>> TransformStackPool { get; } =
ThreadSafeObjectPool<Stack<Matrix>>.Default;
private Stack<Matrix>? _transforms;
private static ThreadSafeObjectPool<Stack<bool>> OpacityMaskPopStackPool { get; } =
ThreadSafeObjectPool<Stack<bool>>.Default;
private Stack<bool>? _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<GeometryNode>();
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<IBitmapImpl> source, double opacity, Rect sourceRect, Rect destRect)
{
var next = NextDrawAs<ImageNode>();
if (next == null ||
!next.Item.Equals(Transform, source, opacity, sourceRect, destRect))
{
Add(new ImageNode(Transform, source, opacity, sourceRect, destRect));
}
else
{
++_drawOperationIndex;
}
}
/// <inheritdoc/>
protected override void DrawLineCore(IPen? pen, Point p1, Point p2)
{
if (pen is null)
{
return;
}
var next = NextDrawAs<LineNode>();
if (next == null || !next.Item.Equals(Transform, pen, p1, p2))
{
Add(new LineNode(Transform, pen, p1, p2));
}
else
{
++_drawOperationIndex;
}
}
/// <inheritdoc/>
protected override void DrawRectangleCore(IBrush? brush, IPen? pen, RoundedRect rect,
BoxShadows boxShadows = default)
{
var next = NextDrawAs<RectangleNode>();
if (next == null || !next.Item.Equals(Transform, brush, pen, rect, boxShadows))
{
Add(new RectangleNode(Transform, ConvertBrush(brush), pen, rect, boxShadows));
}
else
{
++_drawOperationIndex;
}
}
/// <inheritdoc/>
public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect)
{
var next = NextDrawAs<ExperimentalAcrylicNode>();
if (next == null || !next.Item.Equals(Transform, material, rect))
{
Add(new ExperimentalAcrylicNode(Transform, material, rect));
}
else
{
++_drawOperationIndex;
}
}
protected override void DrawEllipseCore(IBrush? brush, IPen? pen, Rect rect)
{
var next = NextDrawAs<EllipseNode>();
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<CustomDrawOperation>();
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<GlyphRunNode>();
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<ClipNode>();
if (next == null || !next.Item.Equals(null))
{
Add(new ClipNode());
}
else
{
++_drawOperationIndex;
}
}
/// <inheritdoc/>
protected override void PopGeometryClipCore()
{
var next = NextDrawAs<GeometryClipNode>();
if (next == null || !next.Item.Equals(null))
{
Add(new GeometryClipNode());
}
else
{
++_drawOperationIndex;
}
}
protected override void PopOpacityCore()
{
var next = NextDrawAs<OpacityNode>();
if (next == null || !next.Item.Equals(null))
{
Add(new OpacityNode());
}
else
{
++_drawOperationIndex;
}
}
protected override void PopOpacityMaskCore()
{
if (!_needsToPopOpacityMask!.Pop())
return;
var next = NextDrawAs<OpacityMaskNode>();
if (next == null || !next.Item.Equals(null, null))
{
Add(new OpacityMaskPopNode());
}
else
{
++_drawOperationIndex;
}
}
protected override void PushClipCore(Rect clip)
{
var next = NextDrawAs<ClipNode>();
if (next == null || !next.Item.Equals(Transform, clip))
{
Add(new ClipNode(Transform, clip));
}
else
{
++_drawOperationIndex;
}
}
protected override void PushClipCore(RoundedRect clip)
{
var next = NextDrawAs<ClipNode>();
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<GeometryClipNode>();
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<OpacityNode>();
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<OpacityMaskNode>();
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>(T node) where T : class, IDrawOperation
{
if (_drawOperationIndex < _builder.Count)
{
_builder.ReplaceDrawOperation(_drawOperationIndex, node);
}
else
{
_builder.AddDrawOperation(node);
}
++_drawOperationIndex;
}
private IRef<T>? NextDrawAs<T>() where T : class, IDrawOperation
{
return _drawOperationIndex < _builder.Count
? _builder.DrawOperations![_drawOperationIndex] as IRef<T>
: null;
}
private IImmutableBrush? ConvertBrush(IBrush? brush)
{
if (brush is IMutableBrush mutable)
return mutable.ToImmutable();
if (brush is ISceneBrush sceneBrush)
return sceneBrush.CreateContent();
return (IImmutableBrush?)brush;
}
}

68
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<ICompositionRenderResource> _resources;
private PooledInlineList<IRenderDataItem> _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;
}
}

47
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; }
}

123
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<T> 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<T> where T : SimpleServerObject
{
private InlineDictionary<Compositor, CompositorRefCountableResource<T>> _dictionary;
public bool IsAttached => _dictionary.HasEntries;
public bool CreateOrAddRef(Compositor compositor, ICompositorSerializable owner, out T resource, Func<Compositor, T> factory)
{
if (_dictionary.TryGetValue(compositor, out var handle))
{
handle.AddRef();
resource = handle.Value;
return false;
}
resource = factory(compositor);
_dictionary.Add(compositor, new CompositorRefCountableResource<T>(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);
}
}

12
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<T> : ICompositionRenderResource where T : class
{
T GetForCompositor(Compositor c);
}

81
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<IRenderDataItem>? _items;
private readonly ThreadSafeObjectPool<List<IRenderDataItem>> _pool;
public ImmediateRenderDataSceneBrushContent(ITileBrush brush, List<IRenderDataItem> items, Rect? rect,
bool useScalableRasterization, ThreadSafeObjectPool<List<IRenderDataItem>> 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; }
}

28
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<IBitmapImpl>? 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;
}
}

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

29
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;
}

35
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<IGlyphRunImpl>? 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;
}
}

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

214
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<Matrix>? _stack;
private static readonly ThreadSafeObjectPool<Stack<Matrix>> s_matrixStackPool = new();
public RenderDataNodeRenderContext(IDrawingContextImpl context)
{
Context = context;
}
public IDrawingContextImpl Context { get; }
public Stack<Matrix> MatrixStack
{
get => _stack ??= s_matrixStackPool.Get();
}
public void Dispose()
{
if (_stack != null)
{
_stack.Clear();
s_matrixStackPool.ReturnAndSetNull(ref _stack);
}
}
}
interface IRenderDataItem
{
/// <summary>
/// Renders the node to a drawing context.
/// </summary>
/// <param name="context">The drawing context.</param>
void Invoke(ref RenderDataNodeRenderContext context);
/// <summary>
/// Gets the bounds of the visible content in the node in global coordinates.
/// </summary>
Rect? Bounds { get; }
/// <summary>
/// Hit test the geometry in this node.
/// </summary>
/// <param name="p">The point in global coordinates.</param>
/// <returns>True if the point hits the node's geometry; otherwise false.</returns>
/// <remarks>
/// This method does not recurse to childs, if you want
/// to hit test children they must be hit tested manually.
/// </remarks>
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<IRenderDataItem> 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);
}

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

25
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();
}

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

349
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<object>? _resourcesHashSet;
private static readonly ThreadSafeObjectPool<HashSet<object>> s_hashSetPool = new();
private CompositionRenderData RenderData
{
get
{
Debug.Assert(_compositor != null);
return _renderData ??= new(_compositor);
}
}
struct ParentStackItem
{
public RenderDataPushNode? Node;
public List<IRenderDataItem> Items;
}
private List<IRenderDataItem>? _currentItemList;
private static readonly ThreadSafeObjectPool<List<IRenderDataItem>> s_listPool = new();
private Stack<ParentStackItem>? _parentNodeStack;
private static readonly ThreadSafeObjectPool<Stack<ParentStackItem>> 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<T>() 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<RenderDataClipNode>();
protected override void PopGeometryClipCore() => Pop<RenderDataGeometryClipNode>();
protected override void PopOpacityCore() => Pop<RenderDataOpacityNode>();
protected override void PopOpacityMaskCore() => Pop<RenderDataOpacityMaskNode>();
protected override void PopTransformCore() => Pop<RenderDataPushMatrixNode>();
internal override void DrawBitmap(IRef<IBitmapImpl>? 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<IRenderDataItem>();
}
}
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);
}
}

136
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<IRenderDataItem> _items;
private PooledInlineList<IServerRenderResource> _referencedResources;
private Rect? _bounds;
private bool _boundsValid;
private static readonly ThreadSafeObjectPool<Collector> s_resourceHashSetPool = new();
public ServerCompositionRenderData(ServerCompositor compositor) : base(compositor)
{
}
class Collector : IRenderDataServerResourcesCollector
{
public readonly HashSet<IServerRenderResource> 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<int>();
_items.EnsureCapacity(count);
for (var c = 0; c < count; c++)
_items.Add(reader.ReadObject<IRenderDataItem>());
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();
}
}

12
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;
}

56
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<IBrush> 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<IPen> 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<ITransform> resource)
resource.GetForCompositor(compositor);
ThrowNotCompatible(transform);
return null;
}
}

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

2
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;
}

9
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<IBitmapImpl> 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<IBitmapImpl> 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<IGlyphRunImpl> 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)

24
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<byte>() == 1)
{
_renderCommands?.Dispose();
_renderCommands = reader.ReadObject<CompositionDrawList?>();
_contentBounds = null;
_renderCommands = reader.ReadObject<ServerCompositionRenderData?>();
}
base.DeserializeChangesCore(reader, committedAt);
}

26
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)
{
}
}

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

2
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));

22
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<IServerRenderResource> _renderResourcesInvalidationQueue = new();
private HashSet<IServerRenderResource> _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);
}
}

23
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
/// </summary>
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<int>();
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
{

11
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<T>.Enumerator GetEnumerator() => List.GetEnumerator();
public ServerList(ServerCompositor compositor) : base(compositor)

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

@ -15,12 +15,8 @@ namespace Avalonia.Rendering.Composition.Server
/// Server-side <see cref="CompositionObject" /> counterpart.
/// Is responsible for animation activation and invalidation
/// </summary>
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<CompositionProperty, ServerObjectSubscriptionStore> _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<T>(CompositionProperty prop, out T field, T value)
protected new void SetValue<T>(CompositionProperty prop, ref T field, T value)
{
field = value;
InvalidateSubscriptions(prop);
}
protected T GetValue<T>(CompositionProperty prop, ref T field)
protected new T GetValue<T>(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<byte>() == 1)
disp.Dispose();
}
public void DeserializeChanges(BatchStreamReader reader, Batch batch)
{
DeserializeChangesCore(reader, batch.CommittedAt);
ValuesInvalidated();
ItselfLastChangedBy = batch.SequenceId;
}
}
}

123
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<IServerRenderResource> _observers;
public SimpleServerRenderResource(ServerCompositor compositor) : base(compositor)
{
}
protected new void SetValue<T>(CompositionProperty prop, ref T field, T value) => SetValue(ref field, value);
protected void SetValue<T>(ref T field, T value)
{
if (EqualityComparer<T>.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<T>(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);
}
}

34
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<T>(CompositionProperty prop, ref T field, T value) => field = value;
protected T GetValue<T>(CompositionProperty prop, ref T field) => field;
}

27
src/Avalonia.Base/Rendering/SceneGraph/BrushDrawOperation.cs

@ -1,27 +0,0 @@
using System;
using System.Collections.Generic;
using Avalonia.Media;
using Avalonia.VisualTree;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// Base class for draw operations that can use a brush.
/// </summary>
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();
}
}
}

87
src/Avalonia.Base/Rendering/SceneGraph/ClipNode.cs

@ -1,87 +0,0 @@
using Avalonia.Platform;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// A node in the scene graph which represents a clip push or pop.
/// </summary>
internal class ClipNode : IDrawOperationWithTransform
{
/// <summary>
/// Initializes a new instance of the <see cref="ClipNode"/> class that represents a
/// clip push.
/// </summary>
/// <param name="transform">The current transform.</param>
/// <param name="clip">The clip to push.</param>
public ClipNode(Matrix transform, Rect clip)
{
Transform = transform;
Clip = clip;
}
/// <summary>
/// Initializes a new instance of the <see cref="ClipNode"/> class that represents a
/// clip push.
/// </summary>
/// <param name="transform">The current transform.</param>
/// <param name="clip">The clip to push.</param>
public ClipNode(Matrix transform, RoundedRect clip)
{
Transform = transform;
Clip = clip;
}
/// <summary>
/// Initializes a new instance of the <see cref="ClipNode"/> class that represents a
/// clip pop.
/// </summary>
public ClipNode()
{
}
/// <inheritdoc/>
public Rect Bounds => default;
/// <summary>
/// Gets the clip to be pushed or null if the operation represents a pop.
/// </summary>
public RoundedRect? Clip { get; }
/// <summary>
/// Gets the transform with which the node will be drawn.
/// </summary>
public Matrix Transform { get; }
/// <inheritdoc/>
public bool HitTest(Point p) => false;
/// <summary>
/// Determines if this draw operation equals another.
/// </summary>
/// <param name="transform">The transform of the other draw operation.</param>
/// <param name="clip">The clip of the other draw operation.</param>
/// <returns>True if the draw operations are the same, otherwise false.</returns>
/// <remarks>
/// The properties of the other draw operation are passed in as arguments to prevent
/// allocation of a not-yet-constructed draw operation object.
/// </remarks>
public bool Equals(Matrix transform, RoundedRect? clip) => Transform == transform && Clip == clip;
/// <inheritdoc/>
public void Render(IDrawingContextImpl context)
{
if (Clip.HasValue)
{
context.PushClip(Clip.Value);
}
else
{
context.PopClip();
}
}
public void Dispose()
{
}
}
}

31
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<ICustomDrawOperation>, IDisposable
{
/// <summary>

56
src/Avalonia.Base/Rendering/SceneGraph/DrawOperation.cs

@ -1,56 +0,0 @@
using System;
using Avalonia.Media;
using Avalonia.Platform;
namespace Avalonia.Rendering.SceneGraph
{
/// <summary>
/// Base class for draw operations that have bounds.
/// </summary>
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);
}
}

97
src/Avalonia.Base/Rendering/SceneGraph/EllipseNode.cs

@ -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
{
/// <summary>
/// A node in the scene graph which represents an ellipse draw.
/// </summary>
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;
}
/// <summary>
/// Gets the stroke pen.
/// </summary>
public ImmutablePen? Pen { get; }
/// <summary>
/// Gets the rect of the ellipse to draw.
/// </summary>
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();
}
}
}

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

Loading…
Cancel
Save