From 3e61cdf10ba29fcc9e252023ede091595da153dc Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 6 Aug 2024 04:25:18 +0600 Subject: [PATCH] Added Compositor.CreateCompositionVisualSnapshot API (#16599) * Added Compositor.CreateCompositionVisualSnapshot API * Hurr durr api compat in [Unstable] interface --------- Co-authored-by: Max Katz #Conflicts: # api/Avalonia.Skia.nupkg.xml # samples/ControlCatalog/Pages/OpenGlPage.xaml # samples/ControlCatalog/Pages/OpenGlPage.xaml.cs --- api/Avalonia.Skia.nupkg.xml | 10 ++- .../Platform/IDrawingContextImpl.cs | 6 ++ .../Platform/IPlatformRenderInterface.cs | 8 ++ src/Avalonia.Base/Platform/IScopedResource.cs | 44 +++++++++++ .../Rendering/Composition/Compositor.cs | 45 ++++++++--- .../ServerCompositionContainerVisual.cs | 7 +- .../Server/ServerCompositionDrawListVisual.cs | 10 +-- ...verCompositionExperimentalAcrylicVisual.cs | 7 +- .../ServerCompositionSolidColorVisual.cs | 5 +- .../Server/ServerCompositionSurfaceVisual.cs | 5 +- .../ServerCompositionTarget.DirtyRects.cs | 2 +- .../Server/ServerCompositionTarget.cs | 5 +- .../Server/ServerCompositionVisual.cs | 22 +++--- .../Server/ServerCompositor.UserApis.cs | 40 ++++++++++ .../Composition/Server/ServerCompositor.cs | 25 +++++-- .../Server/ServerCustomCompositionVisual.cs | 9 +-- .../Server/ServerVisualRenderContext.cs | 75 +++++++++++++++++++ src/Avalonia.Controls/BorderVisual.cs | 6 +- .../HeadlessPlatformRenderInterface.cs | 3 + src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs | 1 + .../Avalonia.Skia/Gpu/Metal/SkiaMetalGpu.cs | 4 + .../Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs | 2 + src/Skia/Avalonia.Skia/SkiaBackendContext.cs | 22 ++++++ src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs | 11 ++- .../Avalonia.Direct2D1/Direct2D1Platform.cs | 4 + 25 files changed, 315 insertions(+), 63 deletions(-) create mode 100644 src/Avalonia.Base/Platform/IScopedResource.cs create mode 100644 src/Avalonia.Base/Rendering/Composition/Server/ServerVisualRenderContext.cs diff --git a/api/Avalonia.Skia.nupkg.xml b/api/Avalonia.Skia.nupkg.xml index a88fc8ba0a9..946658f26ec 100644 --- a/api/Avalonia.Skia.nupkg.xml +++ b/api/Avalonia.Skia.nupkg.xml @@ -1,10 +1,16 @@ - + + + CP0006 + M:Avalonia.Skia.ISkiaGpuWithPlatformGraphicsContext.TryGetGrContext + baseline/netstandard2.0/Avalonia.Skia.dll + target/netstandard2.0/Avalonia.Skia.dll + CP0006 M:Avalonia.Skia.ISkiaSharpApiLease.TryLeasePlatformGraphicsApi baseline/netstandard2.0/Avalonia.Skia.dll target/netstandard2.0/Avalonia.Skia.dll - \ No newline at end of file + diff --git a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs index 9621037cc10..476bca5a33e 100644 --- a/src/Avalonia.Base/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Base/Platform/IDrawingContextImpl.cs @@ -229,4 +229,10 @@ public interface IDrawingContextLayerImpl : IRenderTargetBitmapImpl /// bool CanBlit { get; } } + + public interface IDrawingContextLayerWithRenderContextAffinityImpl : IDrawingContextLayerImpl + { + bool HasRenderContextAffinity { get; } + IBitmapImpl CreateNonAffinedSnapshot(); + } } diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index 0ead242a271..30b426489ac 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -215,6 +215,14 @@ public interface IPlatformRenderInterfaceContext : IOptionalFeatureProvider, IDi /// /// An . IRenderTarget CreateRenderTarget(IEnumerable surfaces); + + /// + /// Creates an offscreen render target + /// + /// The size, in pixels, of the render target + /// The scaling which will be reported by IBitmap.Dpi + /// + IDrawingContextLayerImpl CreateOffscreenRenderTarget(PixelSize pixelSize, double scaling); /// /// Indicates that the context is no longer usable. This method should be thread-safe diff --git a/src/Avalonia.Base/Platform/IScopedResource.cs b/src/Avalonia.Base/Platform/IScopedResource.cs new file mode 100644 index 00000000000..616b2111824 --- /dev/null +++ b/src/Avalonia.Base/Platform/IScopedResource.cs @@ -0,0 +1,44 @@ +using System; +using System.Threading; + +namespace Avalonia.Platform; + +public interface IScopedResource : IDisposable +{ + public T Value { get; } +} + +public class ScopedResource : IScopedResource +{ + private int _disposed = 0; + private T _value; + private Action? _dispose; + private ScopedResource(T value, Action dispose) + { + _value = value; + _dispose = dispose; + } + + public static IScopedResource Create(T value, Action dispose) => new ScopedResource(value, dispose); + + public void Dispose() + { + if (Interlocked.CompareExchange(ref _disposed, 1, 0) == 0) + { + var disp = _dispose!; + _value = default!; + _dispose = null; + disp(); + } + } + + public T Value + { + get + { + if (_disposed == 1) + throw new ObjectDisposedException(this.GetType().FullName); + return _value; + } + } +} \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Compositor.cs b/src/Avalonia.Base/Rendering/Composition/Compositor.cs index 48553d3b91c..6b7a2ab081d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs @@ -5,6 +5,7 @@ using Avalonia.Animation.Easings; using Avalonia.Controls; using Avalonia.Media; +using Avalonia.Media.Imaging; using Avalonia.Metadata; using Avalonia.Platform; using Avalonia.Rendering.Composition.Server; @@ -34,6 +35,7 @@ public partial class Compositor private CompositionBatch? _pendingBatch; private readonly object _pendingBatchLock = new(); private readonly List _pendingServerCompositorJobs = new(); + private readonly List _pendingServerCompositorPostTargetJobs = new(); private DiagnosticTextRenderer? _diagnosticTextRenderer; private readonly Action _triggerCommitRequested; @@ -170,14 +172,23 @@ CompositionBatch CommitCore() _disposeOnNextBatch.Clear(); } - if (_pendingServerCompositorJobs.Count > 0) + + static void SerializeServerJobs(BatchStreamWriter writer, List list, object startMarker, object endMarker) { - writer.WriteObject(ServerCompositor.RenderThreadJobsStartMarker); - foreach (var job in _pendingServerCompositorJobs) - writer.WriteObject(job); - writer.WriteObject(ServerCompositor.RenderThreadJobsEndMarker); + if (list.Count > 0) + { + writer.WriteObject(startMarker); + foreach (var job in list) + writer.WriteObject(job); + writer.WriteObject(endMarker); + } + list.Clear(); } - _pendingServerCompositorJobs.Clear(); + + SerializeServerJobs(writer, _pendingServerCompositorJobs, ServerCompositor.RenderThreadJobsStartMarker, + ServerCompositor.RenderThreadJobsEndMarker); + SerializeServerJobs(writer, _pendingServerCompositorPostTargetJobs, ServerCompositor.RenderThreadPostTargetJobsStartMarker, + ServerCompositor.RenderThreadPostTargetJobsEndMarker); } _nextCommit.CommittedAt = Server.Clock.Elapsed; @@ -227,21 +238,21 @@ public void RequestCompositionUpdate(Action action) RequestCommitAsync(); } - internal void PostServerJob(Action job) + internal void PostServerJob(Action job, bool postTarget = false) { Dispatcher.VerifyAccess(); - _pendingServerCompositorJobs.Add(job); + (postTarget ? _pendingServerCompositorPostTargetJobs : _pendingServerCompositorJobs).Add(job); RequestCommitAsync(); } - internal Task InvokeServerJobAsync(Action job) => + internal Task InvokeServerJobAsync(Action job, bool postTarget = false) => InvokeServerJobAsync(() => { job(); return null; - }); + }, postTarget); - internal Task InvokeServerJobAsync(Func job) + internal Task InvokeServerJobAsync(Func job, bool postTarget = false) { var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); PostServerJob(() => @@ -254,7 +265,7 @@ internal Task InvokeServerJobAsync(Func job) { tcs.TrySetException(e); } - }); + }, postTarget); return tcs.Task; } @@ -275,6 +286,16 @@ internal ValueTask> GetRenderInterfacePublicFe (await GetRenderInterfacePublicFeatures().ConfigureAwait(false)).TryGetValue(featureType, out var rv); return rv; } + + public async Task CreateCompositionVisualSnapshot(CompositionVisual visual, double scaling) + { + if (visual.Compositor != this) + throw new InvalidOperationException(); + if (visual.Root == null) + throw new InvalidOperationException(); + var impl = await InvokeServerJobAsync(() => _server.CreateCompositionVisualSnapshot(visual.Server, scaling), true); + return new Bitmap(RefCountable.Create(impl)); + } /// /// Attempts to query for GPU interop feature from the platform render interface diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs index 6071d7a5f20..69d317fff15 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionContainerVisual.cs @@ -16,14 +16,13 @@ internal partial class ServerCompositionContainerVisual : ServerCompositionVisua private LtrbRect? _transformedContentBounds; private IImmutableEffect? _oldEffect; - protected override void RenderCore(CompositorDrawingContextProxy canvas, LtrbRect currentTransformedClip, - IDirtyRectTracker dirtyRects) + protected override void RenderCore(ServerVisualRenderContext context, LtrbRect currentTransformedClip) { - base.RenderCore(canvas, currentTransformedClip, dirtyRects); + base.RenderCore(context, currentTransformedClip); foreach (var ch in Children) { - ch.Render(canvas, currentTransformedClip, dirtyRects); + ch.Render(context, currentTransformedClip); } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs index 3adf0284385..ba9b042ad3d 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionDrawListVisual.cs @@ -40,17 +40,15 @@ protected override void DeserializeChangesCore(BatchStreamReader reader, TimeSpa base.DeserializeChangesCore(reader, committedAt); } - protected override void RenderCore(CompositorDrawingContextProxy canvas, LtrbRect currentTransformedClip, - IDirtyRectTracker dirtyRects) + protected override void RenderCore(ServerVisualRenderContext context, LtrbRect currentTransformedClip) { if (_renderCommands != null - && currentTransformedClip.Intersects(TransformedOwnContentBounds) - && dirtyRects.Intersects(TransformedOwnContentBounds)) + && context.ShouldRenderOwnContent(this, currentTransformedClip)) { - _renderCommands.Render(canvas); + _renderCommands.Render(context.Canvas); } - base.RenderCore(canvas, currentTransformedClip, dirtyRects); + base.RenderCore(context, currentTransformedClip); } public void DependencyQueuedInvalidate(IServerRenderResource sender) => ValuesInvalidated(); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs index 20acf87d84a..20a2501ca7e 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionExperimentalAcrylicVisual.cs @@ -5,18 +5,17 @@ namespace Avalonia.Rendering.Composition.Server; internal partial class ServerCompositionExperimentalAcrylicVisual { - protected override void RenderCore(CompositorDrawingContextProxy canvas, LtrbRect currentTransformedClip, - IDirtyRectTracker dirtyRects) + protected override void RenderCore(ServerVisualRenderContext context, LtrbRect currentTransformedClip) { var cornerRadius = CornerRadius; - canvas.DrawRectangle( + context.Canvas.DrawRectangle( Material, new RoundedRect( new Rect(0, 0, Size.X, Size.Y), cornerRadius.TopLeft, cornerRadius.TopRight, cornerRadius.BottomRight, cornerRadius.BottomLeft)); - base.RenderCore(canvas, currentTransformedClip, dirtyRects); + base.RenderCore(context, currentTransformedClip); } public override LtrbRect OwnContentBounds => new(0, 0, Size.X, Size.Y); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSolidColorVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSolidColorVisual.cs index e38ae57c570..a494692021c 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSolidColorVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSolidColorVisual.cs @@ -5,9 +5,8 @@ namespace Avalonia.Rendering.Composition.Server; internal partial class ServerCompositionSolidColorVisual { - protected override void RenderCore(CompositorDrawingContextProxy canvas, LtrbRect currentTransformedClip, - IDirtyRectTracker dirtyRects) + protected override void RenderCore(ServerVisualRenderContext context, LtrbRect currentTransformedClip) { - canvas.DrawRectangle(new ImmutableSolidColorBrush(Color), null, new Rect(0, 0, Size.X, Size.Y)); + context.Canvas.DrawRectangle(new ImmutableSolidColorBrush(Color), null, new Rect(0, 0, Size.X, Size.Y)); } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs index d4b1662bd93..28663ce3425 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionSurfaceVisual.cs @@ -5,8 +5,7 @@ namespace Avalonia.Rendering.Composition.Server; internal partial class ServerCompositionSurfaceVisual { - protected override void RenderCore(CompositorDrawingContextProxy canvas, LtrbRect currentTransformedClip, - IDirtyRectTracker dirtyRects) + protected override void RenderCore(ServerVisualRenderContext context, LtrbRect currentTransformedClip) { if (Surface == null) return; @@ -15,7 +14,7 @@ protected override void RenderCore(CompositorDrawingContextProxy canvas, LtrbRec var bmp = Surface.Bitmap.Item; //TODO: add a way to always render the whole bitmap instead of just assuming 96 DPI - canvas.DrawBitmap(Surface.Bitmap.Item, 1, new Rect(bmp.PixelSize.ToSize(1)), new Rect( + context.Canvas.DrawBitmap(Surface.Bitmap.Item, 1, new Rect(bmp.PixelSize.ToSize(1)), new Rect( new Size(Size.X, Size.Y))); } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.DirtyRects.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.DirtyRects.cs index d82502fc3c0..1b5cac520e6 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.DirtyRects.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.DirtyRects.cs @@ -35,7 +35,7 @@ public void AddDirtyRect(LtrbRect rect) public Rect SnapToDevicePixels(Rect rect) => SnapToDevicePixels(new(rect), Scaling).ToRect(); public LtrbRect SnapToDevicePixels(LtrbRect rect) => SnapToDevicePixels(rect, Scaling); - private static LtrbRect SnapToDevicePixels(LtrbRect rect, double scale) + public static LtrbRect SnapToDevicePixels(LtrbRect rect, double scale) { return new LtrbRect( Math.Floor(rect.Left * scale) / scale, diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index f2de59dda7a..b9bfb0b54a0 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -214,7 +214,10 @@ void RenderRootToContextWithClip(IDrawingContextImpl context, ServerCompositionV context.PushLayer(DirtyRects.CombinedRect.ToRectUnscaled()); using (var proxy = new CompositorDrawingContextProxy(context)) - root.Render(proxy, null, DirtyRects); + { + var ctx = new ServerVisualRenderContext(proxy, DirtyRects, false); + root.Render(ctx, null); + } if (useLayerClip) context.PopLayer(); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs index ccc4b658bee..54848d885d4 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs @@ -22,24 +22,22 @@ partial class ServerCompositionVisual : ServerObject private LtrbRect? _transformedClipBounds; private LtrbRect _combinedTransformedClipBounds; - protected virtual void RenderCore(CompositorDrawingContextProxy canvas, LtrbRect currentTransformedClip, - IDirtyRectTracker dirtyRects) + protected virtual void RenderCore(ServerVisualRenderContext canvas, LtrbRect currentTransformedClip) { } - public void Render(CompositorDrawingContextProxy canvas, LtrbRect? parentTransformedClip, IDirtyRectTracker dirtyRects) + public void Render(ServerVisualRenderContext context, LtrbRect? parentTransformedClip) { if (Visible == false || IsVisibleInFrame == false) return; if (Opacity == 0) return; - + var canvas = context.Canvas; + var currentTransformedClip = parentTransformedClip.HasValue ? parentTransformedClip.Value.Intersect(_combinedTransformedClipBounds) : _combinedTransformedClipBounds; - if (currentTransformedClip.IsZeroSize) - return; - if(!dirtyRects.Intersects(currentTransformedClip)) + if(!context.ShouldRender(this, currentTransformedClip)) return; Root!.RenderedVisuals++; @@ -49,12 +47,16 @@ public void Render(CompositorDrawingContextProxy canvas, LtrbRect? parentTransfo if (AdornedVisual != null) { + // Adorners are currently not supported in detached rendering mode + if(context.DetachedRendering) + return; + canvas.Transform = Matrix.Identity; if (AdornerIsClipped) canvas.PushClip(AdornedVisual._combinedTransformedClipBounds.ToRect()); } - var transform = GlobalTransformMatrix; - canvas.Transform = transform; + + using var _ = context.SetOrPushTransform(this); var applyRenderOptions = RenderOptions != default; @@ -72,7 +74,7 @@ public void Render(CompositorDrawingContextProxy canvas, LtrbRect? parentTransfo if (OpacityMaskBrush != null) canvas.PushOpacityMask(OpacityMaskBrush, boundsRect); - RenderCore(canvas, currentTransformedClip, dirtyRects); + RenderCore(context, currentTransformedClip); if (OpacityMaskBrush != null) canvas.PopOpacityMask(); diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs index bbdea64004d..7d203a7c478 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.UserApis.cs @@ -47,4 +47,44 @@ public IReadOnlyDictionary RT_GetRenderInterfaceFeatures() lock (_renderInterfaceFeaturesUserApiLock) return _renderInterfaceFeatureCache ??= RenderInterface.Value.PublicFeatures; } + + public IBitmapImpl CreateCompositionVisualSnapshot(ServerCompositionVisual visual, + double scaling) + { + using (RenderInterface.EnsureCurrent()) + { + var pixelSize = PixelSize.FromSize(new Size(visual.Size.X, visual.Size.Y), scaling); + + var scaleTransform = Matrix.CreateScale(scaling, scaling); + var invertRootTransform = visual.CombinedTransformMatrix.Invert(); + + IDrawingContextLayerImpl? target = null; + try + { + target = RenderInterface.Value.CreateOffscreenRenderTarget(pixelSize, scaling); + using (var canvas = target.CreateDrawingContext(false)) + { + var proxy = new CompositorDrawingContextProxy(canvas) + { + PostTransform = invertRootTransform * scaleTransform, + Transform = Matrix.Identity + }; + var ctx = new ServerVisualRenderContext(proxy, null, true); + visual.Render(ctx, null); + } + + if (target is IDrawingContextLayerWithRenderContextAffinityImpl affined) + return affined.CreateNonAffinedSnapshot(); + + // We are returning the original target, so prevent it from being disposed + var rv = target; + target = null; + return rv; + } + finally + { + target?.Dispose(); + } + } + } } \ No newline at end of file diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs index a471fc765ba..676ad5d7cc5 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs @@ -23,6 +23,7 @@ internal partial class ServerCompositor : IRenderLoopTask private readonly Queue _batches = new Queue(); private readonly Queue _receivedJobQueue = new(); + private readonly Queue _receivedPostTargetJobQueue = new(); public long LastBatchId { get; private set; } public Stopwatch Clock { get; } = Stopwatch.StartNew(); public TimeSpan ServerNow { get; private set; } @@ -38,6 +39,8 @@ internal partial class ServerCompositor : IRenderLoopTask internal static readonly object RenderThreadDisposeStartMarker = new(); internal static readonly object RenderThreadJobsStartMarker = new(); internal static readonly object RenderThreadJobsEndMarker = new(); + internal static readonly object RenderThreadPostTargetJobsStartMarker = new(); + internal static readonly object RenderThreadPostTargetJobsEndMarker = new(); public CompositionOptions Options { get; } public ServerCompositor(IRenderLoop renderLoop, IPlatformGraphics? platformGraphics, @@ -83,7 +86,12 @@ void ApplyPendingBatches() var readObject = stream.ReadObject(); if (readObject == RenderThreadJobsStartMarker) { - ReadServerJobs(stream); + ReadServerJobs(stream, _receivedJobQueue, RenderThreadJobsEndMarker); + continue; + } + if (readObject == RenderThreadPostTargetJobsStartMarker) + { + ReadServerJobs(stream, _receivedPostTargetJobQueue, RenderThreadPostTargetJobsEndMarker); continue; } @@ -111,11 +119,11 @@ void ApplyPendingBatches() } } - void ReadServerJobs(BatchStreamReader reader) + void ReadServerJobs(BatchStreamReader reader, Queue queue, object endMarker) { object? readObject; - while ((readObject = reader.ReadObject()) != RenderThreadJobsEndMarker) - _receivedJobQueue.Enqueue((Action)readObject!); + while ((readObject = reader.ReadObject()) != endMarker) + queue.Enqueue((Action)readObject!); } void ReadDisposeJobs(BatchStreamReader reader) @@ -128,12 +136,12 @@ void ReadDisposeJobs(BatchStreamReader reader) } } - void ExecuteServerJobs() + void ExecuteServerJobs(Queue queue) { - while(_receivedJobQueue.Count > 0) + while(queue.Count > 0) try { - _receivedJobQueue.Dequeue()(); + queue.Dequeue()(); } catch { @@ -224,9 +232,10 @@ private void RenderCore(bool catchExceptions) try { RenderInterface.EnsureValidBackendContext(); - ExecuteServerJobs(); + ExecuteServerJobs(_receivedJobQueue); foreach (var t in _activeTargets) t.Render(); + ExecuteServerJobs(_receivedPostTargetJobQueue); } catch (Exception e) when(RT_OnContextLostExceptionFilterObserver(e) && catchExceptions) { diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs index cfcffb00f0a..e37c142de0f 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCustomCompositionVisual.cs @@ -71,11 +71,10 @@ internal void HandlerRegisterForNextAnimationFrameUpdate() Compositor.AddToClock(this); } - protected override void RenderCore(CompositorDrawingContextProxy canvas, LtrbRect currentTransformedClip, - IDirtyRectTracker dirtyRects) + protected override void RenderCore(ServerVisualRenderContext ctx, LtrbRect currentTransformedClip) { - canvas.AutoFlush = true; - using var context = new ImmediateDrawingContext(canvas, GlobalTransformMatrix, false); + ctx.Canvas.AutoFlush = true; + using var context = new ImmediateDrawingContext(ctx.Canvas, GlobalTransformMatrix, false); try { _handler.Render(context, currentTransformedClip.ToRect()); @@ -86,6 +85,6 @@ protected override void RenderCore(CompositorDrawingContextProxy canvas, LtrbRec ?.Log(_handler, $"Exception in {_handler.GetType().Name}.{nameof(CompositionCustomVisualHandler.OnRender)} {{0}}", e); } - canvas.AutoFlush = false; + ctx.Canvas.AutoFlush = false; } } diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerVisualRenderContext.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisualRenderContext.cs new file mode 100644 index 00000000000..0778a7a6213 --- /dev/null +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerVisualRenderContext.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using Avalonia.Platform; + +namespace Avalonia.Rendering.Composition.Server; + +internal class ServerVisualRenderContext +{ + public IDirtyRectTracker? DirtyRects { get; } + public bool DetachedRendering { get; } + public CompositorDrawingContextProxy Canvas { get; } + private readonly Stack? _transformStack; + + public ServerVisualRenderContext(CompositorDrawingContextProxy canvas, IDirtyRectTracker? dirtyRects, + bool detachedRendering) + { + Canvas = canvas; + DirtyRects = dirtyRects; + DetachedRendering = detachedRendering; + if (detachedRendering) + { + _transformStack = new(); + _transformStack.Push(canvas.Transform); + } + } + + + public bool ShouldRender(ServerCompositionVisual visual, LtrbRect currentTransformedClip) + { + if (DetachedRendering) + return true; + if (currentTransformedClip.IsZeroSize) + return false; + if (DirtyRects?.Intersects(currentTransformedClip) == false) + return false; + return true; + } + + public bool ShouldRenderOwnContent(ServerCompositionVisual visual, LtrbRect currentTransformedClip) + { + if (DetachedRendering) + return true; + return currentTransformedClip.Intersects(visual.TransformedOwnContentBounds) + && DirtyRects?.Intersects(visual.TransformedOwnContentBounds) != false; + } + + public RestoreTransform SetOrPushTransform(ServerCompositionVisual visual) + { + if (!DetachedRendering) + { + Canvas.Transform = visual.GlobalTransformMatrix; + return default; + } + else + { + var transform = visual.CombinedTransformMatrix * _transformStack!.Peek(); + Canvas.Transform = transform; + _transformStack.Push(transform); + return new RestoreTransform(this); + } + } + + public struct RestoreTransform(ServerVisualRenderContext? parent) : IDisposable + { + public void Dispose() + { + if (parent != null) + { + parent._transformStack!.Pop(); + parent.Canvas.Transform = parent._transformStack.Peek(); + } + } + } + +} \ No newline at end of file diff --git a/src/Avalonia.Controls/BorderVisual.cs b/src/Avalonia.Controls/BorderVisual.cs index 36a557d5ede..3046e7e8751 100644 --- a/src/Avalonia.Controls/BorderVisual.cs +++ b/src/Avalonia.Controls/BorderVisual.cs @@ -46,9 +46,9 @@ public ServerBorderVisual(ServerCompositor compositor, Visual v) : base(composit { } - protected override void RenderCore(CompositorDrawingContextProxy canvas, LtrbRect currentTransformedClip, - IDirtyRectTracker dirtyRects) + protected override void RenderCore(ServerVisualRenderContext ctx, LtrbRect currentTransformedClip) { + var canvas = ctx.Canvas; if (ClipToBounds) { var clipRect = Root!.SnapToDevicePixels(new Rect(new Size(Size.X, Size.Y))); @@ -58,7 +58,7 @@ protected override void RenderCore(CompositorDrawingContextProxy canvas, LtrbRec canvas.PushClip(new RoundedRect(clipRect, _cornerRadius)); } - base.RenderCore(canvas, currentTransformedClip, dirtyRects); + base.RenderCore(ctx, currentTransformedClip); if(ClipToBounds) canvas.PopClip(); diff --git a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs index 7c7eea1c6f6..d018ef491fa 100644 --- a/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs +++ b/src/Headless/Avalonia.Headless/HeadlessPlatformRenderInterface.cs @@ -61,6 +61,9 @@ public IGeometryImpl CreateCombinedGeometry(GeometryCombineMode combineMode, IGe => new HeadlessGeometryStub(g1.Bounds.Union(g2.Bounds)); public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => new HeadlessRenderTarget(); + public IDrawingContextLayerImpl CreateOffscreenRenderTarget(PixelSize pixelSize, double scaling) => + new HeadlessBitmapStub(pixelSize, new Vector(96 * scaling, 96 * scaling)); + public bool IsLost => false; public IReadOnlyDictionary PublicFeatures { get; } = new Dictionary(); public object? TryGetFeature(Type featureType) => null; diff --git a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs index 20e7adb334e..85a2cbdd22b 100644 --- a/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/ISkiaGpu.cs @@ -31,6 +31,7 @@ public interface ISkiaGpu : IPlatformGraphicsContext public interface ISkiaGpuWithPlatformGraphicsContext : ISkiaGpu { IPlatformGraphicsContext? PlatformGraphicsContext { get; } + IScopedResource? TryGetGrContext(); } public interface ISkiaSurface : IDisposable diff --git a/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalGpu.cs b/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalGpu.cs index 8e5573a4657..bfd6e1aa59d 100644 --- a/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/Metal/SkiaMetalGpu.cs @@ -34,6 +34,10 @@ public void Dispose() public IDisposable EnsureCurrent() => _device.EnsureCurrent(); public IPlatformGraphicsContext? PlatformGraphicsContext => _device; + public IScopedResource TryGetGrContext() => + ScopedResource.Create(_context ?? throw new ObjectDisposedException(nameof(SkiaMetalApi)), + EnsureCurrent().Dispose); + public ISkiaGpuRenderTarget? TryCreateRenderTarget(IEnumerable surfaces) { foreach (var surface in surfaces) diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs index 00752f8b2b4..988fd4e7e2f 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs @@ -154,6 +154,8 @@ public void Dispose() public bool IsLost => _glContext.IsLost; public IDisposable EnsureCurrent() => _glContext.EnsureCurrent(); public IPlatformGraphicsContext? PlatformGraphicsContext => _glContext; + public IScopedResource TryGetGrContext() => + ScopedResource.Create(GrContext, EnsureCurrent().Dispose); public object? TryGetFeature(Type featureType) { diff --git a/src/Skia/Avalonia.Skia/SkiaBackendContext.cs b/src/Skia/Avalonia.Skia/SkiaBackendContext.cs index 555c564f4aa..db50c1e424e 100644 --- a/src/Skia/Avalonia.Skia/SkiaBackendContext.cs +++ b/src/Skia/Avalonia.Skia/SkiaBackendContext.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.Platform.Surfaces; using Avalonia.OpenGL; using Avalonia.Platform; +using SkiaSharp; namespace Avalonia.Skia; @@ -60,6 +61,27 @@ public IRenderTarget CreateRenderTarget(IEnumerable surfaces) "Don't know how to create a Skia render target from any of provided surfaces"); } + public IDrawingContextLayerImpl CreateOffscreenRenderTarget(PixelSize pixelSize, double scaling) + { + using (var gr = (_gpu as ISkiaGpuWithPlatformGraphicsContext)?.TryGetGrContext()) + { + var createInfo = new SurfaceRenderTarget.CreateInfo + { + Width = pixelSize.Width, + Height = pixelSize.Height, + Dpi = new Vector(96 * scaling, 96 * scaling), + Format = null, + DisableTextLcdRendering = false, + GrContext = gr?.Value, + Gpu = _gpu, + DisableManualFbo = true, + Session = null + }; + + return new SurfaceRenderTarget(createInfo); + } + } + public bool IsLost => _gpu?.IsLost ?? false; public IReadOnlyDictionary PublicFeatures { get; } diff --git a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs index 9b5d104aad0..27e36d99b33 100644 --- a/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/SurfaceRenderTarget.cs @@ -11,7 +11,7 @@ namespace Avalonia.Skia /// /// Skia render target that writes to a surface. /// - internal class SurfaceRenderTarget : IDrawingContextLayerImpl, IDrawableBitmapImpl + internal class SurfaceRenderTarget : IDrawingContextLayerImpl, IDrawableBitmapImpl, IDrawingContextLayerWithRenderContextAffinityImpl { private readonly ISkiaSurface _surface; private readonly SKCanvas _canvas; @@ -234,5 +234,14 @@ public struct CreateInfo public bool DisableManualFbo; } + + public bool HasRenderContextAffinity => _grContext != null; + public IBitmapImpl CreateNonAffinedSnapshot() + { + if (!HasRenderContextAffinity) + throw new InvalidOperationException(); + using var image = SnapshotImage(); + return new ImmutableBitmap(image.ToRasterImage(true)); + } } } diff --git a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs index 1b609af5d93..e3aa990f14e 100644 --- a/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs +++ b/src/Windows/Avalonia.Direct2D1/Direct2D1Platform.cs @@ -179,6 +179,10 @@ public void Dispose() } public IRenderTarget CreateRenderTarget(IEnumerable surfaces) => _platform.CreateRenderTarget(surfaces); + + public IDrawingContextLayerImpl CreateOffscreenRenderTarget(PixelSize pixelSize, double scaling) => + new WicRenderTargetBitmapImpl(pixelSize, new Vector(96 * scaling, 96 * scaling)); + public bool IsLost => false; public IReadOnlyDictionary PublicFeatures { get; } = new Dictionary(); }