Browse Source
* Composition-aware geometries * Propagate resource changes in DrawingBrush * Change detection in Drawing * Make IRenderDataGeometry a private APIpull/21710/head
committed by
GitHub
27 changed files with 671 additions and 38 deletions
@ -0,0 +1,20 @@ |
|||||
|
using Avalonia.Metadata; |
||||
|
using Avalonia.Platform; |
||||
|
|
||||
|
namespace Avalonia.Rendering.Composition.Drawing; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Common abstraction for a geometry to render.
|
||||
|
/// It is implemented by both the immutable platform <see cref="IGeometryImpl"/> (which returns itself)
|
||||
|
/// and the compositor-aware <see cref="Server.ServerCompositionSimpleGeometry"/> server resource
|
||||
|
/// (which returns its current, mutable backing geometry). This lets the render data stay strongly typed
|
||||
|
/// while still resolving to the concrete <see cref="IGeometryImpl"/> that platform drawing contexts require.
|
||||
|
/// </summary>
|
||||
|
[PrivateApi] |
||||
|
public interface IRenderDataGeometry |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The underlying immutable platform geometry, or <c>null</c> when there's nothing to draw.
|
||||
|
/// </summary>
|
||||
|
IGeometryImpl? GeometryImpl { get; } |
||||
|
} |
||||
@ -0,0 +1,5 @@ |
|||||
|
using Avalonia.Rendering.Composition.Drawing; |
||||
|
|
||||
|
namespace Avalonia.Rendering.Composition.Server; |
||||
|
|
||||
|
internal partial class ServerCompositionSimpleGeometry : IRenderDataGeometry; |
||||
@ -0,0 +1,84 @@ |
|||||
|
using Avalonia.Media; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Base.UnitTests.Media; |
||||
|
|
||||
|
public class GeometryRenderResourceTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Changing_Geometry_Property_Raises_Invalidated() |
||||
|
{ |
||||
|
var target = new EllipseGeometry(new Rect(0, 0, 10, 10)); |
||||
|
|
||||
|
RenderResourceTestHelper.AssertResourceInvalidation( |
||||
|
target, |
||||
|
() => target.Rect = new Rect(0, 0, 20, 20)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Changing_Transform_Raises_Invalidated() |
||||
|
{ |
||||
|
var target = new EllipseGeometry(new Rect(0, 0, 10, 10)); |
||||
|
|
||||
|
RenderResourceTestHelper.AssertResourceInvalidation( |
||||
|
target, |
||||
|
() => target.Transform = new TranslateTransform(5, 5)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Changing_Transform_Value_Raises_Invalidated() |
||||
|
{ |
||||
|
var transform = new TranslateTransform(5, 5); |
||||
|
var target = new EllipseGeometry(new Rect(0, 0, 10, 10)) { Transform = transform }; |
||||
|
|
||||
|
RenderResourceTestHelper.AssertResourceInvalidation( |
||||
|
target, |
||||
|
() => transform.X = 10); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Adding_Child_To_GeometryGroup_Raises_Invalidated() |
||||
|
{ |
||||
|
var target = new GeometryGroup(); |
||||
|
|
||||
|
RenderResourceTestHelper.AssertResourceInvalidation( |
||||
|
target, |
||||
|
() => target.Children.Add(new EllipseGeometry(new Rect(0, 0, 10, 10)))); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Changing_Child_Of_GeometryGroup_Raises_Invalidated() |
||||
|
{ |
||||
|
var child = new EllipseGeometry(new Rect(0, 0, 10, 10)); |
||||
|
var target = new GeometryGroup(); |
||||
|
target.Children.Add(child); |
||||
|
|
||||
|
RenderResourceTestHelper.AssertResourceInvalidation( |
||||
|
target, |
||||
|
() => child.Rect = new Rect(0, 0, 20, 20)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Changing_Geometry1_Of_CombinedGeometry_Raises_Invalidated() |
||||
|
{ |
||||
|
var geometry1 = new EllipseGeometry(new Rect(0, 0, 10, 10)); |
||||
|
var geometry2 = new RectangleGeometry(new Rect(5, 5, 10, 10)); |
||||
|
var target = new CombinedGeometry(GeometryCombineMode.Union, geometry1, geometry2); |
||||
|
|
||||
|
RenderResourceTestHelper.AssertResourceInvalidation( |
||||
|
target, |
||||
|
() => geometry1.Rect = new Rect(0, 0, 20, 20)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Changing_CombineMode_Of_CombinedGeometry_Raises_Invalidated() |
||||
|
{ |
||||
|
var geometry1 = new EllipseGeometry(new Rect(0, 0, 10, 10)); |
||||
|
var geometry2 = new RectangleGeometry(new Rect(5, 5, 10, 10)); |
||||
|
var target = new CombinedGeometry(GeometryCombineMode.Union, geometry1, geometry2); |
||||
|
|
||||
|
RenderResourceTestHelper.AssertResourceInvalidation( |
||||
|
target, |
||||
|
() => target.GeometryCombineMode = GeometryCombineMode.Intersect); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,56 @@ |
|||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Media; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Base.UnitTests.Rendering; |
||||
|
|
||||
|
public class DrawingBrushPropagationTests : CompositorTestsBase |
||||
|
{ |
||||
|
private static Border CreateBorder(DrawingBrush brush) => new() |
||||
|
{ |
||||
|
Background = brush, |
||||
|
Width = 20, |
||||
|
Height = 10, |
||||
|
[Canvas.LeftProperty] = 30, |
||||
|
[Canvas.TopProperty] = 50 |
||||
|
}; |
||||
|
|
||||
|
[Fact] |
||||
|
public void Mutating_Geometry_Inside_DrawingBrush_Invalidates_Consumer() |
||||
|
{ |
||||
|
using var services = new CompositorCanvas(); |
||||
|
|
||||
|
var geometry = new RectangleGeometry(new Rect(0, 0, 20, 10)); |
||||
|
var brush = new DrawingBrush(new GeometryDrawing { Brush = Brushes.Red, Geometry = geometry }); |
||||
|
services.Canvas.Children.Add(CreateBorder(brush)); |
||||
|
services.RunJobs(); |
||||
|
services.Events.Rects.Clear(); |
||||
|
|
||||
|
geometry.Rect = new Rect(0, 0, 30, 15); |
||||
|
|
||||
|
services.AssertRects(new Rect(30, 50, 20, 10)); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Replacing_Drawing_Invalidates_Consumer() |
||||
|
{ |
||||
|
using var services = new CompositorCanvas(); |
||||
|
|
||||
|
var brush = new DrawingBrush(new GeometryDrawing |
||||
|
{ |
||||
|
Brush = Brushes.Red, |
||||
|
Geometry = new RectangleGeometry(new Rect(0, 0, 20, 10)) |
||||
|
}); |
||||
|
services.Canvas.Children.Add(CreateBorder(brush)); |
||||
|
services.RunJobs(); |
||||
|
services.Events.Rects.Clear(); |
||||
|
|
||||
|
brush.Drawing = new GeometryDrawing |
||||
|
{ |
||||
|
Brush = Brushes.Blue, |
||||
|
Geometry = new RectangleGeometry(new Rect(0, 0, 20, 10)) |
||||
|
}; |
||||
|
|
||||
|
services.AssertRects(new Rect(30, 50, 20, 10)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,35 @@ |
|||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Media; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Base.UnitTests.Rendering; |
||||
|
|
||||
|
public class DrawingImagePropagationTests : CompositorTestsBase |
||||
|
{ |
||||
|
private static Image CreateImage(DrawingImage image) => new() |
||||
|
{ |
||||
|
Source = image, |
||||
|
Width = 20, |
||||
|
Height = 10, |
||||
|
[Canvas.LeftProperty] = 30, |
||||
|
[Canvas.TopProperty] = 50 |
||||
|
}; |
||||
|
|
||||
|
[Fact] |
||||
|
public void Mutating_Geometry_Inside_DrawingImage_Invalidates_Consumer() |
||||
|
{ |
||||
|
using var services = new CompositorCanvas(); |
||||
|
|
||||
|
var geometry = new RectangleGeometry(new Rect(0, 0, 20, 10)); |
||||
|
var image = new DrawingImage(new GeometryDrawing { Brush = Brushes.Red, Geometry = geometry }); |
||||
|
services.Canvas.Children.Add(CreateImage(image)); |
||||
|
services.RunJobs(); |
||||
|
services.Events.Rects.Clear(); |
||||
|
|
||||
|
geometry.Rect = new Rect(0, 0, 30, 15); |
||||
|
|
||||
|
services.AssertRects( |
||||
|
new Rect(30, 50, 20, 10), |
||||
|
new Rect(30, 50, 30, 15)); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,114 @@ |
|||||
|
#if AVALONIA_SKIA
|
||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Platform.Surfaces; |
||||
|
using Avalonia.Rendering; |
||||
|
using Avalonia.Rendering.Composition; |
||||
|
using Avalonia.Threading; |
||||
|
using Avalonia.UnitTests; |
||||
|
using SkiaSharp; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Skia.RenderTests; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Verifies that replacing a resource inside a <see cref="Drawing"/> is reflected in what a
|
||||
|
/// consuming control renders. The initial frame is intentionally ignored (covered elsewhere); only
|
||||
|
/// the frame produced _after_ the change is compared against the reference image.
|
||||
|
/// </summary>
|
||||
|
public class DrawingContentTests() |
||||
|
: TestBase(@"Media\DrawingContent") |
||||
|
{ |
||||
|
private sealed class FuncFramebufferSurface(Func<IFramebufferRenderTarget> cb) : IFramebufferPlatformSurface |
||||
|
{ |
||||
|
public IFramebufferRenderTarget CreateFramebufferRenderTarget() => cb(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void DrawingBrush_Reflects_Replaced_Inner_Brush() |
||||
|
{ |
||||
|
var drawing = new GeometryDrawing |
||||
|
{ |
||||
|
Brush = Brushes.Blue, |
||||
|
Geometry = new RectangleGeometry(new Rect(0, 0, 100, 100)) |
||||
|
}; |
||||
|
|
||||
|
var target = new Border |
||||
|
{ |
||||
|
Width = 100, |
||||
|
Height = 100, |
||||
|
Background = new DrawingBrush(drawing) { Stretch = Stretch.Fill } |
||||
|
}; |
||||
|
|
||||
|
RenderChange(target, () => drawing.Brush = Brushes.Red); |
||||
|
CompareImages(skipImmediate: true); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void DrawingImage_Reflects_Replaced_Inner_Brush() |
||||
|
{ |
||||
|
var drawing = new GeometryDrawing |
||||
|
{ |
||||
|
Brush = Brushes.Blue, |
||||
|
Geometry = new RectangleGeometry(new Rect(0, 0, 100, 100)) |
||||
|
}; |
||||
|
|
||||
|
var target = new Image |
||||
|
{ |
||||
|
Width = 100, |
||||
|
Height = 100, |
||||
|
Stretch = Stretch.Fill, |
||||
|
Source = new DrawingImage(drawing) |
||||
|
}; |
||||
|
|
||||
|
RenderChange(target, () => drawing.Brush = Brushes.Red); |
||||
|
CompareImages(skipImmediate: true); |
||||
|
} |
||||
|
|
||||
|
// Renders the target once, applies the change, renders again and writes the second frame to the
|
||||
|
// standard composited output path so it can be compared against the committed reference image.
|
||||
|
private void RenderChange(Control target, Action change, [CallerMemberName] string testName = "") |
||||
|
{ |
||||
|
var timer = new ManualRenderTimer(); |
||||
|
var compositor = new Compositor( |
||||
|
RenderLoop.FromTimer(timer), |
||||
|
null, |
||||
|
true, |
||||
|
new DispatcherCompositorScheduler(), true, Dispatcher.UIThread); |
||||
|
|
||||
|
var root = new TestRenderRoot(1, null!); |
||||
|
using var frameBuffer = new SKBitmap(100, 100, SKColorType.Rgba8888, SKAlphaType.Premul); |
||||
|
|
||||
|
var renderTarget = new FuncFramebufferRenderTarget(() => new LockedFramebuffer( |
||||
|
frameBuffer.GetAddress(0, 0), |
||||
|
new PixelSize(frameBuffer.Width, frameBuffer.Height), |
||||
|
frameBuffer.RowBytes, |
||||
|
new Vector(96, 96), |
||||
|
PixelFormat.Rgba8888, |
||||
|
AlphaFormat.Premul, |
||||
|
null)); |
||||
|
|
||||
|
using (var renderer = new CompositingRenderer(root, compositor, () => [new FuncFramebufferSurface(() => renderTarget)])) |
||||
|
{ |
||||
|
root.Initialize(renderer, target); |
||||
|
renderer.Start(); |
||||
|
Dispatcher.UIThread.RunJobs(); |
||||
|
timer.TriggerTick(); |
||||
|
|
||||
|
change(); |
||||
|
Dispatcher.UIThread.RunJobs(); |
||||
|
timer.TriggerTick(); |
||||
|
} |
||||
|
|
||||
|
Directory.CreateDirectory(OutputPath); |
||||
|
var path = Path.Combine(OutputPath, testName + ".composited.out.png"); |
||||
|
using var data = frameBuffer.Encode(SKEncodedImageFormat.Png, 100); |
||||
|
using var file = File.Create(path); |
||||
|
data.SaveTo(file); |
||||
|
} |
||||
|
} |
||||
|
#endif
|
||||
|
After Width: | Height: | Size: 329 B |
|
After Width: | Height: | Size: 329 B |
Loading…
Reference in new issue