Browse Source
* Don't create a layer if the previous frame is retained by the render target * D2D * compile * Only check PreviousFrameIsRetained if not using layer * ABIpull/14938/head
committed by
GitHub
25 changed files with 547 additions and 130 deletions
@ -0,0 +1,30 @@ |
|||||
|
using Avalonia.Metadata; |
||||
|
|
||||
|
namespace Avalonia.Platform; |
||||
|
|
||||
|
[PrivateApi] |
||||
|
public struct RenderTargetProperties |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// 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
|
||||
|
/// </summary>
|
||||
|
public bool RetainsPreviousFrameContents { get; init; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// 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
|
||||
|
/// </summary>
|
||||
|
public bool IsSuitableForDirectRendering { get; init; } |
||||
|
} |
||||
|
|
||||
|
[PrivateApi] |
||||
|
public struct RenderTargetDrawingContextProperties |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Indicates that the drawing context targets a surface that preserved its contents since the previous frame
|
||||
|
/// </summary>
|
||||
|
public bool PreviousFrameIsRetained { get; init; } |
||||
|
} |
||||
@ -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<RetainedFramebuffer> 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; |
||||
|
} |
||||
|
} |
||||
@ -1,64 +0,0 @@ |
|||||
using System; |
|
||||
using System.IO; |
|
||||
using Avalonia.Platform; |
|
||||
using Avalonia.Platform.Internal; |
|
||||
using SkiaSharp; |
|
||||
using static Avalonia.X11.XLib; |
|
||||
namespace Avalonia.X11 |
|
||||
{ |
|
||||
internal class X11Framebuffer : ILockedFramebuffer |
|
||||
{ |
|
||||
private readonly IntPtr _display; |
|
||||
private readonly IntPtr _xid; |
|
||||
private readonly int _depth; |
|
||||
private UnmanagedBlob _blob; |
|
||||
|
|
||||
public X11Framebuffer(IntPtr display, IntPtr xid, int depth, int width, int height, double factor) |
|
||||
{ |
|
||||
// HACK! Please fix renderer, should never ask for 0x0 bitmap.
|
|
||||
width = Math.Max(1, width); |
|
||||
height = Math.Max(1, height); |
|
||||
|
|
||||
_display = display; |
|
||||
_xid = xid; |
|
||||
_depth = depth; |
|
||||
Size = new PixelSize(width, height); |
|
||||
RowBytes = width * 4; |
|
||||
Dpi = new Vector(96, 96) * factor; |
|
||||
Format = PixelFormat.Bgra8888; |
|
||||
_blob = new UnmanagedBlob(RowBytes * height); |
|
||||
Address = _blob.Address; |
|
||||
} |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
var image = new XImage(); |
|
||||
int bitsPerPixel = 32; |
|
||||
image.width = Size.Width; |
|
||||
image.height = Size.Height; |
|
||||
image.format = 2; //ZPixmap;
|
|
||||
image.data = Address; |
|
||||
image.byte_order = 0;// LSBFirst;
|
|
||||
image.bitmap_unit = bitsPerPixel; |
|
||||
image.bitmap_bit_order = 0;// LSBFirst;
|
|
||||
image.bitmap_pad = bitsPerPixel; |
|
||||
image.depth = _depth; |
|
||||
image.bytes_per_line = RowBytes; |
|
||||
image.bits_per_pixel = bitsPerPixel; |
|
||||
XLockDisplay(_display); |
|
||||
XInitImage(ref image); |
|
||||
var gc = XCreateGC(_display, _xid, 0, IntPtr.Zero); |
|
||||
XPutImage(_display, _xid, gc, ref image, 0, 0, 0, 0, (uint) Size.Width, (uint) Size.Height); |
|
||||
XFreeGC(_display, gc); |
|
||||
XSync(_display, true); |
|
||||
XUnlockDisplay(_display); |
|
||||
_blob.Dispose(); |
|
||||
} |
|
||||
|
|
||||
public IntPtr Address { get; } |
|
||||
public PixelSize Size { get; } |
|
||||
public int RowBytes { get; } |
|
||||
public Vector Dpi { get; } |
|
||||
public PixelFormat Format { get; } |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,28 @@ |
|||||
|
using Avalonia.Platform; |
||||
|
|
||||
|
namespace Avalonia.LinuxFramebuffer.Output; |
||||
|
|
||||
|
public class FbDevOutputOptions |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// The frame buffer device name.
|
||||
|
/// Defaults to the value in environment variable FRAMEBUFFER or /dev/fb0 when FRAMEBUFFER is not set
|
||||
|
/// </summary>
|
||||
|
public string? FileName { get; set; } |
||||
|
/// <summary>
|
||||
|
/// The required pixel format for the frame buffer.
|
||||
|
/// A null value will leave the frame buffer in the current pixel format.
|
||||
|
/// Otherwise sets the frame buffer to the required format
|
||||
|
/// </summary>
|
||||
|
public PixelFormat? PixelFormat { get; set; } |
||||
|
/// <summary>
|
||||
|
/// If set to true, double-buffering will be disabled and scene will be composed directly into mmap-ed memory region
|
||||
|
/// While this mode saves a blit, you need to check if it won't cause rendering artifacts your particular device.
|
||||
|
/// </summary>
|
||||
|
public bool RenderDirectlyToMappedMemory { get; set; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// The initial scale factor to use
|
||||
|
/// </summary>
|
||||
|
public double Scaling { get; set; } = 1; |
||||
|
} |
||||
@ -0,0 +1,135 @@ |
|||||
|
#if AVALONIA_SKIA
|
||||
|
using System; |
||||
|
using System.IO; |
||||
|
using System.Runtime.CompilerServices; |
||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Controls.Platform.Surfaces; |
||||
|
using Avalonia.Controls.Primitives; |
||||
|
using Avalonia.Controls.Shapes; |
||||
|
using Avalonia.Layout; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Platform; |
||||
|
using Avalonia.Rendering; |
||||
|
using Avalonia.Rendering.Composition; |
||||
|
using Avalonia.Threading; |
||||
|
using Avalonia.UnitTests; |
||||
|
using SkiaSharp; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Skia.RenderTests; |
||||
|
|
||||
|
public class DirectFbCompositionTests : TestBase |
||||
|
{ |
||||
|
public DirectFbCompositionTests() |
||||
|
: base(@"Composition\DirectFb") |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
class FuncFramebufferSurface : IFramebufferPlatformSurface |
||||
|
{ |
||||
|
private readonly Func<IFramebufferRenderTarget> _cb; |
||||
|
|
||||
|
public FuncFramebufferSurface(Func<IFramebufferRenderTarget> cb) |
||||
|
{ |
||||
|
_cb = cb; |
||||
|
} |
||||
|
|
||||
|
public IFramebufferRenderTarget CreateFramebufferRenderTarget() |
||||
|
{ |
||||
|
return _cb(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Theory, |
||||
|
InlineData(false), |
||||
|
InlineData(true)] |
||||
|
void Should_Only_Update_Clipped_Rects_When_Retained_Fb_Is_Advertised(bool advertised) |
||||
|
{ |
||||
|
var timer = new ManualRenderTimer(); |
||||
|
var compositor = new Compositor(new RenderLoop(timer), null, true, |
||||
|
new DispatcherCompositorScheduler(), true, Dispatcher.UIThread, new CompositionOptions |
||||
|
{ |
||||
|
UseRegionDirtyRectClipping = true |
||||
|
}); |
||||
|
|
||||
|
Rectangle r1, r2; |
||||
|
var control = new Canvas |
||||
|
{ |
||||
|
Width = 200, Height = 200, Background = Brushes.Yellow, |
||||
|
Children = |
||||
|
{ |
||||
|
(r1 = new Rectangle |
||||
|
{ |
||||
|
Fill = Brushes.Black, |
||||
|
Width = 40, |
||||
|
Height = 40, |
||||
|
Opacity = 0.6, |
||||
|
[Canvas.LeftProperty] = 40, |
||||
|
[Canvas.TopProperty] = 40, |
||||
|
}), |
||||
|
(r2 = new Rectangle |
||||
|
{ |
||||
|
Fill = Brushes.Black, |
||||
|
Width = 40, |
||||
|
Height = 40, |
||||
|
Opacity = 0.6, |
||||
|
[Canvas.LeftProperty] = 120, |
||||
|
[Canvas.TopProperty] = 40, |
||||
|
}), |
||||
|
} |
||||
|
}; |
||||
|
var root = new TestRenderRoot(1, null!); |
||||
|
SKBitmap fb = new SKBitmap(200, 200, SKColorType.Rgba8888, SKAlphaType.Premul); |
||||
|
|
||||
|
ILockedFramebuffer LockFb() => new LockedFramebuffer(fb.GetAddress(0, 0), new(fb.Width, fb.Height), |
||||
|
fb.RowBytes, new Vector(96, 96), PixelFormat.Rgba8888, null); |
||||
|
|
||||
|
bool previousFrameIsRetained = false; |
||||
|
IFramebufferRenderTarget rt = advertised |
||||
|
? new FuncRetainedFramebufferRenderTarget((out FramebufferLockProperties props) => |
||||
|
{ |
||||
|
props = new() { PreviousFrameIsRetained = previousFrameIsRetained }; |
||||
|
return LockFb(); |
||||
|
}) |
||||
|
: new FuncFramebufferRenderTarget(LockFb); |
||||
|
|
||||
|
using var renderer = |
||||
|
new CompositingRenderer(root, compositor, () => new[] { new FuncFramebufferSurface(() => rt) }); |
||||
|
root.Initialize(renderer, control); |
||||
|
control.Measure(new Size(control.Width, control.Height)); |
||||
|
control.Arrange(new Rect(control.DesiredSize)); |
||||
|
renderer.Start(); |
||||
|
Dispatcher.UIThread.RunJobs(); |
||||
|
timer.TriggerTick(); |
||||
|
var image1 = |
||||
|
$"{nameof(Should_Only_Update_Clipped_Rects_When_Retained_Fb_Is_Advertised)}_advertized-{advertised}_initial"; |
||||
|
SaveFile(fb, image1); |
||||
|
|
||||
|
fb.Erase(SKColor.Empty); |
||||
|
|
||||
|
previousFrameIsRetained = advertised; |
||||
|
|
||||
|
r1.Fill = Brushes.Red; |
||||
|
r2.Fill = Brushes.Green; |
||||
|
Dispatcher.UIThread.RunJobs(); |
||||
|
timer.TriggerTick(); |
||||
|
var image2 = |
||||
|
$"{nameof(Should_Only_Update_Clipped_Rects_When_Retained_Fb_Is_Advertised)}_advertized-{advertised}_updated"; |
||||
|
SaveFile(fb, image2); |
||||
|
CompareImages(image1, skipImmediate: true); |
||||
|
CompareImages(image2, skipImmediate: true); |
||||
|
|
||||
|
} |
||||
|
|
||||
|
void SaveFile(SKBitmap bmp, string name) |
||||
|
{ |
||||
|
Directory.CreateDirectory(OutputPath); |
||||
|
var path = System.IO.Path.Combine(OutputPath, name + ".composited.out.png"); |
||||
|
using var d = bmp.Encode(SKEncodedImageFormat.Png, 100); |
||||
|
using var f = File.Create(path); |
||||
|
d.SaveTo(f); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
#endif
|
||||
|
After Width: | Height: | Size: 633 B |
|
After Width: | Height: | Size: 639 B |
|
After Width: | Height: | Size: 633 B |
|
After Width: | Height: | Size: 358 B |
Loading…
Reference in new issue