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