diff --git a/samples/ControlCatalog.NetCore/Program.cs b/samples/ControlCatalog.NetCore/Program.cs
index fa5b78c278..b3372ee0f8 100644
--- a/samples/ControlCatalog.NetCore/Program.cs
+++ b/samples/ControlCatalog.NetCore/Program.cs
@@ -9,6 +9,7 @@ using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Fonts.Inter;
using Avalonia.Headless;
+using Avalonia.LinuxFramebuffer.Output;
using Avalonia.LogicalTree;
using Avalonia.Rendering.Composition;
using Avalonia.Threading;
@@ -52,8 +53,11 @@ namespace ControlCatalog.NetCore
}
if (s_useFramebuffer)
{
- SilenceConsole();
- return builder.StartLinuxFbDev(args, scaling: GetScaling());
+ SilenceConsole();
+ return builder.StartLinuxFbDev(args, new FbDevOutputOptions()
+ {
+ Scaling = GetScaling()
+ });
}
else if (args.Contains("--vnc"))
{
diff --git a/src/Avalonia.Base/Platform/IRenderTarget.cs b/src/Avalonia.Base/Platform/IRenderTarget.cs
index a1f33db6c7..39504ac9fb 100644
--- a/src/Avalonia.Base/Platform/IRenderTarget.cs
+++ b/src/Avalonia.Base/Platform/IRenderTarget.cs
@@ -24,4 +24,31 @@ namespace Avalonia.Platform
///
public bool IsCorrupted { get; }
}
+
+ [PrivateApi]
+ public interface IRenderTargetWithProperties : IRenderTarget
+ {
+ RenderTargetProperties Properties { get; }
+
+ ///
+ /// Creates an for a rendering session.
+ ///
+ /// Apply DPI reported by the render target as a hidden transform matrix
+ /// Returns various properties about the returned drawing context
+ IDrawingContextImpl CreateDrawingContext(bool useScaledDrawing, out RenderTargetDrawingContextProperties properties);
+ }
+
+ internal static class RenderTargetExtensions
+ {
+ public static IDrawingContextImpl CreateDrawingContextWithProperties(
+ this IRenderTarget renderTarget,
+ bool useScaledDrawing,
+ out RenderTargetDrawingContextProperties properties)
+ {
+ if (renderTarget is IRenderTargetWithProperties target)
+ return target.CreateDrawingContext(useScaledDrawing, out properties);
+ properties = default;
+ return renderTarget.CreateDrawingContext(useScaledDrawing);
+ }
+ }
}
diff --git a/src/Avalonia.Base/Platform/RenderTargetProperties.cs b/src/Avalonia.Base/Platform/RenderTargetProperties.cs
new file mode 100644
index 0000000000..33b117ff1e
--- /dev/null
+++ b/src/Avalonia.Base/Platform/RenderTargetProperties.cs
@@ -0,0 +1,30 @@
+using Avalonia.Metadata;
+
+namespace Avalonia.Platform;
+
+[PrivateApi]
+public struct RenderTargetProperties
+{
+ ///
+ /// Indicates that render target contents are preserved between CreateDrawingContext calls.
+ /// Notable examples are retained CPU-memory framebuffers and
+ /// swapchains with DXGI_SWAP_EFFECT_SEQUENTIAL/DXGI_SWAP_EFFECT_FLIP_SEQUENTIAL
+ ///
+ public bool RetainsPreviousFrameContents { get; init; }
+
+ ///
+ /// Indicates that the render target can be used without CreateLayer
+ /// It's currently not true for every render target, since with OpenGL rendering we often use
+ /// framebuffers without a stencil attachment that is required for clipping with Skia
+ ///
+ public bool IsSuitableForDirectRendering { get; init; }
+}
+
+[PrivateApi]
+public struct RenderTargetDrawingContextProperties
+{
+ ///
+ /// Indicates that the drawing context targets a surface that preserved its contents since the previous frame
+ ///
+ public bool PreviousFrameIsRetained { get; init; }
+}
\ No newline at end of file
diff --git a/src/Avalonia.Base/Platform/RetainedFramebuffer.cs b/src/Avalonia.Base/Platform/RetainedFramebuffer.cs
new file mode 100644
index 0000000000..e5ca1070dc
--- /dev/null
+++ b/src/Avalonia.Base/Platform/RetainedFramebuffer.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Runtime.InteropServices;
+using Avalonia.Metadata;
+using Avalonia.Platform.Internal;
+
+namespace Avalonia.Platform;
+
+internal class RetainedFramebuffer : IDisposable
+{
+ public PixelSize Size { get; }
+ public int RowBytes { get; }
+ public PixelFormat Format { get; }
+ public IntPtr Address => _blob?.Address ?? throw new ObjectDisposedException(nameof(RetainedFramebuffer));
+ private UnmanagedBlob? _blob;
+
+ static PixelFormat ValidateKnownFormat(PixelFormat format) => format.BitsPerPixel % 8 == 0
+ ? format
+ : throw new ArgumentOutOfRangeException(nameof(format));
+
+ public RetainedFramebuffer(PixelSize size, PixelFormat format) : this(size, ValidateKnownFormat(format),
+ format.BitsPerPixel / 8 * size.Width)
+ {
+
+ }
+
+ public RetainedFramebuffer(PixelSize size, PixelFormat format, int rowBytes)
+ {
+ if (size.Width <= 0 || size.Height <= 0)
+ throw new ArgumentOutOfRangeException(nameof(size));
+ if (size.Width * (format.BitsPerPixel / 8) > rowBytes)
+ throw new ArgumentOutOfRangeException(nameof(rowBytes));
+ Size = size;
+ RowBytes = rowBytes;
+ Format = format;
+ _blob = new UnmanagedBlob(RowBytes * size.Height);
+ }
+
+ public ILockedFramebuffer Lock(Vector dpi, Action blit)
+ {
+ if (_blob == null)
+ throw new ObjectDisposedException(nameof(RetainedFramebuffer));
+ return new LockedFramebuffer(_blob.Address, Size, RowBytes, dpi, Format, () =>
+ {
+ blit(this);
+ GC.KeepAlive(this);
+ });
+ }
+
+ public void Dispose()
+ {
+ _blob?.Dispose();
+ _blob = null;
+ }
+}
\ 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 a3a8e0e97a..48553d3b91 100644
--- a/src/Avalonia.Base/Rendering/Composition/Compositor.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Compositor.cs
@@ -77,14 +77,15 @@ namespace Avalonia.Rendering.Composition
internal Compositor(IRenderLoop loop, IPlatformGraphics? gpu,
bool useUiThreadForSynchronousCommits,
ICompositorScheduler scheduler, bool reclaimBuffersImmediately,
- Dispatcher dispatcher)
+ Dispatcher dispatcher, CompositionOptions? options = null)
{
+ options ??= AvaloniaLocator.Current.GetService() ?? new();
Loop = loop;
UseUiThreadForSynchronousCommits = useUiThreadForSynchronousCommits;
Dispatcher = dispatcher;
_batchMemoryPool = new(reclaimBuffersImmediately);
_batchObjectPool = new(reclaimBuffersImmediately);
- _server = new ServerCompositor(loop, gpu, _batchObjectPool, _batchMemoryPool);
+ _server = new ServerCompositor(loop, gpu, options, _batchObjectPool, _batchMemoryPool);
_triggerCommitRequested = () => scheduler.CommitRequested(this);
DefaultEasing = new SplineEasing(new KeySpline(0.25, 0.1, 0.25, 1.0));
diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
index e39ffc5e7f..8d374cb15e 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
@@ -30,6 +30,7 @@ namespace Avalonia.Rendering.Composition.Server
private IDrawingContextLayerImpl? _layer;
private bool _updateRequested;
private bool _redrawRequested;
+ private bool _fullRedrawRequested;
private bool _disposed;
private readonly HashSet _attachedVisuals = new();
private readonly Queue _adornerUpdateQueue = new();
@@ -116,6 +117,7 @@ namespace Avalonia.Rendering.Composition.Server
partial void DeserializeChangesExtra(BatchStreamReader c)
{
_redrawRequested = true;
+ _fullRedrawRequested = true;
}
public void Render()
@@ -175,46 +177,56 @@ namespace Avalonia.Rendering.Composition.Server
if (!_redrawRequested)
return;
_redrawRequested = false;
- using (var targetContext = _renderTarget.CreateDrawingContext(false))
+
+ var renderTargetWithProperties = _renderTarget as IRenderTargetWithProperties;
+
+
+ var needLayer = DebugOverlays != RendererDebugOverlays.None // Check if we don't need overlays
+ // Check if render target can be rendered to directly and preserves the previous frame
+ || !(renderTargetWithProperties?.Properties.RetainsPreviousFrameContents == true
+ && renderTargetWithProperties?.Properties.IsSuitableForDirectRendering == true);
+
+ using (var renderTargetContext = _renderTarget.CreateDrawingContextWithProperties(false, out var properties))
{
- if (PixelSize != _layerSize || _layer == null || _layer.IsCorrupted)
+ if(needLayer && (PixelSize != _layerSize || _layer == null || _layer.IsCorrupted))
{
_layer?.Dispose();
_layer = null;
- _layer = targetContext.CreateLayer(PixelSize);
+ _layer = renderTargetContext.CreateLayer(PixelSize);
_layerSize = PixelSize;
DirtyRects.AddRect(new PixelRect(_layerSize));
}
+ else if (!needLayer)
+ {
+ _layer?.Dispose();
+ _layer = null;
+ }
+
+ if (_fullRedrawRequested || (!needLayer && !properties.PreviousFrameIsRetained))
+ {
+ DirtyRects.AddRect(new PixelRect(_layerSize));
+ _fullRedrawRequested = false;
+ }
if (!DirtyRects.IsEmpty)
{
- var useLayerClip = Compositor.Options.UseSaveLayerRootClip ??
- Compositor.RenderInterface.GpuContext != null;
- using (var context = _layer.CreateDrawingContext(false))
+ if (_layer != null)
{
- using (DirtyRects.BeginDraw(context))
+ using (var context = _layer.CreateDrawingContext(false))
+ RenderRootToContextWithClip(context, Root);
+
+ renderTargetContext.Clear(Colors.Transparent);
+ renderTargetContext.Transform = Matrix.Identity;
+ if (_layer.CanBlit)
+ _layer.Blit(renderTargetContext);
+ else
{
- context.Clear(Colors.Transparent);
- if (useLayerClip)
- context.PushLayer(DirtyRects.CombinedRect.ToRect(1));
-
-
- Root.Render(new CompositorDrawingContextProxy(context), null, DirtyRects);
-
- if (useLayerClip)
- context.PopLayer();
+ var rect = new PixelRect(default, PixelSize).ToRect(1);
+ renderTargetContext.DrawBitmap(_layer, 1, rect, rect);
}
}
- }
-
- targetContext.Clear(Colors.Transparent);
- targetContext.Transform = Matrix.Identity;
- if (_layer.CanBlit)
- _layer.Blit(targetContext);
- else
- {
- var rect = new PixelRect(default, PixelSize).ToRect(1);
- targetContext.DrawBitmap(_layer, 1, rect, rect);
+ else
+ RenderRootToContextWithClip(renderTargetContext, Root);
}
if (DebugOverlays != RendererDebugOverlays.None)
@@ -225,7 +237,7 @@ namespace Avalonia.Rendering.Composition.Server
RenderTimeGraph?.AddFrameValue(elapsed.TotalMilliseconds);
}
- DrawOverlays(targetContext, PixelSize.ToSize(Scaling));
+ DrawOverlays(renderTargetContext, PixelSize.ToSize(Scaling));
}
RenderedVisuals = 0;
@@ -234,6 +246,26 @@ namespace Avalonia.Rendering.Composition.Server
}
}
+ void RenderRootToContextWithClip(IDrawingContextImpl context, ServerCompositionVisual root)
+ {
+ var useLayerClip = Compositor.Options.UseSaveLayerRootClip ??
+ Compositor.RenderInterface.GpuContext != null;
+
+ using (DirtyRects.BeginDraw(context))
+ {
+ context.Clear(Colors.Transparent);
+ if (useLayerClip)
+ context.PushLayer(DirtyRects.CombinedRect.ToRect(1));
+
+
+ root.Render(new CompositorDrawingContextProxy(context), null, DirtyRects);
+
+ if (useLayerClip)
+ context.PopLayer();
+ }
+ }
+
+
private void DrawOverlays(IDrawingContextImpl targetContext, Size logicalSize)
{
if ((DebugOverlays & RendererDebugOverlays.DirtyRects) != 0)
diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs
index c9f4474654..a471fc765b 100644
--- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs
+++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositor.cs
@@ -38,11 +38,13 @@ namespace Avalonia.Rendering.Composition.Server
internal static readonly object RenderThreadDisposeStartMarker = new();
internal static readonly object RenderThreadJobsStartMarker = new();
internal static readonly object RenderThreadJobsEndMarker = new();
- public CompositionOptions Options { get; } = AvaloniaLocator.Current.GetService() ?? new();
+ public CompositionOptions Options { get; }
public ServerCompositor(IRenderLoop renderLoop, IPlatformGraphics? platformGraphics,
+ CompositionOptions options,
BatchStreamObjectPool