Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ partial class CompositionGeometricClip
return null;
}

private static readonly SKPath _spareTransformedPath = new();

internal override SKPath? GetClipPath(Visual visual)
{
if (Geometry is not null)
Expand All @@ -39,7 +41,8 @@ partial class CompositionGeometricClip
var path = geometrySource.Geometry;
if (!TransformMatrix.IsIdentity)
{
var transformedPath = new SKPath();
var transformedPath = _spareTransformedPath;
transformedPath.Rewind();
path.Transform(TransformMatrix.ToSKMatrix(), transformedPath);
path = transformedPath;
}
Expand Down
21 changes: 13 additions & 8 deletions src/Uno.UI.Composition/Composition/RectangleClip.skia.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#nullable enable

using System;
using System.Numerics;
using SkiaSharp;
using Uno.Extensions;
Expand All @@ -9,9 +10,8 @@ namespace Microsoft.UI.Composition;

partial class RectangleClip
{
private static readonly SKPoint[] _radiiStore = new SKPoint[4];

private SKRoundRect? _skRoundRect;
private static readonly SKPath _spareClipPath = new();

private protected override Rect? GetBoundsCore(Visual visual)
{
Expand All @@ -24,7 +24,8 @@ partial class RectangleClip

internal override SKPath GetClipPath(Visual visual)
{
var path = new SKPath();
var path = _spareClipPath;
path.Rewind();
path.AddRoundRect(GetClipRoundedRect(visual));
return path;
}
Expand All @@ -50,12 +51,16 @@ _bottomLeftRadius.X is 0 && _bottomLeftRadius.Y is 0 &&
{
_skRoundRect ??= new SKRoundRect();

_radiiStore[0] = new SKPoint(_topLeftRadius.X, _topLeftRadius.Y);
_radiiStore[1] = new SKPoint(_topRightRadius.X, _topRightRadius.Y);
_radiiStore[2] = new SKPoint(_bottomRightRadius.X, _bottomRightRadius.Y);
_radiiStore[3] = new SKPoint(_bottomLeftRadius.X, _bottomLeftRadius.Y);
Span<SKPoint> radii = stackalloc SKPoint[]
{
new SKPoint(_topLeftRadius.X, _topLeftRadius.Y),
new SKPoint(_topRightRadius.X, _topRightRadius.Y),
new SKPoint(_bottomRightRadius.X, _bottomRightRadius.Y),
new SKPoint(_bottomLeftRadius.X, _bottomLeftRadius.Y),
};

_skRoundRect.SetRectRadii(bounds.ToSKRect(), radii);

_skRoundRect.SetRectRadii(bounds.ToSKRect(), _radiiStore);
return _skRoundRect;
}
else
Expand Down
9 changes: 9 additions & 0 deletions src/Uno.UI.Composition/Composition/Uno/UnoSkiaApi.skia.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,21 @@ private static IntPtr DllImportResolver(string libraryName, Assembly assembly, D
return IntPtr.Zero;
}

[DllImport(SKIA, CallingConvention = CallingConvention.Cdecl)]
internal static extern unsafe void sk_canvas_draw_picture(IntPtr canvas, IntPtr picture, SKMatrix* matrix, IntPtr paint);

[DllImport(SKIA, CallingConvention = CallingConvention.Cdecl)]
internal static extern void sk_canvas_draw_text_blob(IntPtr canvas, IntPtr textBlob, float x, float y, IntPtr paint);

[DllImport(SKIA, CallingConvention = CallingConvention.Cdecl)]
internal static extern unsafe void sk_canvas_set_matrix(IntPtr canvas, SKMatrix44* matrix);

[DllImport(SKIA, CallingConvention = CallingConvention.Cdecl)]
internal static extern IntPtr sk_picture_recorder_end_recording(IntPtr recorder);

[DllImport(SKIA, CallingConvention = CallingConvention.Cdecl)]
internal static extern void sk_refcnt_safe_unref(IntPtr refcnt);

[DllImport(SKIA, CallingConvention = CallingConvention.Cdecl)]
internal static extern unsafe void sk_rrect_set_rect_radii(IntPtr rrect, SKRect* rect, SKPoint* radii);

Expand Down
86 changes: 62 additions & 24 deletions src/Uno.UI.Composition/Composition/Visual.skia.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,14 @@
private int _pictureCollapsingOptimizationFrameThreshold;
private int _pictureCollapsingOptimizationVisualCountThreshold;

// Since painting (and recording) is done on the UI thread, we need a single SKPictureRecorder per UI thread.
// If we move to a UI-thread-per-window model, then we need multiple recorders.
[ThreadStatic]
private static SKPictureRecorder? _recorder;
private static SKPictureRecorder _recorder = new();

private CompositionClip? _clip;
private Vector2 _anchorPoint = Vector2.Zero; // Backing for scroll offsets
private int _zIndex;
private (Matrix4x4 matrix, bool isLocalMatrixIdentity) _totalMatrix = (Matrix4x4.Identity, true);
private SKPicture? _picture;
private SKPicture? _childrenPicture;
private IntPtr _picture;
private IntPtr _childrenPicture;
private int _framesSinceSubtreeNotChanged;

private VisualFlags _flags = VisualFlags.MatrixDirty | VisualFlags.PaintDirty | VisualFlags.ChildrenSKPictureInvalid;
Expand Down Expand Up @@ -207,8 +204,11 @@
/// </remarks>
internal void InvalidatePaint()
{
_picture?.Dispose();
_picture = null;
if (_picture != IntPtr.Zero)
{
UnoSkiaApi.sk_refcnt_safe_unref(_picture);
_picture = IntPtr.Zero;
}
_flags |= VisualFlags.PaintDirty;
InvalidateParentChildrenPicture(false);
}
Expand All @@ -218,8 +218,11 @@
var parent = includeSelf ? this : Parent;
while (parent is not null && (parent._flags & VisualFlags.ChildrenSKPictureInvalid) == 0)
{
parent._childrenPicture?.Dispose();
parent._childrenPicture = null;
if (parent._childrenPicture != IntPtr.Zero)
{
UnoSkiaApi.sk_refcnt_safe_unref(parent._childrenPicture);
parent._childrenPicture = IntPtr.Zero;
}
parent._flags |= VisualFlags.ChildrenSKPictureInvalid;
parent = parent.Parent;
}
Expand Down Expand Up @@ -376,9 +379,16 @@
PostPaintingClipStep(this, recordingCanvas);
RenderChildrenStep(this, childSession, applyChildOptimization);
}
var childrenPicture = recorder.EndRecording();
canvas.DrawPicture(childrenPicture, ShadowState.ShadowOnlyPaint);
canvas.DrawPicture(childrenPicture);

unsafe
{
var childrenPicture = UnoSkiaApi.sk_picture_recorder_end_recording(recorder.Handle);

UnoSkiaApi.sk_canvas_draw_picture(canvas.Handle, childrenPicture, null, ShadowState.ShadowOnlyPaint.Handle);
UnoSkiaApi.sk_canvas_draw_picture(canvas.Handle, childrenPicture, null, IntPtr.Zero);

UnoSkiaApi.sk_refcnt_safe_unref(childrenPicture);
}
}
}

Expand All @@ -399,17 +409,28 @@
if ((visual._flags & VisualFlags.PaintDirty) != 0)
{
visual._flags &= ~VisualFlags.PaintDirty;
_recorder ??= new SKPictureRecorder();

var recordingCanvas = _recorder.BeginRecording(new SKRect(-999999, -999999, 999999, 999999));
_factory.CreateInstance(visual, recordingCanvas, ref session.RootTransform, session.Opacity, out var recorderSession);
// To debug what exactly gets repainted, replace the following line with `Paint(in session);`
visual.Paint(in recorderSession);
visual._picture = _recorder.EndRecording();

var picture = UnoSkiaApi.sk_picture_recorder_end_recording(_recorder.Handle);

if (visual._picture != IntPtr.Zero)
{
UnoSkiaApi.sk_refcnt_safe_unref(visual._picture);
}

visual._picture = picture;
}

if (visual._picture is not null)
if (visual._picture != IntPtr.Zero)
{
session.Canvas.DrawPicture(visual._picture);
unsafe
{
UnoSkiaApi.sk_canvas_draw_picture(session.Canvas.Handle, visual._picture, null, IntPtr.Zero);
}
}
}
#if DEBUG
Expand Down Expand Up @@ -437,9 +458,12 @@

static void RenderChildrenStep(Visual visual, PaintingSession session, bool applyChildOptimization)
{
if (visual._childrenPicture is not null)
if (visual._childrenPicture != IntPtr.Zero)
{
session.Canvas.DrawPicture(visual._childrenPicture);
unsafe
{
UnoSkiaApi.sk_canvas_draw_picture(session.Canvas.Handle, visual._childrenPicture, null, IntPtr.Zero);
}
}
else if (!visual._enablePictureCollapsingOptimization
|| visual._framesSinceSubtreeNotChanged < visual._pictureCollapsingOptimizationFrameThreshold
Expand All @@ -466,22 +490,36 @@
}
}

var picture = recorder.EndRecording();
session.Canvas.DrawPicture(picture);
var picture = IntPtr.Zero;

Check warning on line 493 in src/Uno.UI.Composition/Composition/Visual.skia.cs

View check run for this annotation

Codacy Production / Codacy Static Code Analysis

src/Uno.UI.Composition/Composition/Visual.skia.cs#L493

Remove this useless assignment to local variable 'picture'.

unsafe
{
picture = UnoSkiaApi.sk_picture_recorder_end_recording(recorder.Handle);
UnoSkiaApi.sk_canvas_draw_picture(session.Canvas.Handle, picture, null, IntPtr.Zero);
}

// The visual can be set on a ChildrenSKPictureInvalid path after the render has started.
// In such case, we should not cache this picture. Not only it is outdated, it will also lead to a corrupted state,
// where subtree rendering is skipped with the cached picture,
// and its descendant can't invalidate the cached picture since they area already on a ChildrenSKPictureInvalid path.
if ((visual._flags & VisualFlags.ChildrenSKPictureInvalid) == 0)
{
if (visual._childrenPicture != IntPtr.Zero)
{
UnoSkiaApi.sk_refcnt_safe_unref(visual._childrenPicture);
}

visual._childrenPicture = picture;
}
else
{
UnoSkiaApi.sk_refcnt_safe_unref(picture);
}
}
}
}

internal void GetNativeViewPath(SKPath clipFromParent, SKPath outPath)
internal void GetNativeViewPath(SKPath clipFromParent, SKPath clipPath)
{
if (this is { Opacity: 0 } or { IsVisible: false } || clipFromParent.IsEmpty)
{
Expand All @@ -505,7 +543,7 @@

if (IsNativeHostVisual || CanPaint())
{
outPath.Op(localClipCombinedByClipFromParent, IsNativeHostVisual ? SKPathOp.Union : SKPathOp.Difference, outPath);
clipPath.Op(localClipCombinedByClipFromParent, IsNativeHostVisual ? SKPathOp.Union : SKPathOp.Difference, clipPath);
}

if (GetPostPaintingClipping() is { } postClip)
Expand All @@ -515,7 +553,7 @@
}
foreach (var child in GetChildrenInRenderOrder())
{
child.GetNativeViewPath(localClipCombinedByClipFromParent, outPath);
child.GetNativeViewPath(localClipCombinedByClipFromParent, clipPath);
}
}

Expand Down
4 changes: 1 addition & 3 deletions src/Uno.UI.Dispatching/Native/NativeDispatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -426,7 +426,7 @@ internal UIAsyncOperation EnqueueIdleOperation(Action<NativeDispatcher> handler)

private void EnqueueCore(Delegate handler, NativeDispatcherPriority priority)
{
Debug.Assert((int)priority >= 0 && (int)priority <= 4);
Debug.Assert((int)priority >= 0 && (int)priority <= 3);

bool shouldEnqueue;

Expand Down Expand Up @@ -493,8 +493,6 @@ private static EventActivity LogScheduleEvent(Action handler, NativeDispatcherPr
/// </summary>
internal static NativeDispatcher Main { get; } = new NativeDispatcher();

// Dispatching for the CompositionTarget.Rendering event

public static class TraceProvider
{
public readonly static Guid Id = Guid.Parse("{EA0762E9-8208-4501-B4A5-CC7ECF7BE85E}");
Expand Down
2 changes: 1 addition & 1 deletion src/Uno.UI.Runtime.Skia.Win32/Hosting/Win32Host.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ protected override Task RunLoop()
// This will keep running until the event loop has no queued actions left and all the windows are closed
while (true)
{
Win32EventLoop.RunOnce(TimeSpan.FromSeconds(1));
Win32EventLoop.RunOnce();

if (_allWindowsClosed && !Win32EventLoop.HasMessages())
{
Expand Down
51 changes: 6 additions & 45 deletions src/Uno.UI.Runtime.Skia.Win32/Native/Win32EventLoop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -104,62 +104,23 @@ public static bool HasMessages()
=> PInvoke.PeekMessage(out var msg, HWND.Null, 0, 0, PEEK_MESSAGE_REMOVE_TYPE.PM_NOREMOVE);

/// <summary>
/// By default, uses PeekMessage to get a message if available and does nothing if
/// the message pump is empty. If the timeout is set, blocks with GetMessage for
/// the duration of the timeout and then exits if there are no messages.
/// By default, uses PeekMessage to get input messages if available and does nothing if
/// the message pump is empty. Otherwise, blocks with GetMessage.
/// </summary>
public static void RunOnce(TimeSpan? timeout = null)
public static void RunOnce()
{
// We need to prioritize input messages in some cases like wheel
// scrolling where we don't want to wait for the queue to be empty
// before continuing to scroll.
if (PInvoke.PeekMessage(out var msg, HWND.Null, 0, 0, PEEK_MESSAGE_REMOVE_TYPE.PM_REMOVE | PEEK_MESSAGE_REMOVE_TYPE.PM_QS_INPUT)
|| PInvoke.PeekMessage(out msg, HWND.Null, 0, 0, PEEK_MESSAGE_REMOVE_TYPE.PM_REMOVE))
|| PInvoke.GetMessage(out msg, HWND.Null, 0, 0).Value != -1)
{
PInvoke.TranslateMessage(msg);
PInvoke.DispatchMessage(msg);
}
else if (timeout.HasValue)
else
{
// In the common case, we never hit this path as the dispatcher runs inside the message pump and
// will continue to have messages. This only hits when the app is idle and we need to stop
// spamming PeekMessage. Instead, we use GetMessage but we make sure to unblock it every
// second to see if we should continue or not, otherwise the GetMessage call could be stuck forever.
var cts = new CancellationTokenSource();
_ = Task.Run(async () =>
{
try
{
for (var i = 0; i < 10; i++)
{
await Task.Delay(timeout.Value / 10);
if (cts.IsCancellationRequested)
{
return;
}
}
// This sends an UnoWin32DispatcherMsg and unblocks the GetMessage call.
if (!cts.IsCancellationRequested)
{
NativeDispatcher.Main.Enqueue(() => { });
}
}
catch (TaskCanceledException)
{
// No need to unblock anything.
}
}, cts.Token);

if (PInvoke.GetMessage(out var msg2, HWND.Null, 0, 0).Value != -1)
{
cts.Cancel();
PInvoke.TranslateMessage(msg2);
PInvoke.DispatchMessage(msg2);
}
else
{
typeof(Win32EventLoop).LogError()?.Error($"{nameof(PInvoke.GetMessage)} failed: {Win32Helper.GetErrorMessage()}");
}
typeof(Win32EventLoop).LogError()?.Error($"{nameof(PInvoke.GetMessage)} failed: {Win32Helper.GetErrorMessage()}");
}
}
}
Expand Down
1 change: 0 additions & 1 deletion src/Uno.UI.Runtime.Skia.X11/Hosting/X11XamlRootHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ internal partial class X11XamlRootHost : IXamlRootHost
private X11Window? _x11Window;
private X11Window? _x11TopWindow;
private X11Renderer? _renderer;
private readonly SKPictureRecorder _recorder = new SKPictureRecorder();

private static readonly Stopwatch _stopwatch = Stopwatch.StartNew();

Expand Down
Loading
Loading