Browse Source

Reworked the way we are clipping dirty rects with CompositionTarget (#14806)

* Reworked dirty rect tracking to support regions

* Fixed FPS counter
pull/16303/head
Nikita Tsukanov 3 years ago
committed by GitHub
parent
commit
4dbb165a7b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 5
      samples/ControlCatalog.NetCore/Program.cs
  2. 4
      samples/ControlCatalog/Pages/CompositionPage.axaml
  3. 64
      samples/ControlCatalog/Pages/CompositionPage.axaml.cs
  4. 2
      src/Avalonia.Base/Media/Imaging/RenderTargetBitmap.cs
  5. 11
      src/Avalonia.Base/PixelSize.cs
  6. 33
      src/Avalonia.Base/Platform/IDrawingContextImpl.cs
  7. 3
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  8. 17
      src/Avalonia.Base/Platform/IPlatformRenderInterfaceRegion.cs
  9. 3
      src/Avalonia.Base/Platform/IRenderTarget.cs
  10. 3
      src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs
  11. 32
      src/Avalonia.Base/Rendering/Composition/CompositionCustomVisual.cs
  12. 47
      src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs
  13. 16
      src/Avalonia.Base/Rendering/Composition/CompositionOptions.cs
  14. 1
      src/Avalonia.Base/Rendering/Composition/CompositionTarget.cs
  15. 2
      src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs
  16. 103
      src/Avalonia.Base/Rendering/Composition/Server/DirtyRectTracker.cs
  17. 11
      src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs
  18. 13
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs
  19. 6
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs
  20. 5
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs
  21. 3
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSolidColorVisual.cs
  22. 3
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs
  23. 35
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.DirtyRects.cs
  24. 121
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  25. 21
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  26. 1
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs
  27. 19
      src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs
  28. 2
      src/Avalonia.Base/composition-schema.xml
  29. 5
      src/Avalonia.Controls/BorderVisual.cs
  30. 2
      src/Avalonia.X11/X11CursorFactory.cs
  31. 2
      src/Avalonia.X11/X11IconLoader.cs
  32. 26
      src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs
  33. 95
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  34. 5
      src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs
  35. 3
      src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs
  36. 1
      src/Skia/Avalonia.Skia/PictureRenderTarget.cs
  37. 3
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  38. 3
      src/Skia/Avalonia.Skia/RenderTargetBitmapImpl.cs
  39. 53
      src/Skia/Avalonia.Skia/SkiaRegionImpl.cs
  40. 10
      src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
  41. 3
      src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs
  42. 3
      src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs
  43. 4
      src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs
  44. 8
      src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs
  45. 58
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  46. 2
      src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs
  47. 7
      src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs
  48. 8
      src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs
  49. 4
      src/Windows/Avalonia.Direct2D1/RenderTarget.cs
  50. 4
      src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs
  51. 2
      tests/Avalonia.RenderTests/Media/BitmapTests.cs
  52. 6
      tests/Avalonia.UnitTests/TestRoot.cs

5
samples/ControlCatalog.NetCore/Program.cs

@ -10,6 +10,7 @@ using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Fonts.Inter; using Avalonia.Fonts.Inter;
using Avalonia.Headless; using Avalonia.Headless;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Rendering.Composition;
using Avalonia.Threading; using Avalonia.Threading;
using ControlCatalog.Pages; using ControlCatalog.Pages;
@ -130,6 +131,10 @@ namespace ControlCatalog.NetCore
UseDBusMenu = true, UseDBusMenu = true,
EnableIme = true EnableIme = true
}) })
.With(new CompositionOptions()
{
UseRegionDirtyRectClipping = true
})
.UseSkia() .UseSkia()
.WithInterFont() .WithInterFont()
.AfterSetup(builder => .AfterSetup(builder =>

4
samples/ControlCatalog/Pages/CompositionPage.axaml

@ -50,6 +50,10 @@
<Button Margin="10" Click="ButtonThreadSleep">Thread.Sleep(10000);</Button> <Button Margin="10" Click="ButtonThreadSleep">Thread.Sleep(10000);</Button>
<Button Margin="10" Click="ButtonStartCustomVisual">Start</Button> <Button Margin="10" Click="ButtonStartCustomVisual">Start</Button>
<Button Margin="10" Click="ButtonStopCustomVisual">Stop</Button> <Button Margin="10" Click="ButtonStopCustomVisual">Stop</Button>
<CheckBox Margin="10"
x:Name="PreciseDirtyRectsCheckboxCustomVisual"
IsCheckedChanged="PreciseDirtyRectsCheckboxCustomVisualChanged"
>Precise dirty rects</CheckBox>
</StackPanel> </StackPanel>
<Control x:Name="CustomVisualHost" /> <Control x:Name="CustomVisualHost" />
</DockPanel> </DockPanel>

64
samples/ControlCatalog/Pages/CompositionPage.axaml.cs

@ -24,7 +24,7 @@ public partial class CompositionPage : UserControl
public CompositionPage() public CompositionPage()
{ {
AvaloniaXamlLoader.Load(this); InitializeComponent();
AttachAnimatedSolidVisual(this.FindControl<Control>("SolidVisualHost")!); AttachAnimatedSolidVisual(this.FindControl<Control>("SolidVisualHost")!);
AttachCustomVisual(this.FindControl<Control>("CustomVisualHost")!); AttachCustomVisual(this.FindControl<Control>("CustomVisualHost")!);
} }
@ -206,6 +206,7 @@ public partial class CompositionPage : UserControl
_customVisual = compositor.CreateCustomVisual(new CustomVisualHandler()); _customVisual = compositor.CreateCustomVisual(new CustomVisualHandler());
ElementComposition.SetElementChildVisual(v, _customVisual); ElementComposition.SetElementChildVisual(v, _customVisual);
_customVisual.SendHandlerMessage(CustomVisualHandler.StartMessage); _customVisual.SendHandlerMessage(CustomVisualHandler.StartMessage);
PreciseDirtyRectsCheckboxCustomVisualChanged(this, new());
Update(); Update();
}; };
@ -221,10 +222,16 @@ public partial class CompositionPage : UserControl
private TimeSpan _animationElapsed; private TimeSpan _animationElapsed;
private TimeSpan? _lastServerTime; private TimeSpan? _lastServerTime;
private bool _running; private bool _running;
private bool _preciseDirtyRects;
public static readonly object StopMessage = new(), StartMessage = new(); public static readonly object StopMessage = new(),
StartMessage = new(),
public override void OnRender(ImmediateDrawingContext drawingContext) UsePreciseDirtyRects = new(),
UseNonPreciseDirtyRects = new();
private List<(Point center, double size, ImmutableSolidColorBrush brush)> _ellipses = new();
void UpdateRects()
{ {
if (_running) if (_running)
{ {
@ -232,6 +239,8 @@ public partial class CompositionPage : UserControl
_lastServerTime = CompositionNow; _lastServerTime = CompositionNow;
} }
_ellipses.Clear();
const int cnt = 20; const int cnt = 20;
var maxPointSizeX = EffectiveSize.X / (cnt * 1.6); var maxPointSizeX = EffectiveSize.X / (cnt * 1.6);
var maxPointSizeY = EffectiveSize.Y / 4; var maxPointSizeY = EffectiveSize.Y / 4;
@ -250,16 +259,22 @@ public partial class CompositionPage : UserControl
var posY = (EffectiveSize.Y - pointSize) * (1 + Math.Sin(stage * 3.14 * 3 + sinOffset)) / 2 + pointSize / 2; var posY = (EffectiveSize.Y - pointSize) * (1 + Math.Sin(stage * 3.14 * 3 + sinOffset)) / 2 + pointSize / 2;
var opacity = Math.Sin(stage * 3.14); var opacity = Math.Sin(stage * 3.14);
_ellipses.Add((new Point(posX, posY), pointSize / 2, new ImmutableSolidColorBrush(Color.FromArgb(
drawingContext.DrawEllipse(new ImmutableSolidColorBrush(Color.FromArgb( 255,
255, (byte)(255 - 255 * colorStage),
(byte)(255 - 255 * colorStage), (byte)(255 * Math.Abs(0.5 - colorStage) * 2),
(byte)(255 * Math.Abs(0.5 - colorStage) * 2), (byte)(255 * colorStage)
(byte)(255 * colorStage) ), opacity)));
), opacity), null,
new Point(posX, posY), pointSize / 2, pointSize / 2);
} }
}
public override void OnRender(ImmediateDrawingContext drawingContext)
{
if (_ellipses.Count == 0)
UpdateRects();
foreach(var e in _ellipses)
drawingContext.DrawEllipse(e.brush, null, e.center, e.size, e.size);
} }
public override void OnMessage(object message) public override void OnMessage(object message)
@ -272,13 +287,29 @@ public partial class CompositionPage : UserControl
} }
else if (message == StopMessage) else if (message == StopMessage)
_running = false; _running = false;
else if (message == UsePreciseDirtyRects)
_preciseDirtyRects = true;
else if (message == UseNonPreciseDirtyRects)
_preciseDirtyRects = false;
} }
void InvalidateCurrentEllipseRects()
{
foreach (var e in _ellipses)
Invalidate(new Rect(e.center.X - e.size, e.center.Y - e.size, e.size * 2, e.size * 2));
}
public override void OnAnimationFrameUpdate() public override void OnAnimationFrameUpdate()
{ {
if (_running) if (_running)
{ {
Invalidate(); if (_preciseDirtyRects)
InvalidateCurrentEllipseRects();
else
Invalidate();
UpdateRects();
if(_preciseDirtyRects)
InvalidateCurrentEllipseRects();
RegisterForNextAnimationFrameUpdate(); RegisterForNextAnimationFrameUpdate();
} }
} }
@ -298,6 +329,13 @@ public partial class CompositionPage : UserControl
{ {
_customVisual?.SendHandlerMessage(CustomVisualHandler.StopMessage); _customVisual?.SendHandlerMessage(CustomVisualHandler.StopMessage);
} }
private void PreciseDirtyRectsCheckboxCustomVisualChanged(object sender, RoutedEventArgs e)
{
_customVisual?.SendHandlerMessage(PreciseDirtyRectsCheckboxCustomVisual?.IsChecked == true
? CustomVisualHandler.UsePreciseDirtyRects
: CustomVisualHandler.UseNonPreciseDirtyRects);
}
} }
public class CompositionPageColorItem public class CompositionPageColorItem

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

@ -77,7 +77,7 @@ namespace Avalonia.Media.Imaging
/// <returns>The drawing context.</returns> /// <returns>The drawing context.</returns>
public DrawingContext CreateDrawingContext(bool clear) public DrawingContext CreateDrawingContext(bool clear)
{ {
var platform = PlatformImpl.Item.CreateDrawingContext(); var platform = PlatformImpl.Item.CreateDrawingContext(true);
if(clear) if(clear)
platform.Clear(Colors.Transparent); platform.Clear(Colors.Transparent);
return new PlatformDrawingContext(platform); return new PlatformDrawingContext(platform);

11
src/Avalonia.Base/PixelSize.cs

@ -166,6 +166,17 @@ namespace Avalonia
public static PixelSize FromSize(Size size, double scale) => new PixelSize( public static PixelSize FromSize(Size size, double scale) => new PixelSize(
(int)Math.Ceiling(size.Width * scale), (int)Math.Ceiling(size.Width * scale),
(int)Math.Ceiling(size.Height * scale)); (int)Math.Ceiling(size.Height * scale));
/// <summary>
/// A reversible variant of <see cref="FromSize(Size, double)"/> that uses Round instead of Ceiling to make it reversible from ToSize
/// </summary>
/// <param name="size">The size.</param>
/// <param name="scale">The scaling factor.</param>
/// <returns>The device-independent size.</returns>
internal static PixelSize FromSizeRounded(Size size, double scale) => new PixelSize(
(int)Math.Round(size.Width * scale),
(int)Math.Round(size.Height * scale));
/// <summary> /// <summary>
/// Converts a <see cref="Size"/> to device pixels using the specified scaling factor. /// Converts a <see cref="Size"/> to device pixels using the specified scaling factor.

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

@ -3,6 +3,7 @@ using Avalonia.Media;
using Avalonia.Utilities; using Avalonia.Utilities;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Media.Immutable;
namespace Avalonia.Platform namespace Avalonia.Platform
{ {
@ -75,6 +76,18 @@ namespace Avalonia.Platform
/// </remarks> /// </remarks>
void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect, void DrawRectangle(IBrush? brush, IPen? pen, RoundedRect rect,
BoxShadows boxShadows = default); BoxShadows boxShadows = default);
/// <summary>
/// Draws the specified region with the specified Brush and Pen.
/// </summary>
/// <param name="brush">The brush used to fill the rectangle, or <c>null</c> for no fill.</param>
/// <param name="pen">The pen used to stroke the rectangle, or <c>null</c> for no stroke.</param>
/// <param name="region">The region to draw.</param>
/// <remarks>
/// The brush and the pen can both be null. If the brush is null, then no fill is performed.
/// If the pen is null, then no stoke is performed. If both the pen and the brush are null, then the drawing is not visible.
/// </remarks>
void DrawRegion(IBrush? brush, IPen? pen, IPlatformRenderInterfaceRegion region);
/// <summary> /// <summary>
/// Draws an ellipse with the specified Brush and Pen. /// Draws an ellipse with the specified Brush and Pen.
@ -108,7 +121,7 @@ namespace Avalonia.Platform
/// has to do a format conversion each time a standard render target bitmap is rendered, /// has to do a format conversion each time a standard render target bitmap is rendered,
/// but a layer created via this method has no such overhead. /// but a layer created via this method has no such overhead.
/// </remarks> /// </remarks>
IDrawingContextLayerImpl CreateLayer(Size size); IDrawingContextLayerImpl CreateLayer(PixelSize size);
/// <summary> /// <summary>
/// Pushes a clip rectangle. /// Pushes a clip rectangle.
@ -121,12 +134,28 @@ namespace Avalonia.Platform
/// </summary> /// </summary>
/// <param name="clip">The clip rounded rectangle</param> /// <param name="clip">The clip rounded rectangle</param>
void PushClip(RoundedRect clip); void PushClip(RoundedRect clip);
/// <summary>
/// Pushes a clip region.
/// </summary>
/// <param name="region">The clip region</param>
void PushClip(IPlatformRenderInterfaceRegion region);
/// <summary> /// <summary>
/// Pops the latest pushed clip rectangle. /// Pops the latest pushed clip rectangle.
/// </summary> /// </summary>
void PopClip(); void PopClip();
/// <summary>
/// Enforces rendering to happen on an intermediate surface
/// </summary>
void PushLayer(Rect bounds);
/// <summary>
/// Pops the latest pushed intermediate surface layer.
/// </summary>
void PopLayer();
/// <summary> /// <summary>
/// Pushes an opacity value. /// Pushes an opacity value.
/// </summary> /// </summary>

3
src/Avalonia.Base/Platform/IPlatformRenderInterface.cs

@ -199,6 +199,9 @@ namespace Avalonia.Platform
public PixelFormat DefaultPixelFormat { get; } public PixelFormat DefaultPixelFormat { get; }
bool IsSupportedBitmapPixelFormat(PixelFormat format); bool IsSupportedBitmapPixelFormat(PixelFormat format);
bool SupportsRegions { get; }
IPlatformRenderInterfaceRegion CreateRegion();
} }
[Unstable, PrivateApi] [Unstable, PrivateApi]

17
src/Avalonia.Base/Platform/IPlatformRenderInterfaceRegion.cs

@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using Avalonia.Metadata;
namespace Avalonia.Platform;
[Unstable, PrivateApi]
public interface IPlatformRenderInterfaceRegion : IDisposable
{
void AddRect(PixelRect rect);
void Reset();
bool IsEmpty { get; }
PixelRect Bounds { get; }
IList<PixelRect> Rects { get; }
bool Intersects(Rect rect);
bool Contains(Point pt);
}

3
src/Avalonia.Base/Platform/IRenderTarget.cs

@ -16,7 +16,8 @@ namespace Avalonia.Platform
/// <summary> /// <summary>
/// Creates an <see cref="IDrawingContextImpl"/> for a rendering session. /// Creates an <see cref="IDrawingContextImpl"/> for a rendering session.
/// </summary> /// </summary>
IDrawingContextImpl CreateDrawingContext(); /// <param name="useScaledDrawing">Apply DPI reported by the render target as a hidden transform matrix</param>
IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing);
/// <summary> /// <summary>
/// Indicates if the render target is no longer usable and needs to be recreated /// Indicates if the render target is no longer usable and needs to be recreated

3
src/Avalonia.Base/Rendering/Composition/CompositingRenderer.cs

@ -172,7 +172,8 @@ internal class CompositingRenderer : IRendererWithCompositor, IHitTester
v.SynchronizeCompositionChildVisuals(); v.SynchronizeCompositionChildVisuals();
_dirty.Clear(); _dirty.Clear();
_recalculateChildren.Clear(); _recalculateChildren.Clear();
CompositionTarget.Size = _root.ClientSize;
CompositionTarget.PixelSize = PixelSize.FromSizeRounded(_root.ClientSize, _root.RenderScaling);
CompositionTarget.Scaling = _root.RenderScaling; CompositionTarget.Scaling = _root.RenderScaling;
var commit = _compositor.RequestCommitAsync(); var commit = _compositor.RequestCommitAsync();

32
src/Avalonia.Base/Rendering/Composition/CompositionCustomVisual.cs

@ -1,12 +1,15 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using Avalonia.Media;
using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.Composition.Server;
using Avalonia.Rendering.Composition.Transport; using Avalonia.Rendering.Composition.Transport;
using Avalonia.Threading;
namespace Avalonia.Rendering.Composition; namespace Avalonia.Rendering.Composition;
public sealed class CompositionCustomVisual : CompositionContainerVisual public sealed class CompositionCustomVisual : CompositionContainerVisual
{ {
private static readonly ThreadSafeObjectPool<List<object>> s_messageListPool = new();
private List<object>? _messages; private List<object>? _messages;
internal CompositionCustomVisual(Compositor compositor, CompositionCustomVisualHandler handler) internal CompositionCustomVisual(Compositor compositor, CompositionCustomVisualHandler handler)
@ -17,21 +20,26 @@ public sealed class CompositionCustomVisual : CompositionContainerVisual
public void SendHandlerMessage(object message) public void SendHandlerMessage(object message)
{ {
(_messages ??= new()).Add(message); if (_messages == null)
RegisterForSerialization(); {
_messages = s_messageListPool.Get();
Compositor.RequestCompositionUpdate(OnCompositionUpdate);
}
_messages.Add(message);
} }
private protected override void SerializeChangesCore(BatchStreamWriter writer) private void OnCompositionUpdate()
{ {
base.SerializeChangesCore(writer); if(_messages == null)
if (_messages == null || _messages.Count == 0) return;
writer.Write(0);
else var messages = _messages;
_messages = null;
Compositor.PostServerJob(()=>
{ {
writer.Write(_messages.Count); ((ServerCompositionCustomVisual)Server).DispatchMessages(messages);
foreach (var m in _messages) messages.Clear();
writer.WriteObject(m); s_messageListPool.ReturnAndSetNull(ref messages);
_messages.Clear(); });
}
} }
} }

47
src/Avalonia.Base/Rendering/Composition/CompositionCustomVisualHandler.cs

@ -1,5 +1,7 @@
using System; using System;
using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using Avalonia.Collections.Pooled;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Rendering.Composition.Server; using Avalonia.Rendering.Composition.Server;
@ -8,6 +10,8 @@ namespace Avalonia.Rendering.Composition;
public abstract class CompositionCustomVisualHandler public abstract class CompositionCustomVisualHandler
{ {
private ServerCompositionCustomVisual? _host; private ServerCompositionCustomVisual? _host;
private bool _inRender;
private Rect _currentTransformedClip;
public virtual void OnMessage(object message) public virtual void OnMessage(object message)
{ {
@ -18,7 +22,21 @@ public abstract class CompositionCustomVisualHandler
{ {
} }
internal void Render(ImmediateDrawingContext drawingContext, Rect currentTransformedClip)
{
_inRender = true;
_currentTransformedClip = currentTransformedClip;
try
{
OnRender(drawingContext);
}
finally
{
_inRender = false;
}
}
public abstract void OnRender(ImmediateDrawingContext drawingContext); public abstract void OnRender(ImmediateDrawingContext drawingContext);
void VerifyAccess() void VerifyAccess()
@ -28,6 +46,13 @@ public abstract class CompositionCustomVisualHandler
_host.Compositor.VerifyAccess(); _host.Compositor.VerifyAccess();
} }
void VerifyInRender()
{
VerifyAccess();
if (!_inRender)
throw new InvalidOperationException("This API is only available from OnRender");
}
protected Vector EffectiveSize protected Vector EffectiveSize
{ {
get get
@ -57,9 +82,29 @@ public abstract class CompositionCustomVisualHandler
_host!.HandlerInvalidate(); _host!.HandlerInvalidate();
} }
protected void Invalidate(Rect rc)
{
VerifyAccess();
_host!.HandlerInvalidate(rc);
}
protected void RegisterForNextAnimationFrameUpdate() protected void RegisterForNextAnimationFrameUpdate()
{ {
VerifyAccess(); VerifyAccess();
_host!.HandlerRegisterForNextAnimationFrameUpdate(); _host!.HandlerRegisterForNextAnimationFrameUpdate();
} }
protected bool RenderClipContains(Point pt)
{
VerifyInRender();
pt *= _host!.GlobalTransformMatrix;
return _currentTransformedClip.Contains(pt) && _host.Root!.DirtyRects.Contains(pt);
}
protected bool RenderClipIntersectes(Rect rc)
{
VerifyInRender();
rc = rc.TransformToAABB(_host!.GlobalTransformMatrix);
return _currentTransformedClip.Intersects(rc) && _host.Root!.DirtyRects.Intersects(rc);
}
} }

16
src/Avalonia.Base/Rendering/Composition/CompositionOptions.cs

@ -0,0 +1,16 @@
namespace Avalonia.Rendering.Composition;
public class CompositionOptions
{
/// <summary>
/// Enables more accurate tracking of dirty rects by utilizing regions if supported by the underlying
/// drawing context
/// </summary>
public bool? UseRegionDirtyRectClipping { get; set; }
/// <summary>
/// Enforces dirty contents to be rendered into an extra intermediate surface before being applied onto the
/// saved frame.
/// Required as a workaround for Skia bug https://issues.skia.org/issues/327877721
/// </summary>
public bool? UseSaveLayerRootClip { get; set; }
}

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

@ -29,6 +29,7 @@ namespace Avalonia.Rendering.Composition
/// <returns></returns> /// <returns></returns>
public PooledList<CompositionVisual>? TryHitTest(Point point, CompositionVisual? root, Func<CompositionVisual, bool>? filter) public PooledList<CompositionVisual>? TryHitTest(Point point, CompositionVisual? root, Func<CompositionVisual, bool>? filter)
{ {
point *= Scaling;
Server.Readback.NextRead(); Server.Readback.NextRead();
root ??= Root; root ??= Root;
if (root == null) if (root == null)

2
src/Avalonia.Base/Rendering/Composition/Server/DiagnosticTextRenderer.cs

@ -67,7 +67,7 @@ namespace Avalonia.Rendering.Composition.Server
{ {
var effectiveChar = c is >= FirstChar and <= LastChar ? c : ' '; var effectiveChar = c is >= FirstChar and <= LastChar ? c : ' ';
var run = _runs[effectiveChar - FirstChar]; var run = _runs[effectiveChar - FirstChar];
context.Transform = originalTransform * Matrix.CreateTranslation(offset, 0.0); context.Transform = Matrix.CreateTranslation(offset, 0.0) * originalTransform;
context.DrawGlyphRun(foreground, run.PlatformImpl.Item); context.DrawGlyphRun(foreground, run.PlatformImpl.Item);
offset += run.Bounds.Width; offset += run.Bounds.Width;
} }

103
src/Avalonia.Base/Rendering/Composition/Server/DirtyRectTracker.cs

@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Avalonia.Media;
using Avalonia.Media.Immutable;
using Avalonia.Platform;
using Avalonia.Reactive;
namespace Avalonia.Rendering.Composition.Server;
internal interface IDirtyRectTracker
{
void AddRect(PixelRect rect);
IDisposable BeginDraw(IDrawingContextImpl ctx);
bool IsEmpty { get; }
bool Intersects(Rect rect);
bool Contains(Point pt);
void Reset();
void Visualize(IDrawingContextImpl context);
PixelRect CombinedRect { get; }
IList<PixelRect> Rects { get; }
}
internal class DirtyRectTracker : IDirtyRectTracker
{
private PixelRect _rect;
private Rect _doubleRect;
private PixelRect[] _rectsForApi = new PixelRect[1];
private Random _random = new();
public void AddRect(PixelRect rect)
{
_rect = _rect.Union(rect);
}
public IDisposable BeginDraw(IDrawingContextImpl ctx)
{
ctx.PushClip(_rect.ToRect(1));
_doubleRect = _rect.ToRect(1);
return Disposable.Create(ctx.PopClip);
}
public bool IsEmpty => _rect.Width == 0 | _rect.Height == 0;
public bool Intersects(Rect rect) => _doubleRect.Intersects(rect);
public bool Contains(Point pt) => _rect.Contains(PixelPoint.FromPoint(pt, 1));
public void Reset() => _rect = default;
public void Visualize(IDrawingContextImpl context)
{
context.DrawRectangle(
new ImmutableSolidColorBrush(
new Color(30, (byte)_random.Next(255), (byte)_random.Next(255), (byte)_random.Next(255))),
null, _doubleRect);
}
public PixelRect CombinedRect => _rect;
public IList<PixelRect> Rects
{
get
{
if (_rect.Width == 0 || _rect.Height == 0)
return Array.Empty<PixelRect>();
_rectsForApi[0] = _rect;
return _rectsForApi;
}
}
}
internal class RegionDirtyRectTracker : IDirtyRectTracker
{
private readonly IPlatformRenderInterfaceRegion _region;
private Random _random = new();
public RegionDirtyRectTracker(IPlatformRenderInterface platformRender)
{
_region = platformRender.CreateRegion();
}
public void AddRect(PixelRect rect) => _region.AddRect(rect);
public IDisposable BeginDraw(IDrawingContextImpl ctx)
{
ctx.PushClip(_region);
return Disposable.Create(ctx.PopClip);
}
public bool IsEmpty => _region.IsEmpty;
public bool Intersects(Rect rect) => _region.Intersects(rect);
public bool Contains(Point pt) => _region.Contains(pt);
public void Reset() => _region.Reset();
public void Visualize(IDrawingContextImpl context)
{
context.DrawRegion(
new ImmutableSolidColorBrush(
new Color(150, (byte)_random.Next(255), (byte)_random.Next(255), (byte)_random.Next(255))),
null, _region);
}
public PixelRect CombinedRect => _region.Bounds;
public IList<PixelRect> Rects => _region.Rects;
}

11
src/Avalonia.Base/Rendering/Composition/Server/DrawingContextProxy.cs

@ -77,6 +77,9 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl,
_impl.DrawRectangle(brush, pen, rect, boxShadows); _impl.DrawRectangle(brush, pen, rect, boxShadows);
} }
public void DrawRegion(IBrush? brush, IPen? pen, IPlatformRenderInterfaceRegion region) =>
_impl.DrawRegion(brush, pen, region);
public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect)
{ {
_impl.DrawEllipse(brush, pen, rect); _impl.DrawEllipse(brush, pen, rect);
@ -87,7 +90,7 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl,
_impl.DrawGlyphRun(foreground, glyphRun); _impl.DrawGlyphRun(foreground, glyphRun);
} }
public IDrawingContextLayerImpl CreateLayer(Size size) public IDrawingContextLayerImpl CreateLayer(PixelSize size)
{ {
return _impl.CreateLayer(size); return _impl.CreateLayer(size);
} }
@ -102,11 +105,17 @@ internal class CompositorDrawingContextProxy : IDrawingContextImpl,
_impl.PushClip(clip); _impl.PushClip(clip);
} }
public void PushClip(IPlatformRenderInterfaceRegion region) => _impl.PushClip(region);
public void PopClip() public void PopClip()
{ {
_impl.PopClip(); _impl.PopClip();
} }
public void PushLayer(Rect bounds) => _impl.PushLayer(bounds);
public void PopLayer() => _impl.PopLayer();
public void PushOpacity(double opacity, Rect? bounds) public void PushOpacity(double opacity, Rect? bounds)
{ {
_impl.PushOpacity(opacity, bounds); _impl.PushOpacity(opacity, bounds);

13
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs

@ -16,26 +16,27 @@ namespace Avalonia.Rendering.Composition.Server
private Rect? _transformedContentBounds; private Rect? _transformedContentBounds;
private IImmutableEffect? _oldEffect; private IImmutableEffect? _oldEffect;
protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip,
IDirtyRectTracker dirtyRects)
{ {
base.RenderCore(canvas, currentTransformedClip); base.RenderCore(canvas, currentTransformedClip, dirtyRects);
foreach (var ch in Children) foreach (var ch in Children)
{ {
ch.Render(canvas, currentTransformedClip); ch.Render(canvas, currentTransformedClip, dirtyRects);
} }
} }
public override UpdateResult Update(ServerCompositionTarget root) public override UpdateResult Update(ServerCompositionTarget root, Matrix parentCombinedTransform)
{ {
var (combinedBounds, oldInvalidated, newInvalidated) = base.Update(root); var (combinedBounds, oldInvalidated, newInvalidated) = base.Update(root, parentCombinedTransform);
foreach (var child in Children) foreach (var child in Children)
{ {
if (child.AdornedVisual != null) if (child.AdornedVisual != null)
root.EnqueueAdornerUpdate(child); root.EnqueueAdornerUpdate(child);
else else
{ {
var res = child.Update(root); var res = child.Update(root, GlobalTransformMatrix);
oldInvalidated |= res.InvalidatedOld; oldInvalidated |= res.InvalidatedOld;
newInvalidated |= res.InvalidatedNew; newInvalidated |= res.InvalidatedNew;
combinedBounds = Rect.Union(combinedBounds, res.Bounds); combinedBounds = Rect.Union(combinedBounds, res.Bounds);

6
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs

@ -40,13 +40,15 @@ internal class ServerCompositionDrawListVisual : ServerCompositionContainerVisua
base.DeserializeChangesCore(reader, committedAt); base.DeserializeChangesCore(reader, committedAt);
} }
protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip,
IDirtyRectTracker dirtyRects)
{ {
if (_renderCommands != null) if (_renderCommands != null)
{ {
_renderCommands.Render(canvas); _renderCommands.Render(canvas);
} }
base.RenderCore(canvas, currentTransformedClip);
base.RenderCore(canvas, currentTransformedClip, dirtyRects);
} }
public void DependencyQueuedInvalidate(IServerRenderResource sender) => ValuesInvalidated(); public void DependencyQueuedInvalidate(IServerRenderResource sender) => ValuesInvalidated();

5
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs

@ -5,7 +5,8 @@ namespace Avalonia.Rendering.Composition.Server;
internal partial class ServerCompositionExperimentalAcrylicVisual internal partial class ServerCompositionExperimentalAcrylicVisual
{ {
protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip,
IDirtyRectTracker dirtyRects)
{ {
var cornerRadius = CornerRadius; var cornerRadius = CornerRadius;
canvas.DrawRectangle( canvas.DrawRectangle(
@ -15,7 +16,7 @@ internal partial class ServerCompositionExperimentalAcrylicVisual
cornerRadius.TopLeft, cornerRadius.TopRight, cornerRadius.TopLeft, cornerRadius.TopRight,
cornerRadius.BottomRight, cornerRadius.BottomLeft)); cornerRadius.BottomRight, cornerRadius.BottomLeft));
base.RenderCore(canvas, currentTransformedClip); base.RenderCore(canvas, currentTransformedClip, dirtyRects);
} }
public override Rect OwnContentBounds => new(0, 0, Size.X, Size.Y); public override Rect OwnContentBounds => new(0, 0, Size.X, Size.Y);

3
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSolidColorVisual.cs

@ -4,7 +4,8 @@ namespace Avalonia.Rendering.Composition.Server;
internal partial class ServerCompositionSolidColorVisual internal partial class ServerCompositionSolidColorVisual
{ {
protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip,
IDirtyRectTracker dirtyRects)
{ {
canvas.DrawRectangle(new ImmutableSolidColorBrush(Color), null, new Rect(0, 0, Size.X, Size.Y)); canvas.DrawRectangle(new ImmutableSolidColorBrush(Color), null, new Rect(0, 0, Size.X, Size.Y));
} }

3
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs

@ -4,7 +4,8 @@ namespace Avalonia.Rendering.Composition.Server;
internal partial class ServerCompositionSurfaceVisual internal partial class ServerCompositionSurfaceVisual
{ {
protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip,
IDirtyRectTracker dirtyRects)
{ {
if (Surface == null) if (Surface == null)
return; return;

35
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.DirtyRects.cs

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using Avalonia.Collections.Pooled;
namespace Avalonia.Rendering.Composition.Server;
internal partial class ServerCompositionTarget
{
public readonly IDirtyRectTracker DirtyRects;
public void AddDirtyRect(Rect rect)
{
if (rect.Width == 0 && rect.Height == 0)
return;
var snapped = PixelRect.FromRect(SnapToDevicePixels(rect, Scaling), 1);
DebugEvents?.RectInvalidated(rect);
DirtyRects.AddRect(snapped);
_redrawRequested = true;
}
public Rect SnapToDevicePixels(Rect rect) => SnapToDevicePixels(rect, Scaling);
private static Rect SnapToDevicePixels(Rect rect, double scale)
{
return new Rect(
new Point(
Math.Floor(rect.X * scale) / scale,
Math.Floor(rect.Y * scale) / scale),
new Point(
Math.Ceiling(rect.Right * scale) / scale,
Math.Ceiling(rect.Bottom * scale) / scale));
}
}

121
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs

@ -2,6 +2,7 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Threading; using System.Threading;
using Avalonia.Collections.Pooled;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Media.Immutable; using Avalonia.Media.Immutable;
@ -25,10 +26,9 @@ namespace Avalonia.Rendering.Composition.Server
private FpsCounter? _fpsCounter; private FpsCounter? _fpsCounter;
private FrameTimeGraph? _renderTimeGraph; private FrameTimeGraph? _renderTimeGraph;
private FrameTimeGraph? _layoutTimeGraph; private FrameTimeGraph? _layoutTimeGraph;
private Rect _dirtyRect; private PixelSize _layerSize;
private readonly Random _random = new();
private Size _layerSize;
private IDrawingContextLayerImpl? _layer; private IDrawingContextLayerImpl? _layer;
private bool _updateRequested;
private bool _redrawRequested; private bool _redrawRequested;
private bool _disposed; private bool _disposed;
private readonly HashSet<ServerCompositionVisual> _attachedVisuals = new(); private readonly HashSet<ServerCompositionVisual> _attachedVisuals = new();
@ -56,6 +56,11 @@ namespace Avalonia.Rendering.Composition.Server
_compositor = compositor; _compositor = compositor;
_surfaces = surfaces; _surfaces = surfaces;
_diagnosticTextRenderer = diagnosticTextRenderer; _diagnosticTextRenderer = diagnosticTextRenderer;
var platformRender = AvaloniaLocator.Current.GetService<IPlatformRenderInterface>();
DirtyRects = compositor.Options.UseRegionDirtyRectClipping == true &&
platformRender?.SupportsRegions == true
? new RegionDirtyRectTracker(platformRender)
: new DirtyRectTracker();
Id = Interlocked.Increment(ref s_nextId); Id = Interlocked.Increment(ref s_nextId);
} }
@ -146,7 +151,7 @@ namespace Avalonia.Rendering.Composition.Server
return; return;
} }
if ((_dirtyRect.Width == 0 && _dirtyRect.Height == 0) && !_redrawRequested) if (DirtyRects.IsEmpty && !_redrawRequested && !_updateRequested)
return; return;
Revision++; Revision++;
@ -154,39 +159,51 @@ namespace Avalonia.Rendering.Composition.Server
var captureTiming = (DebugOverlays & RendererDebugOverlays.RenderTimeGraph) != 0; var captureTiming = (DebugOverlays & RendererDebugOverlays.RenderTimeGraph) != 0;
var startingTimestamp = captureTiming ? Stopwatch.GetTimestamp() : 0L; var startingTimestamp = captureTiming ? Stopwatch.GetTimestamp() : 0L;
var transform = Matrix.CreateScale(Scaling, Scaling);
// Update happens in a separate phase to extend dirty rect if needed // Update happens in a separate phase to extend dirty rect if needed
Root.Update(this); Root.Update(this, transform);
while (_adornerUpdateQueue.Count > 0) while (_adornerUpdateQueue.Count > 0)
{ {
var adorner = _adornerUpdateQueue.Dequeue(); var adorner = _adornerUpdateQueue.Dequeue();
adorner.Update(this); adorner.Update(this, transform);
} }
_updateRequested = false;
Readback.CompleteWrite(Revision); Readback.CompleteWrite(Revision);
if (!_redrawRequested)
return;
_redrawRequested = false; _redrawRequested = false;
using (var targetContext = _renderTarget.CreateDrawingContext()) using (var targetContext = _renderTarget.CreateDrawingContext(false))
{ {
var size = Size; if (PixelSize != _layerSize || _layer == null || _layer.IsCorrupted)
var layerSize = size * Scaling;
if (layerSize != _layerSize || _layer == null || _layer.IsCorrupted)
{ {
_layer?.Dispose(); _layer?.Dispose();
_layer = null; _layer = null;
_layer = targetContext.CreateLayer(size); _layer = targetContext.CreateLayer(PixelSize);
_layerSize = layerSize; _layerSize = PixelSize;
_dirtyRect = new Rect(0, 0, size.Width, size.Height); DirtyRects.AddRect(new PixelRect(_layerSize));
} }
if (_dirtyRect.Width != 0 || _dirtyRect.Height != 0) if (!DirtyRects.IsEmpty)
{ {
using (var context = _layer.CreateDrawingContext()) var useLayerClip = Compositor.Options.UseSaveLayerRootClip ??
Compositor.RenderInterface.GpuContext != null;
using (var context = _layer.CreateDrawingContext(false))
{ {
context.PushClip(_dirtyRect); using (DirtyRects.BeginDraw(context))
context.Clear(Colors.Transparent); {
Root.Render(new CompositorDrawingContextProxy(context), _dirtyRect); context.Clear(Colors.Transparent);
context.PopClip(); if (useLayerClip)
context.PushLayer(DirtyRects.CombinedRect.ToRect(1));
Root.Render(new CompositorDrawingContextProxy(context), null, DirtyRects);
if (useLayerClip)
context.PopLayer();
}
} }
} }
@ -195,9 +212,10 @@ namespace Avalonia.Rendering.Composition.Server
if (_layer.CanBlit) if (_layer.CanBlit)
_layer.Blit(targetContext); _layer.Blit(targetContext);
else else
targetContext.DrawBitmap(_layer, 1, {
new Rect(_layerSize), var rect = new PixelRect(default, PixelSize).ToRect(1);
new Rect(size)); targetContext.DrawBitmap(_layer, 1, rect, rect);
}
if (DebugOverlays != RendererDebugOverlays.None) if (DebugOverlays != RendererDebugOverlays.None)
{ {
@ -206,27 +224,24 @@ namespace Avalonia.Rendering.Composition.Server
var elapsed = StopwatchHelper.GetElapsedTime(startingTimestamp); var elapsed = StopwatchHelper.GetElapsedTime(startingTimestamp);
RenderTimeGraph?.AddFrameValue(elapsed.TotalMilliseconds); RenderTimeGraph?.AddFrameValue(elapsed.TotalMilliseconds);
} }
DrawOverlays(targetContext); DrawOverlays(targetContext, PixelSize.ToSize(Scaling));
} }
RenderedVisuals = 0; RenderedVisuals = 0;
_dirtyRect = default; DirtyRects.Reset();
} }
} }
private void DrawOverlays(IDrawingContextImpl targetContext) private void DrawOverlays(IDrawingContextImpl targetContext, Size logicalSize)
{ {
if ((DebugOverlays & RendererDebugOverlays.DirtyRects) != 0) if ((DebugOverlays & RendererDebugOverlays.DirtyRects) != 0)
{ DirtyRects.Visualize(targetContext);
targetContext.DrawRectangle(
new ImmutableSolidColorBrush(
new Color(30, (byte)_random.Next(255), (byte)_random.Next(255), (byte)_random.Next(255))),
null,
_dirtyRect);
}
targetContext.Transform = Matrix.CreateScale(Scaling, Scaling);
if ((DebugOverlays & RendererDebugOverlays.Fps) != 0) if ((DebugOverlays & RendererDebugOverlays.Fps) != 0)
{ {
var nativeMem = ByteSizeHelper.ToString((ulong) ( var nativeMem = ByteSizeHelper.ToString((ulong) (
@ -247,9 +262,13 @@ namespace Avalonia.Rendering.Composition.Server
if (graph == null) if (graph == null)
return; return;
top += 8.0; top += 8.0;
targetContext.Transform = Matrix.CreateTranslation(Size.Width - graph.Size.Width - 8.0, top); var oldTransform = targetContext.Transform;
targetContext.Transform = Matrix.CreateTranslation(logicalSize.Width - graph.Size.Width - 8.0, top) *
oldTransform;
graph.Render(targetContext); graph.Render(targetContext);
top += graph.Size.Height; top += graph.Size.Height;
targetContext.Transform = oldTransform;
} }
if ((DebugOverlays & RendererDebugOverlays.LayoutTimeGraph) != 0) if ((DebugOverlays & RendererDebugOverlays.LayoutTimeGraph) != 0)
@ -261,35 +280,11 @@ namespace Avalonia.Rendering.Composition.Server
{ {
DrawTimeGraph(RenderTimeGraph); DrawTimeGraph(RenderTimeGraph);
} }
}
targetContext.Transform = Matrix.Identity;
public Rect SnapToDevicePixels(Rect rect) => SnapToDevicePixels(rect, Scaling);
private static Rect SnapToDevicePixels(Rect rect, double scale)
{
return new Rect(
new Point(
Math.Floor(rect.X * scale) / scale,
Math.Floor(rect.Y * scale) / scale),
new Point(
Math.Ceiling(rect.Right * scale) / scale,
Math.Ceiling(rect.Bottom * scale) / scale));
} }
public void AddDirtyRect(Rect rect) public void RequestUpdate() => _updateRequested = true;
{
if (rect.Width == 0 && rect.Height == 0)
return;
var snapped = SnapToDevicePixels(rect, Scaling);
DebugEvents?.RectInvalidated(rect);
_dirtyRect = _dirtyRect.Union(snapped);
_redrawRequested = true;
}
public void Invalidate()
{
_redrawRequested = true;
}
public void Dispose() public void Dispose()
{ {

21
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs

@ -22,20 +22,25 @@ namespace Avalonia.Rendering.Composition.Server
private Rect? _transformedClipBounds; private Rect? _transformedClipBounds;
private Rect _combinedTransformedClipBounds; private Rect _combinedTransformedClipBounds;
protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) protected virtual void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip,
IDirtyRectTracker dirtyRects)
{ {
} }
public void Render(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) public void Render(CompositorDrawingContextProxy canvas, Rect? parentTransformedClip, IDirtyRectTracker dirtyRects)
{ {
if (Visible == false || IsVisibleInFrame == false) if (Visible == false || IsVisibleInFrame == false)
return; return;
if (Opacity == 0) if (Opacity == 0)
return; return;
currentTransformedClip = currentTransformedClip.Intersect(_combinedTransformedClipBounds); var currentTransformedClip = parentTransformedClip.HasValue
? parentTransformedClip.Value.Intersect(_combinedTransformedClipBounds)
: _combinedTransformedClipBounds;
if (currentTransformedClip.Width == 0 && currentTransformedClip.Height == 0) if (currentTransformedClip.Width == 0 && currentTransformedClip.Height == 0)
return; return;
if(!dirtyRects.Intersects(currentTransformedClip))
return;
Root!.RenderedVisuals++; Root!.RenderedVisuals++;
Root!.DebugEvents?.IncrementRenderedVisuals(); Root!.DebugEvents?.IncrementRenderedVisuals();
@ -67,7 +72,7 @@ namespace Avalonia.Rendering.Composition.Server
canvas.RenderOptions = RenderOptions; canvas.RenderOptions = RenderOptions;
RenderCore(canvas, currentTransformedClip); RenderCore(canvas, currentTransformedClip, dirtyRects);
// Hack to force invalidation of SKMatrix // Hack to force invalidation of SKMatrix
canvas.PostTransform = transform; canvas.PostTransform = transform;
@ -116,7 +121,7 @@ namespace Avalonia.Rendering.Composition.Server
} }
} }
public virtual UpdateResult Update(ServerCompositionTarget root) public virtual UpdateResult Update(ServerCompositionTarget root, Matrix parentVisualTransform)
{ {
if (Parent == null && Root == null) if (Parent == null && Root == null)
return default; return default;
@ -138,7 +143,7 @@ namespace Avalonia.Rendering.Composition.Server
_combinedTransformDirty = false; _combinedTransformDirty = false;
} }
var parentTransform = (AdornedVisual ?? Parent)?.GlobalTransformMatrix ?? Matrix.Identity; var parentTransform = AdornedVisual?.GlobalTransformMatrix ?? parentVisualTransform;
var newTransform = CombinedTransformMatrix * parentTransform; var newTransform = CombinedTransformMatrix * parentTransform;
@ -207,7 +212,7 @@ namespace Avalonia.Rendering.Composition.Server
_combinedTransformedClipBounds = _combinedTransformedClipBounds =
(AdornerIsClipped ? AdornedVisual?._combinedTransformedClipBounds : null) (AdornerIsClipped ? AdornedVisual?._combinedTransformedClipBounds : null)
?? (Parent?.Effect == null ? Parent?._combinedTransformedClipBounds : null) ?? (Parent?.Effect == null ? Parent?._combinedTransformedClipBounds : null)
?? new Rect(Root!.Size); ?? new Rect(Root!.PixelSize.ToSize(1));
if (_transformedClipBounds != null) if (_transformedClipBounds != null)
_combinedTransformedClipBounds = _combinedTransformedClipBounds.Intersect(_transformedClipBounds.Value); _combinedTransformedClipBounds = _combinedTransformedClipBounds.Intersect(_transformedClipBounds.Value);
@ -301,7 +306,7 @@ namespace Avalonia.Rendering.Composition.Server
protected override void ValuesInvalidated() protected override void ValuesInvalidated()
{ {
_isDirtyForUpdate = true; _isDirtyForUpdate = true;
Root?.Invalidate(); Root?.RequestUpdate();
} }
public bool IsVisibleInFrame { get; set; } public bool IsVisibleInFrame { get; set; }

1
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs

@ -38,6 +38,7 @@ namespace Avalonia.Rendering.Composition.Server
internal static readonly object RenderThreadDisposeStartMarker = new(); internal static readonly object RenderThreadDisposeStartMarker = new();
internal static readonly object RenderThreadJobsStartMarker = new(); internal static readonly object RenderThreadJobsStartMarker = new();
internal static readonly object RenderThreadJobsEndMarker = new(); internal static readonly object RenderThreadJobsEndMarker = new();
public CompositionOptions Options { get; } = AvaloniaLocator.Current.GetService<CompositionOptions>() ?? new();
public ServerCompositor(IRenderLoop renderLoop, IPlatformGraphics? platformGraphics, public ServerCompositor(IRenderLoop renderLoop, IPlatformGraphics? platformGraphics,
BatchStreamObjectPool<object?> batchObjectPool, BatchStreamMemoryPool batchMemoryPool) BatchStreamObjectPool<object?> batchObjectPool, BatchStreamMemoryPool batchMemoryPool)

19
src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.Media; using Avalonia.Media;
@ -16,15 +17,13 @@ internal sealed class ServerCompositionCustomVisual : ServerCompositionContainer
_handler.Attach(this); _handler.Attach(this);
} }
protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpan committedAt) public void DispatchMessages(List<object> messages)
{ {
base.DeserializeChangesCore(reader, committedAt); foreach(var message in messages)
var count = reader.Read<int>();
for (var c = 0; c < count; c++)
{ {
try try
{ {
_handler.OnMessage(reader.ReadObject()!); _handler.OnMessage(message);
} }
catch (Exception e) catch (Exception e)
{ {
@ -58,6 +57,11 @@ internal sealed class ServerCompositionCustomVisual : ServerCompositionContainer
} }
internal void HandlerInvalidate() => ValuesInvalidated(); internal void HandlerInvalidate() => ValuesInvalidated();
internal void HandlerInvalidate(Rect rc)
{
Root?.AddDirtyRect(rc.TransformToAABB(GlobalTransformMatrix));
}
internal void HandlerRegisterForNextAnimationFrameUpdate() internal void HandlerRegisterForNextAnimationFrameUpdate()
{ {
@ -66,12 +70,13 @@ internal sealed class ServerCompositionCustomVisual : ServerCompositionContainer
Compositor.AddToClock(this); Compositor.AddToClock(this);
} }
protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip,
IDirtyRectTracker dirtyRects)
{ {
using var context = new ImmediateDrawingContext(canvas, false); using var context = new ImmediateDrawingContext(canvas, false);
try try
{ {
_handler.OnRender(context); _handler.Render(context, currentTransformedClip);
} }
catch (Exception e) catch (Exception e)
{ {

2
src/Avalonia.Base/composition-schema.xml

@ -54,7 +54,7 @@
<Property Name="DebugOverlays" Type="RendererDebugOverlays"/> <Property Name="DebugOverlays" Type="RendererDebugOverlays"/>
<Property Name="LastLayoutPassTiming" Type="LayoutPassTiming" Internal="true"/> <Property Name="LastLayoutPassTiming" Type="LayoutPassTiming" Internal="true"/>
<Property Name="Scaling" Type="double"/> <Property Name="Scaling" Type="double"/>
<Property Name="Size" Type="Size" /> <Property Name="PixelSize" Type="PixelSize" />
</Object> </Object>
<KeyFrameAnimation Name="Scalar" Type="float"/> <KeyFrameAnimation Name="Scalar" Type="float"/>
<KeyFrameAnimation Name="Double" Type="double"/> <KeyFrameAnimation Name="Double" Type="double"/>

5
src/Avalonia.Controls/BorderVisual.cs

@ -45,7 +45,8 @@ class CompositionBorderVisual : CompositionDrawListVisual
{ {
} }
protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip) protected override void RenderCore(CompositorDrawingContextProxy canvas, Rect currentTransformedClip,
IDirtyRectTracker dirtyRects)
{ {
if (ClipToBounds) if (ClipToBounds)
{ {
@ -56,7 +57,7 @@ class CompositionBorderVisual : CompositionDrawListVisual
canvas.PushClip(new RoundedRect(clipRect, _cornerRadius)); canvas.PushClip(new RoundedRect(clipRect, _cornerRadius));
} }
base.RenderCore(canvas, currentTransformedClip); base.RenderCore(canvas, currentTransformedClip, dirtyRects);
if(ClipToBounds) if(ClipToBounds)
canvas.PopClip(); canvas.PopClip();

2
src/Avalonia.X11/X11CursorFactory.cs

@ -115,7 +115,7 @@ namespace Avalonia.X11
using (var cpuContext = platformRenderInterface.CreateBackendContext(null)) using (var cpuContext = platformRenderInterface.CreateBackendContext(null))
using (var renderTarget = cpuContext.CreateRenderTarget(new[] { this })) using (var renderTarget = cpuContext.CreateRenderTarget(new[] { this }))
using (var ctx = renderTarget.CreateDrawingContext()) using (var ctx = renderTarget.CreateDrawingContext(true))
{ {
var r = new Rect(_pixelSize.ToSize(1)); var r = new Rect(_pixelSize.ToSize(1));
ctx.DrawBitmap(bitmap, 1, r, r); ctx.DrawBitmap(bitmap, 1, r, r);

2
src/Avalonia.X11/X11IconLoader.cs

@ -43,7 +43,7 @@ namespace Avalonia.X11
_bdata = new uint[_width * _height]; _bdata = new uint[_width * _height];
using(var cpuContext = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>().CreateBackendContext(null)) using(var cpuContext = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>().CreateBackendContext(null))
using(var rt = cpuContext.CreateRenderTarget(new[]{this})) using(var rt = cpuContext.CreateRenderTarget(new[]{this}))
using (var ctx = rt.CreateDrawingContext()) using (var ctx = rt.CreateDrawingContext(true))
ctx.DrawBitmap(bitmap.PlatformImpl.Item, 1, new Rect(bitmap.Size), ctx.DrawBitmap(bitmap.PlatformImpl.Item, 1, new Rect(bitmap.Size),
new Rect(0, 0, _width, _height)); new Rect(0, 0, _width, _height));
Data = new UIntPtr[_width * _height + 2]; Data = new UIntPtr[_width * _height + 2];

26
src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs

@ -33,6 +33,8 @@ namespace Avalonia.Headless
public PixelFormat DefaultPixelFormat => PixelFormat.Rgba8888; public PixelFormat DefaultPixelFormat => PixelFormat.Rgba8888;
public bool IsSupportedBitmapPixelFormat(PixelFormat format) => true; public bool IsSupportedBitmapPixelFormat(PixelFormat format) => true;
public bool SupportsRegions => false;
public IPlatformRenderInterfaceRegion CreateRegion() => throw new NotSupportedException();
public IGeometryImpl CreateEllipseGeometry(Rect rect) => new HeadlessGeometryStub(rect); public IGeometryImpl CreateEllipseGeometry(Rect rect) => new HeadlessGeometryStub(rect);
@ -398,7 +400,7 @@ namespace Avalonia.Headless
} }
public IDrawingContextImpl CreateDrawingContext() public IDrawingContextImpl CreateDrawingContext(bool _)
{ {
return new HeadlessDrawingContextStub(); return new HeadlessDrawingContextStub();
} }
@ -454,7 +456,7 @@ namespace Avalonia.Headless
} }
public IDrawingContextLayerImpl CreateLayer(Size size) public IDrawingContextLayerImpl CreateLayer(PixelSize size)
{ {
return new HeadlessBitmapStub(size, new Vector(96, 96)); return new HeadlessBitmapStub(size, new Vector(96, 96));
} }
@ -464,11 +466,24 @@ namespace Avalonia.Headless
} }
public void PushClip(IPlatformRenderInterfaceRegion region)
{
}
public void PopClip() public void PopClip()
{ {
} }
public void PushLayer(Rect bounds)
{
}
public void PopLayer()
{
}
public void PushOpacity(double opacity, Rect? rect) public void PushOpacity(double opacity, Rect? rect)
{ {
@ -541,6 +556,11 @@ namespace Avalonia.Headless
} }
public void DrawRegion(IBrush? brush, IPen? pen, IPlatformRenderInterfaceRegion region)
{
}
public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect)
{ {
} }
@ -573,7 +593,7 @@ namespace Avalonia.Headless
} }
public IDrawingContextImpl CreateDrawingContext() public IDrawingContextImpl CreateDrawingContext(bool _)
{ {
return new HeadlessDrawingContextStub(); return new HeadlessDrawingContextStub();
} }

95
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -21,7 +21,9 @@ namespace Avalonia.Skia
IDrawingContextImplWithEffects IDrawingContextImplWithEffects
{ {
private IDisposable?[]? _disposables; private IDisposable?[]? _disposables;
private readonly Vector _dpi; // TODO: Get rid of this value, it's currently used to calculate intermediate sizes for tile brushes
// but does so ignoring the current transform
private readonly Vector _intermediateSurfaceDpi;
private readonly Stack<PaintWrapper> _maskStack = new(); private readonly Stack<PaintWrapper> _maskStack = new();
private readonly Stack<double> _opacityStack = new(); private readonly Stack<double> _opacityStack = new();
private readonly Stack<RenderOptions> _renderOptionsStack = new(); private readonly Stack<RenderOptions> _renderOptionsStack = new();
@ -57,7 +59,12 @@ namespace Avalonia.Skia
public SKSurface? Surface; public SKSurface? Surface;
/// <summary> /// <summary>
/// Dpi of drawings. /// Makes DPI to be applied as a hidden matrix transform
/// </summary>
public bool ScaleDrawingToDpi;
/// <summary>
/// Dpi for intermediate surfaces
/// </summary> /// </summary>
public Vector Dpi; public Vector Dpi;
@ -180,7 +187,7 @@ namespace Avalonia.Skia
Canvas = createInfo.Canvas ?? createInfo.Surface?.Canvas Canvas = createInfo.Canvas ?? createInfo.Surface?.Canvas
?? throw new ArgumentException("Invalid create info - no Canvas provided", nameof(createInfo)); ?? throw new ArgumentException("Invalid create info - no Canvas provided", nameof(createInfo));
_dpi = createInfo.Dpi; _intermediateSurfaceDpi = createInfo.Dpi;
_disposables = disposables; _disposables = disposables;
_disableSubpixelTextRendering = createInfo.DisableSubpixelTextRendering; _disableSubpixelTextRendering = createInfo.DisableSubpixelTextRendering;
_grContext = createInfo.GrContext; _grContext = createInfo.GrContext;
@ -191,10 +198,12 @@ namespace Avalonia.Skia
_session = createInfo.CurrentSession; _session = createInfo.CurrentSession;
if (!_dpi.NearlyEquals(SkiaPlatform.DefaultDpi))
if (createInfo.ScaleDrawingToDpi && !createInfo.Dpi.NearlyEquals(SkiaPlatform.DefaultDpi))
{ {
_postTransform = _postTransform =
Matrix.CreateScale(_dpi.X / SkiaPlatform.DefaultDpi.X, _dpi.Y / SkiaPlatform.DefaultDpi.Y); Matrix.CreateScale(createInfo.Dpi.X / SkiaPlatform.DefaultDpi.X,
createInfo.Dpi.Y / SkiaPlatform.DefaultDpi.Y);
} }
Transform = Matrix.Identity; Transform = Matrix.Identity;
@ -526,6 +535,32 @@ namespace Avalonia.Skia
SKRoundRectCache.Shared.Return(skRoundRect); SKRoundRectCache.Shared.Return(skRoundRect);
} }
/// <inheritdoc />
public void DrawRegion(IBrush? brush, IPen? pen, IPlatformRenderInterfaceRegion region)
{
var r = (SkiaRegionImpl)region;
if(r.IsEmpty)
return;
CheckLease();
if (brush != null)
{
using (var fill = CreatePaint(_fillPaint, brush, r.Bounds.ToRect(1)))
{
Canvas.DrawRegion(r.Region, fill.Paint);
}
}
if (pen is not null
&& TryCreatePaint(_strokePaint, pen, r.Bounds.ToRect(1).Inflate(new Thickness(pen.Thickness / 2))) is { } stroke)
{
using (stroke)
{
Canvas.DrawRegion(r.Region, stroke.Paint);
}
}
}
/// <inheritdoc /> /// <inheritdoc />
public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect) public void DrawEllipse(IBrush? brush, IPen? pen, Rect rect)
{ {
@ -591,7 +626,7 @@ namespace Avalonia.Skia
} }
/// <inheritdoc /> /// <inheritdoc />
public IDrawingContextLayerImpl CreateLayer(Size size) public IDrawingContextLayerImpl CreateLayer(PixelSize size)
{ {
CheckLease(); CheckLease();
return CreateRenderTarget(size, true); return CreateRenderTarget(size, true);
@ -629,6 +664,14 @@ namespace Avalonia.Skia
SKRoundRectCache.Shared.Return(roundRect); SKRoundRectCache.Shared.Return(roundRect);
} }
public void PushClip(IPlatformRenderInterfaceRegion region)
{
var r = ((SkiaRegionImpl)region).Region;
CheckLease();
Canvas.Save();
Canvas.ClipRegion(r);
}
/// <inheritdoc /> /// <inheritdoc />
public void PopClip() public void PopClip()
{ {
@ -636,6 +679,18 @@ namespace Avalonia.Skia
Canvas.Restore(); Canvas.Restore();
} }
public void PushLayer(Rect bounds)
{
CheckLease();
Canvas.SaveLayer(bounds.ToSKRect(), null!);
}
public void PopLayer()
{
CheckLease();
Canvas.Restore();
}
/// <inheritdoc /> /// <inheritdoc />
public void PushOpacity(double opacity, Rect? bounds) public void PushOpacity(double opacity, Rect? bounds)
{ {
@ -976,15 +1031,16 @@ namespace Avalonia.Skia
/// <param name="tileBrushImage">Tile brush image.</param> /// <param name="tileBrushImage">Tile brush image.</param>
private void ConfigureTileBrush(ref PaintWrapper paintWrapper, Rect targetBox, ITileBrush tileBrush, IDrawableBitmapImpl tileBrushImage) private void ConfigureTileBrush(ref PaintWrapper paintWrapper, Rect targetBox, ITileBrush tileBrush, IDrawableBitmapImpl tileBrushImage)
{ {
var calc = new TileBrushCalculator(tileBrush, tileBrushImage.PixelSize.ToSizeWithDpi(_dpi), targetBox.Size); var calc = new TileBrushCalculator(tileBrush, tileBrushImage.PixelSize.ToSizeWithDpi(_intermediateSurfaceDpi), targetBox.Size);
var intermediate = CreateRenderTarget(calc.IntermediateSize, false); var intermediate = CreateRenderTarget(
PixelSize.FromSizeWithDpi(calc.IntermediateSize, _intermediateSurfaceDpi), false);
paintWrapper.AddDisposable(intermediate); paintWrapper.AddDisposable(intermediate);
using (var context = intermediate.CreateDrawingContext()) using (var context = intermediate.CreateDrawingContext(true))
{ {
var sourceRect = new Rect(tileBrushImage.PixelSize.ToSizeWithDpi(96)); var sourceRect = new Rect(tileBrushImage.PixelSize.ToSizeWithDpi(96));
var targetRect = new Rect(tileBrushImage.PixelSize.ToSizeWithDpi(_dpi)); var targetRect = new Rect(tileBrushImage.PixelSize.ToSizeWithDpi(_intermediateSurfaceDpi));
context.Clear(Colors.Transparent); context.Clear(Colors.Transparent);
context.PushClip(calc.IntermediateClip); context.PushClip(calc.IntermediateClip);
@ -1028,7 +1084,7 @@ namespace Avalonia.Skia
SKMatrix.Concat( SKMatrix.Concat(
ref paintTransform, ref paintTransform,
tileTransform, tileTransform,
SKMatrix.CreateScale((float)(96.0 / _dpi.X), (float)(96.0 / _dpi.Y))); SKMatrix.CreateScale((float)(96.0 / _intermediateSurfaceDpi.X), (float)(96.0 / _intermediateSurfaceDpi.Y)));
if (tileBrush.Transform is { }) if (tileBrush.Transform is { })
{ {
@ -1066,9 +1122,10 @@ namespace Avalonia.Skia
if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1) if (intermediateSize.Width >= 1 && intermediateSize.Height >= 1)
{ {
using var intermediate = CreateRenderTarget(intermediateSize, false); using var intermediate = CreateRenderTarget(
PixelSize.FromSizeWithDpi(intermediateSize, _intermediateSurfaceDpi), false);
using (var ctx = intermediate.CreateDrawingContext()) using (var ctx = intermediate.CreateDrawingContext(true))
{ {
ctx.RenderOptions = RenderOptions; ctx.RenderOptions = RenderOptions;
ctx.Clear(Colors.Transparent); ctx.Clear(Colors.Transparent);
@ -1096,7 +1153,7 @@ namespace Avalonia.Skia
var calc = new TileBrushCalculator(tileBrush, contentSize, targetRect.Size); var calc = new TileBrushCalculator(tileBrush, contentSize, targetRect.Size);
transform *= calc.IntermediateTransform; transform *= calc.IntermediateTransform;
using var pictureTarget = new PictureRenderTarget(_gpu, _grContext, _dpi); using var pictureTarget = new PictureRenderTarget(_gpu, _grContext, _intermediateSurfaceDpi);
using (var ctx = pictureTarget.CreateDrawingContext(calc.IntermediateSize)) using (var ctx = pictureTarget.CreateDrawingContext(calc.IntermediateSize))
{ {
ctx.RenderOptions = RenderOptions; ctx.RenderOptions = RenderOptions;
@ -1127,7 +1184,7 @@ namespace Avalonia.Skia
: SKShaderTileMode.Repeat; : SKShaderTileMode.Repeat;
paintTransform = SKMatrix.Concat(paintTransform, paintTransform = SKMatrix.Concat(paintTransform,
SKMatrix.CreateScale((float)(96.0 / _dpi.X), (float)(96.0 / _dpi.Y))); SKMatrix.CreateScale((float)(96.0 / _intermediateSurfaceDpi.X), (float)(96.0 / _intermediateSurfaceDpi.Y)));
if (tileBrush.Transform is { }) if (tileBrush.Transform is { })
{ {
@ -1339,18 +1396,18 @@ namespace Avalonia.Skia
/// <summary> /// <summary>
/// Create new render target compatible with this drawing context. /// Create new render target compatible with this drawing context.
/// </summary> /// </summary>
/// <param name="size">The size of the render target in DIPs.</param> /// <param name="pixelSize">The size of the render target.</param>
/// <param name="dpi">The DPI of the render target.</param>
/// <param name="isLayer">Whether the render target is being created for a layer.</param> /// <param name="isLayer">Whether the render target is being created for a layer.</param>
/// <param name="format">Pixel format.</param> /// <param name="format">Pixel format.</param>
/// <returns></returns> /// <returns></returns>
private SurfaceRenderTarget CreateRenderTarget(Size size, bool isLayer, PixelFormat? format = null) private SurfaceRenderTarget CreateRenderTarget(PixelSize pixelSize, bool isLayer, PixelFormat? format = null)
{ {
var pixelSize = PixelSize.FromSizeWithDpi(size, _dpi);
var createInfo = new SurfaceRenderTarget.CreateInfo var createInfo = new SurfaceRenderTarget.CreateInfo
{ {
Width = pixelSize.Width, Width = pixelSize.Width,
Height = pixelSize.Height, Height = pixelSize.Height,
Dpi = _dpi, Dpi = _intermediateSurfaceDpi,
Format = format, Format = format,
DisableTextLcdRendering = isLayer ? _disableSubpixelTextRendering : true, DisableTextLcdRendering = isLayer ? _disableSubpixelTextRendering : true,
GrContext = _grContext, GrContext = _grContext,

5
src/Skia/Avalonia.Skia/FramebufferRenderTarget.cs

@ -37,7 +37,7 @@ namespace Avalonia.Skia
} }
/// <inheritdoc /> /// <inheritdoc />
public IDrawingContextImpl CreateDrawingContext() public IDrawingContextImpl CreateDrawingContext(bool scaleDrawingToDpi)
{ {
if (_renderTarget == null) if (_renderTarget == null)
throw new ObjectDisposedException(nameof(FramebufferRenderTarget)); throw new ObjectDisposedException(nameof(FramebufferRenderTarget));
@ -58,7 +58,8 @@ namespace Avalonia.Skia
var createInfo = new DrawingContextImpl.CreateInfo var createInfo = new DrawingContextImpl.CreateInfo
{ {
Surface = _framebufferSurface, Surface = _framebufferSurface,
Dpi = framebuffer.Dpi Dpi = framebuffer.Dpi,
ScaleDrawingToDpi = scaleDrawingToDpi
}; };
return new DrawingContextImpl(createInfo, _preFramebufferCopyHandler, canvas, framebuffer); return new DrawingContextImpl(createInfo, _preFramebufferCopyHandler, canvas, framebuffer);

3
src/Skia/Avalonia.Skia/Gpu/SkiaGpuRenderTarget.cs

@ -21,7 +21,7 @@ namespace Avalonia.Skia
_renderTarget.Dispose(); _renderTarget.Dispose();
} }
public IDrawingContextImpl CreateDrawingContext() public IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing)
{ {
var session = _renderTarget.BeginRenderingSession(); var session = _renderTarget.BeginRenderingSession();
@ -30,6 +30,7 @@ namespace Avalonia.Skia
GrContext = session.GrContext, GrContext = session.GrContext,
Surface = session.SkSurface, Surface = session.SkSurface,
Dpi = SkiaPlatform.DefaultDpi * session.ScaleFactor, Dpi = SkiaPlatform.DefaultDpi * session.ScaleFactor,
ScaleDrawingToDpi = useScaledDrawing,
Gpu = _skiaGpu, Gpu = _skiaGpu,
CurrentSession = session CurrentSession = session
}; };

1
src/Skia/Avalonia.Skia/PictureRenderTarget.cs

@ -38,6 +38,7 @@ internal class PictureRenderTarget : IDisposable
var createInfo = new DrawingContextImpl.CreateInfo var createInfo = new DrawingContextImpl.CreateInfo
{ {
Canvas = canvas, Canvas = canvas,
ScaleDrawingToDpi = true,
Dpi = _dpi, Dpi = _dpi,
DisableSubpixelTextRendering = true, DisableSubpixelTextRendering = true,
GrContext = _grContext, GrContext = _grContext,

3
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@ -50,6 +50,9 @@ namespace Avalonia.Skia
|| format == PixelFormats.Bgra8888 || format == PixelFormats.Bgra8888
|| format == PixelFormats.Rgba8888; || format == PixelFormats.Rgba8888;
public bool SupportsRegions => true;
public IPlatformRenderInterfaceRegion CreateRegion() => new SkiaRegionImpl();
public IGeometryImpl CreateEllipseGeometry(Rect rect) => new EllipseGeometryImpl(rect); public IGeometryImpl CreateEllipseGeometry(Rect rect) => new EllipseGeometryImpl(rect);
public IGeometryImpl CreateLineGeometry(Point p1, Point p2) => new LineGeometryImpl(p1, p2); public IGeometryImpl CreateLineGeometry(Point p1, Point p2) => new LineGeometryImpl(p1, p2);

3
src/Skia/Avalonia.Skia/RenderTargetBitmapImpl.cs

@ -19,7 +19,8 @@ internal class RenderTargetBitmapImpl : WriteableBitmapImpl,
_renderTarget = new FramebufferRenderTarget(this); _renderTarget = new FramebufferRenderTarget(this);
} }
public IDrawingContextImpl CreateDrawingContext() => _renderTarget.CreateDrawingContext(); IDrawingContextImpl IRenderTarget.CreateDrawingContext(bool useScaledDrawing) =>
_renderTarget.CreateDrawingContext(useScaledDrawing);
public bool IsCorrupted => false; public bool IsCorrupted => false;

53
src/Skia/Avalonia.Skia/SkiaRegionImpl.cs

@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using Avalonia.Platform;
using SkiaSharp;
namespace Avalonia.Skia;
internal class SkiaRegionImpl : IPlatformRenderInterfaceRegion
{
private SKRegion? _region = new();
public SKRegion Region => _region ?? throw new ObjectDisposedException(nameof(SkiaRegionImpl));
private bool _rectsValid;
private List<PixelRect>? _rects;
public void Dispose()
{
_region?.Dispose();
_region = null;
}
public void AddRect(PixelRect rect)
{
_rectsValid = false;
Region.Op(rect.X, rect.Y, rect.Right, rect.Bottom, SKRegionOperation.Union);
}
public void Reset()
{
_rectsValid = false;
Region.SetEmpty();
}
public bool IsEmpty => Region.IsEmpty;
public PixelRect Bounds => Region.Bounds.ToAvaloniaPixelRect();
public IList<PixelRect> Rects
{
get
{
_rects ??= new();
if (!_rectsValid)
{
_rects.Clear();
using var iter = Region.CreateRectIterator();
while (iter.Next(out var rc))
_rects.Add(rc.ToAvaloniaPixelRect());
}
return _rects;
}
}
public bool Intersects(Rect rect) => Region.Intersects(PixelRect.FromRect(rect, 1).ToSKRectI());
public bool Contains(Point pt) => Region.Contains((int)pt.X, (int)pt.Y);
}

10
src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs

@ -75,6 +75,11 @@ namespace Avalonia.Skia
{ {
return new SKRect((float)r.X, (float)r.Y, (float)r.Right, (float)r.Bottom); return new SKRect((float)r.X, (float)r.Y, (float)r.Right, (float)r.Bottom);
} }
public static SKRectI ToSKRectI(this PixelRect r)
{
return new SKRectI(r.X, r.Y, r.Right, r.Bottom);
}
public static SKRoundRect ToSKRoundRect(this RoundedRect r) public static SKRoundRect ToSKRoundRect(this RoundedRect r)
{ {
@ -95,6 +100,11 @@ namespace Avalonia.Skia
{ {
return new Rect(r.Left, r.Top, r.Right - r.Left, r.Bottom - r.Top); return new Rect(r.Left, r.Top, r.Right - r.Left, r.Bottom - r.Top);
} }
public static PixelRect ToAvaloniaPixelRect(this SKRectI r)
{
return new PixelRect(r.Left, r.Top, r.Right - r.Left, r.Bottom - r.Top);
}
public static SKMatrix ToSKMatrix(this Matrix m) public static SKMatrix ToSKMatrix(this Matrix m)
{ {

3
src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs

@ -97,7 +97,7 @@ namespace Avalonia.Skia
} }
/// <inheritdoc /> /// <inheritdoc />
public IDrawingContextImpl CreateDrawingContext() public IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing)
{ {
_canvas.RestoreToCount(-1); _canvas.RestoreToCount(-1);
_canvas.ResetMatrix(); _canvas.ResetMatrix();
@ -106,6 +106,7 @@ namespace Avalonia.Skia
{ {
Surface = _surface.Surface, Surface = _surface.Surface,
Dpi = Dpi, Dpi = Dpi,
ScaleDrawingToDpi = useScaledDrawing,
DisableSubpixelTextRendering = _disableLcdRendering, DisableSubpixelTextRendering = _disableLcdRendering,
GrContext = _grContext, GrContext = _grContext,
Gpu = _gpu, Gpu = _gpu,

3
src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs

@ -296,5 +296,8 @@ namespace Avalonia.Direct2D1
public bool IsSupportedBitmapPixelFormat(PixelFormat format) => public bool IsSupportedBitmapPixelFormat(PixelFormat format) =>
format == PixelFormats.Bgra8888 format == PixelFormats.Bgra8888
|| format == PixelFormats.Rgba8888; || format == PixelFormats.Rgba8888;
public bool SupportsRegions => false;
public IPlatformRenderInterfaceRegion CreateRegion() => throw new NotSupportedException();
} }
} }

4
src/Windows/Avalonia.Direct2D1/ExternalRenderTarget.cs

@ -21,11 +21,11 @@ namespace Avalonia.Direct2D1
_externalRenderTargetProvider.DestroyRenderTarget(); _externalRenderTargetProvider.DestroyRenderTarget();
} }
public IDrawingContextImpl CreateDrawingContext() public IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing)
{ {
var target = _externalRenderTargetProvider.GetOrCreateRenderTarget(); var target = _externalRenderTargetProvider.GetOrCreateRenderTarget();
_externalRenderTargetProvider.BeforeDrawing(); _externalRenderTargetProvider.BeforeDrawing();
return new DrawingContextImpl( null, target, null, () => return new DrawingContextImpl( null, target, useScaledDrawing, null, () =>
{ {
try try
{ {

8
src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs

@ -25,7 +25,7 @@ namespace Avalonia.Direct2D1
_target = null; _target = null;
} }
public IDrawingContextImpl CreateDrawingContext() public IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing)
{ {
if (_target == null) if (_target == null)
throw new ObjectDisposedException(nameof(FramebufferShimRenderTarget)); throw new ObjectDisposedException(nameof(FramebufferShimRenderTarget));
@ -37,7 +37,7 @@ namespace Avalonia.Direct2D1
} }
return new FramebufferShim(locked) return new FramebufferShim(locked)
.CreateDrawingContext(); .CreateDrawingContext(useScaledDrawing);
} }
public bool IsCorrupted => false; public bool IsCorrupted => false;
@ -52,9 +52,9 @@ namespace Avalonia.Direct2D1
_target = target; _target = target;
} }
public override IDrawingContextImpl CreateDrawingContext() public override IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing)
{ {
return base.CreateDrawingContext(() => return base.CreateDrawingContext(useScaledDrawing, () =>
{ {
using (var l = WicImpl.Lock(BitmapLockFlags.Read)) using (var l = WicImpl.Lock(BitmapLockFlags.Read))
{ {

58
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@ -39,6 +39,7 @@ namespace Avalonia.Direct2D1.Media
public DrawingContextImpl( public DrawingContextImpl(
ILayerFactory layerFactory, ILayerFactory layerFactory,
SharpDX.Direct2D1.RenderTarget renderTarget, SharpDX.Direct2D1.RenderTarget renderTarget,
bool useScaledDrawing,
SharpDX.DXGI.SwapChain1 swapChain = null, SharpDX.DXGI.SwapChain1 swapChain = null,
Action finishedCallback = null) Action finishedCallback = null)
{ {
@ -58,6 +59,13 @@ namespace Avalonia.Direct2D1.Media
_ownsDeviceContext = true; _ownsDeviceContext = true;
} }
if (!useScaledDrawing)
{
var scaling = _renderTarget.DotsPerInch.Width / 96;
if (!MathUtilities.AreClose(1, scaling))
_postTransform = Matrix.CreateScale(1 / scaling, 1 / scaling);
}
_deviceContext.BeginDraw(); _deviceContext.BeginDraw();
} }
@ -66,8 +74,13 @@ namespace Avalonia.Direct2D1.Media
/// </summary> /// </summary>
public Matrix Transform public Matrix Transform
{ {
get { return _deviceContext.Transform.ToAvalonia(); } get { return _transform; }
set { _deviceContext.Transform = value.ToDirect2D(); } set
{
_transform = value;
_deviceContext.Transform =
(_postTransform.HasValue ? value * _postTransform.Value : value).ToDirect2D();
}
} }
public Matrix4x4 Transform4x4 public Matrix4x4 Transform4x4
@ -353,6 +366,11 @@ namespace Avalonia.Direct2D1.Media
} }
} }
public void DrawRegion(IBrush brush, IPen pen, IPlatformRenderInterfaceRegion region)
{
throw new NotSupportedException();
}
/// <inheritdoc /> /// <inheritdoc />
public void DrawEllipse(IBrush brush, IPen pen, Rect rect) public void DrawEllipse(IBrush brush, IPen pen, Rect rect)
{ {
@ -410,17 +428,17 @@ namespace Avalonia.Direct2D1.Media
} }
} }
public IDrawingContextLayerImpl CreateLayer(Size size) public IDrawingContextLayerImpl CreateLayer(PixelSize pixelSize)
{ {
var dpi = new Vector(_deviceContext.DotsPerInch.Width, _deviceContext.DotsPerInch.Height);
if (_layerFactory != null) if (_layerFactory != null)
{ {
return _layerFactory.CreateLayer(size); return _layerFactory.CreateLayer(pixelSize.ToSizeWithDpi(dpi));
} }
else else
{ {
var platform = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>(); var platform = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
var dpi = new Vector(_deviceContext.DotsPerInch.Width, _deviceContext.DotsPerInch.Height);
var pixelSize = PixelSize.FromSizeWithDpi(size, dpi);
return (IDrawingContextLayerImpl)platform.CreateRenderTargetBitmap(pixelSize, dpi); return (IDrawingContextLayerImpl)platform.CreateRenderTargetBitmap(pixelSize, dpi);
} }
} }
@ -441,14 +459,40 @@ namespace Avalonia.Direct2D1.Media
_deviceContext.PushAxisAlignedClip(clip.Rect.ToDirect2D(), AntialiasMode.PerPrimitive); _deviceContext.PushAxisAlignedClip(clip.Rect.ToDirect2D(), AntialiasMode.PerPrimitive);
} }
public void PushClip(IPlatformRenderInterfaceRegion region)
{
throw new NotSupportedException();
}
public void PopClip() public void PopClip()
{ {
_deviceContext.PopAxisAlignedClip(); _deviceContext.PopAxisAlignedClip();
} }
public void PushLayer(Rect bounds)
{
var parameters = new LayerParameters
{
ContentBounds = bounds.ToDirect2D(),
MaskTransform = PrimitiveExtensions.Matrix3x2Identity,
Opacity = 1
};
var layer = _layerPool.Count != 0 ? _layerPool.Pop() : new Layer(_deviceContext);
_deviceContext.PushLayer(ref parameters, layer);
_layers.Push(layer);
}
void IDrawingContextImpl.PopLayer()
{
PopLayer();
}
readonly Stack<Layer> _layers = new Stack<Layer>(); readonly Stack<Layer> _layers = new Stack<Layer>();
private readonly Stack<Layer> _layerPool = new Stack<Layer>(); private readonly Stack<Layer> _layerPool = new Stack<Layer>();
private RenderOptions _renderOptions; private RenderOptions _renderOptions;
private readonly Matrix? _postTransform;
private Matrix _transform = Matrix.Identity;
/// <summary> /// <summary>
/// Pushes an opacity value. /// Pushes an opacity value.
@ -574,7 +618,7 @@ namespace Avalonia.Direct2D1.Media
CompatibleRenderTargetOptions.None, CompatibleRenderTargetOptions.None,
pixelSize.ToSizeWithDpi(dpi).ToSharpDX())) pixelSize.ToSizeWithDpi(dpi).ToSharpDX()))
{ {
using (var ctx = new RenderTarget(intermediate).CreateDrawingContext()) using (var ctx = new RenderTarget(intermediate).CreateDrawingContext(true))
{ {
intermediate.Clear(null); intermediate.Clear(null);
sceneBrushContent.Render(ctx, sceneBrushContent.Render(ctx,

2
src/Windows/Avalonia.Direct2D1/Media/ImageBrushImpl.cs

@ -98,7 +98,7 @@ namespace Avalonia.Direct2D1.Media
CompatibleRenderTargetOptions.None, CompatibleRenderTargetOptions.None,
calc.IntermediateSize.ToSharpDX()); calc.IntermediateSize.ToSharpDX());
using (var context = new RenderTarget(result).CreateDrawingContext()) using (var context = new RenderTarget(result).CreateDrawingContext(true))
{ {
var dpi = new Vector(target.DotsPerInch.Width, target.DotsPerInch.Height); var dpi = new Vector(target.DotsPerInch.Width, target.DotsPerInch.Height);
var rect = new Rect(bitmap.PixelSize.ToSizeWithDpi(dpi)); var rect = new Rect(bitmap.PixelSize.ToSizeWithDpi(dpi));

7
src/Windows/Avalonia.Direct2D1/Media/Imaging/D2DRenderTargetBitmapImpl.cs

@ -30,9 +30,10 @@ namespace Avalonia.Direct2D1.Media.Imaging
return new D2DRenderTargetBitmapImpl(bitmapRenderTarget); return new D2DRenderTargetBitmapImpl(bitmapRenderTarget);
} }
public IDrawingContextImpl CreateDrawingContext() public IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing)
{ {
return new DrawingContextImpl( this, _renderTarget, null, () => Version++); return new DrawingContextImpl( this, _renderTarget, useScaledDrawing,
null, () => Version++);
} }
public bool IsCorrupted => false; public bool IsCorrupted => false;
@ -60,7 +61,7 @@ namespace Avalonia.Direct2D1.Media.Imaging
{ {
using (var wic = new WicRenderTargetBitmapImpl(PixelSize, Dpi)) using (var wic = new WicRenderTargetBitmapImpl(PixelSize, Dpi))
{ {
using (var dc = wic.CreateDrawingContext(null)) using (var dc = wic.CreateDrawingContext(true, null))
{ {
dc.DrawBitmap( dc.DrawBitmap(
this, this,

8
src/Windows/Avalonia.Direct2D1/Media/Imaging/WicRenderTargetBitmapImpl.cs

@ -34,14 +34,14 @@ namespace Avalonia.Direct2D1.Media
base.Dispose(); base.Dispose();
} }
public virtual IDrawingContextImpl CreateDrawingContext() public virtual IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing)
=> CreateDrawingContext(null); => CreateDrawingContext(useScaledDrawing, null);
public bool IsCorrupted => false; public bool IsCorrupted => false;
public IDrawingContextImpl CreateDrawingContext(Action finishedCallback) public IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing, Action finishedCallback)
{ {
return new DrawingContextImpl(null, _renderTarget, finishedCallback: () => return new DrawingContextImpl(null, _renderTarget, useScaledDrawing, finishedCallback: () =>
{ {
Version++; Version++;
finishedCallback?.Invoke(); finishedCallback?.Invoke();

4
src/Windows/Avalonia.Direct2D1/RenderTarget.cs

@ -25,9 +25,9 @@ namespace Avalonia.Direct2D1
/// Creates a drawing context for a rendering session. /// Creates a drawing context for a rendering session.
/// </summary> /// </summary>
/// <returns>An <see cref="Avalonia.Platform.IDrawingContextImpl"/>.</returns> /// <returns>An <see cref="Avalonia.Platform.IDrawingContextImpl"/>.</returns>
public IDrawingContextImpl CreateDrawingContext() public IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing)
{ {
return new DrawingContextImpl(this, _renderTarget); return new DrawingContextImpl(this, _renderTarget, useScaledDrawing);
} }
public bool IsCorrupted => false; public bool IsCorrupted => false;

4
src/Windows/Avalonia.Direct2D1/SwapChainRenderTarget.cs

@ -19,7 +19,7 @@ namespace Avalonia.Direct2D1
/// Creates a drawing context for a rendering session. /// Creates a drawing context for a rendering session.
/// </summary> /// </summary>
/// <returns>An <see cref="Avalonia.Platform.IDrawingContextImpl"/>.</returns> /// <returns>An <see cref="Avalonia.Platform.IDrawingContextImpl"/>.</returns>
public IDrawingContextImpl CreateDrawingContext() public IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing)
{ {
var size = GetWindowSize(); var size = GetWindowSize();
var dpi = GetWindowDpi(); var dpi = GetWindowDpi();
@ -32,7 +32,7 @@ namespace Avalonia.Direct2D1
Resize(); Resize();
} }
return new DrawingContextImpl(this, _deviceContext, _swapChain); return new DrawingContextImpl(this, _deviceContext, useScaledDrawing, _swapChain);
} }
public bool IsCorrupted => false; public bool IsCorrupted => false;

2
tests/Avalonia.RenderTests/Media/BitmapTests.cs

@ -72,7 +72,7 @@ namespace Avalonia.Direct2D1.RenderTests.Media
var r = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>(); var r = AvaloniaLocator.Current.GetRequiredService<IPlatformRenderInterface>();
using(var cpuContext = r.CreateBackendContext(null)) using(var cpuContext = r.CreateBackendContext(null))
using (var target = cpuContext.CreateRenderTarget(new object[] { fb })) using (var target = cpuContext.CreateRenderTarget(new object[] { fb }))
using (var ctx = target.CreateDrawingContext()) using (var ctx = target.CreateDrawingContext(false))
{ {
ctx.Clear(Colors.Transparent); ctx.Clear(Colors.Transparent);
ctx.PushOpacity(0.8, new Rect(0, 0, 80, 80)); ctx.PushOpacity(0.8, new Rect(0, 0, 80, 80));

6
tests/Avalonia.UnitTests/TestRoot.cs

@ -79,16 +79,16 @@ namespace Avalonia.UnitTests
public IRenderTarget CreateRenderTarget() public IRenderTarget CreateRenderTarget()
{ {
var dc = new Mock<IDrawingContextImpl>(); var dc = new Mock<IDrawingContextImpl>();
dc.Setup(x => x.CreateLayer(It.IsAny<Size>())).Returns(() => dc.Setup(x => x.CreateLayer(It.IsAny<PixelSize>())).Returns(() =>
{ {
var layerDc = new Mock<IDrawingContextImpl>(); var layerDc = new Mock<IDrawingContextImpl>();
var layer = new Mock<IDrawingContextLayerImpl>(); var layer = new Mock<IDrawingContextLayerImpl>();
layer.Setup(x => x.CreateDrawingContext()).Returns(layerDc.Object); layer.Setup(x => x.CreateDrawingContext(It.IsAny<bool>())).Returns(layerDc.Object);
return layer.Object; return layer.Object;
}); });
var result = new Mock<IRenderTarget>(); var result = new Mock<IRenderTarget>();
result.Setup(x => x.CreateDrawingContext()).Returns(dc.Object); result.Setup(x => x.CreateDrawingContext(It.IsAny<bool>())).Returns(dc.Object);
return result.Object; return result.Object;
} }

Loading…
Cancel
Save