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