diff --git a/src/Uno.UI/UI/Xaml/Media/Imaging/RenderTargetBitmap.cs b/src/Uno.UI/UI/Xaml/Media/Imaging/RenderTargetBitmap.cs index fecfef657190..745016074829 100644 --- a/src/Uno.UI/UI/Xaml/Media/Imaging/RenderTargetBitmap.cs +++ b/src/Uno.UI/UI/Xaml/Media/Imaging/RenderTargetBitmap.cs @@ -14,6 +14,7 @@ using Uno.UI.Xaml.Media; using Buffer = Windows.Storage.Streams.Buffer; using System.Buffers; +using System.Runtime.InteropServices; using WinUICoreServices = Uno.UI.Xaml.Core.CoreServices; @@ -22,8 +23,84 @@ namespace Microsoft.UI.Xaml.Media.Imaging #if NOT_IMPLEMENTED [global::Uno.NotImplemented("IS_UNIT_TESTS", "__WASM__", "__NETSTD_REFERENCE__")] #endif - public partial class RenderTargetBitmap : ImageSource, IDisposable + public partial class RenderTargetBitmap : ImageSource { +#if !__ANDROID__ + // This is to avoid LOH array allocations + private unsafe class UnmanagedArrayOfBytes + { + public nint Pointer; + public int Length { get; } + + public UnmanagedArrayOfBytes(int length) + { + Length = length; + Pointer = Marshal.AllocHGlobal(length); + GC.AddMemoryPressure(length); + } + + public byte this[int index] + { + get + { + return ((byte*)Pointer.ToPointer())[index]; + } + set + { + ((byte*)Pointer.ToPointer())[index] = value; + } + } + + ~UnmanagedArrayOfBytes() + { + Marshal.FreeHGlobal(Pointer); + GC.RemoveMemoryPressure(Length); + } + } + + // https://stackoverflow.com/questions/52190423/c-sharp-access-unmanaged-array-using-memoryt-or-arraysegmentt + private sealed unsafe class UnmanagedMemoryManager : MemoryManager + where T : unmanaged + { + private readonly T* _pointer; + private readonly int _length; + + /// + /// Create a new UnmanagedMemoryManager instance at the given pointer and size + /// + public UnmanagedMemoryManager(T* pointer, int length) + { + if (length < 0) throw new ArgumentOutOfRangeException(nameof(length)); + _pointer = pointer; + _length = length; + } + /// + /// Obtains a span that represents the region + /// + public override Span GetSpan() => new Span(_pointer, _length); + + /// + /// Provides access to a pointer that represents the data (note: no actual pin occurs) + /// + public override MemoryHandle Pin(int elementIndex = 0) + { + if (elementIndex < 0 || elementIndex >= _length) + throw new ArgumentOutOfRangeException(nameof(elementIndex)); + return new MemoryHandle(_pointer + elementIndex); + } + + /// + /// Has no effect + /// + public override void Unpin() { } + + /// + /// Releases all resources associated with this object + /// + protected override void Dispose(bool disposing) { } + } +#endif + #if NOT_IMPLEMENTED internal const bool IsImplemented = false; #else @@ -65,7 +142,11 @@ public int PixelHeight } #endregion +#if !__ANDROID__ + private UnmanagedArrayOfBytes? _buffer; +#else private byte[]? _buffer; +#endif private int _bufferSize; /// @@ -74,19 +155,19 @@ private protected override bool TryOpenSourceSync(int? targetWidth, int? targetH var width = PixelWidth; var height = PixelHeight; - if (_buffer is null || _bufferSize <= 0 || width <= 0 || height <= 0) + if (_buffer is not { } buffer || _bufferSize <= 0 || width <= 0 || height <= 0) { image = default; return false; } - image = Open(_buffer, _bufferSize, width, height); + image = Open(buffer, _bufferSize, width, height); InvalidateImageSource(); return image.HasData; } #if NOT_IMPLEMENTED - private static ImageData Open(byte[] buffer, int bufferLength, int width, int height) + private static ImageData Open(UnmanagedArrayOfBytes buffer, int bufferLength, int width, int height) => default; #endif @@ -156,36 +237,42 @@ public IAsyncOperation GetPixelsAsync() { return Task.FromResult(new Buffer(Array.Empty())); } + +#if !__ANDROID__ + unsafe + { + var mem = new UnmanagedMemoryManager((byte*)_buffer.Pointer.ToPointer(), _bufferSize); + return Task.FromResult(new Buffer(mem.Memory.Slice(0, _bufferSize))); + } +#else return Task.FromResult(new Buffer(_buffer.AsMemory().Slice(0, _bufferSize))); +#endif }); #if NOT_IMPLEMENTED - private (int ByteCount, int Width, int Height) RenderAsBgra8_Premul(UIElement element, ref byte[]? buffer, Size? scaledSize = null) + private (int ByteCount, int Width, int Height) RenderAsBgra8_Premul(UIElement element, ref UnmanagedArrayOfBytes? buffer, Size? scaledSize = null) => throw new NotImplementedException("RenderTargetBitmap is not supported on this platform."); #endif - void IDisposable.Dispose() - { - if (_buffer is not null) - { - ArrayPool.Shared.Return(_buffer); - } - } - #region Misc static helpers #if !NOT_IMPLEMENTED +#if __ANDROID__ private static void EnsureBuffer(ref byte[]? buffer, int length) { - if (buffer is null) + if (buffer is null || buffer.Length < length) { - buffer = ArrayPool.Shared.Rent(length); + buffer = new byte[length]; } - else if (buffer.Length < length) + } +#else + private static void EnsureBuffer(ref UnmanagedArrayOfBytes? buffer, int length) + { + if (buffer is null || buffer.Length < length) { - ArrayPool.Shared.Return(buffer); - buffer = ArrayPool.Shared.Rent(length); + buffer = new UnmanagedArrayOfBytes(length); } } +#endif #endif #endregion } diff --git a/src/Uno.UI/UI/Xaml/Media/Imaging/RenderTargetBitmap.iOS.cs b/src/Uno.UI/UI/Xaml/Media/Imaging/RenderTargetBitmap.iOS.cs index cbcef9b5284b..cf009efb1187 100644 --- a/src/Uno.UI/UI/Xaml/Media/Imaging/RenderTargetBitmap.iOS.cs +++ b/src/Uno.UI/UI/Xaml/Media/Imaging/RenderTargetBitmap.iOS.cs @@ -23,11 +23,11 @@ partial class RenderTargetBitmap private protected override bool IsSourceReady => _buffer != null; /// - private static ImageData Open(byte[] buffer, int bufferLength, int width, int height) + private static ImageData Open(UnmanagedArrayOfBytes buffer, int bufferLength, int width, int height) { using var colorSpace = CGColorSpace.CreateDeviceRGB(); using var context = new CGBitmapContext( - buffer, + buffer.Pointer, width, height, _bitsPerComponent, @@ -44,7 +44,7 @@ private static ImageData Open(byte[] buffer, int bufferLength, int width, int he return default; } - private static (int ByteCount, int Width, int Height) RenderAsBgra8_Premul(UIElement element, ref byte[]? buffer, Size? scaledSize = null) + private static (int ByteCount, int Width, int Height) RenderAsBgra8_Premul(UIElement element, ref UnmanagedArrayOfBytes? buffer, Size? scaledSize = null) { var size = new Size(element.ActualSize.X, element.ActualSize.Y); if (size == default) @@ -85,7 +85,7 @@ private static (int ByteCount, int Width, int Height) RenderAsBgra8_Premul(UIEle using var colorSpace = CGColorSpace.CreateDeviceRGB(); using var context = new CGBitmapContext( - buffer, + buffer!.Pointer, width, height, _bitsPerComponent, diff --git a/src/Uno.UI/UI/Xaml/Media/Imaging/RenderTargetBitmap.macOS.cs b/src/Uno.UI/UI/Xaml/Media/Imaging/RenderTargetBitmap.macOS.cs index d1bf2dc5776d..28ed8615cb1c 100644 --- a/src/Uno.UI/UI/Xaml/Media/Imaging/RenderTargetBitmap.macOS.cs +++ b/src/Uno.UI/UI/Xaml/Media/Imaging/RenderTargetBitmap.macOS.cs @@ -21,11 +21,11 @@ partial class RenderTargetBitmap /// private protected override bool IsSourceReady => _buffer != null; - private static ImageData Open(byte[] buffer, int bufferLength, int width, int height) + private static ImageData Open(UnmanagedArrayOfBytes buffer, int bufferLength, int width, int height) { using var colorSpace = CGColorSpace.CreateDeviceRGB(); using var context = new CGBitmapContext( - buffer, + buffer.Pointer, width, height, _bitsPerComponent, @@ -42,7 +42,7 @@ private static ImageData Open(byte[] buffer, int bufferLength, int width, int he return default; } - private static (int ByteCount, int Width, int Height) RenderAsBgra8_Premul(UIElement element, ref byte[]? buffer, Size? scaledSize = null) + private static (int ByteCount, int Width, int Height) RenderAsBgra8_Premul(UIElement element, ref UnmanagedArrayOfBytes? buffer, Size? scaledSize = null) { var size = new Size(element.ActualSize.X, element.ActualSize.Y); @@ -87,7 +87,7 @@ private static (int ByteCount, int Width, int Height) RenderAsBgra8_Premul(UIEle using var colorSpace = CGColorSpace.CreateDeviceRGB(); using var context = new CGBitmapContext( - buffer, + buffer!.Pointer, width, height, _bitsPerComponent, diff --git a/src/Uno.UI/UI/Xaml/Media/Imaging/RenderTargetBitmap.skia.cs b/src/Uno.UI/UI/Xaml/Media/Imaging/RenderTargetBitmap.skia.cs index 1c6131d3f9fc..80281644a006 100644 --- a/src/Uno.UI/UI/Xaml/Media/Imaging/RenderTargetBitmap.skia.cs +++ b/src/Uno.UI/UI/Xaml/Media/Imaging/RenderTargetBitmap.skia.cs @@ -15,16 +15,15 @@ partial class RenderTargetBitmap private const int _bitsPerComponent = 8; private const int _bytesPerPixel = _bitsPerPixel / _bitsPerComponent; - private static ImageData Open(byte[] buffer, int bufferLength, int width, int height) + private static ImageData Open(UnmanagedArrayOfBytes buffer, int bufferLength, int width, int height) { - var bufferHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned); try { // Note: We use the FromPixelCopy which will create a clone of the buffer, so we are ready to be re-used to render another UIElement. // (It's needed also if we swapped the buffer since we are not maintaining a ref on the swappedBuffer) var bytesPerRow = width * _bytesPerPixel; var info = new SKImageInfo(width, height, SKColorType.Bgra8888, SKAlphaType.Premul); - var image = SKImage.FromPixelCopy(info, bufferHandle.AddrOfPinnedObject(), bytesPerRow); + var image = SKImage.FromPixelCopy(info, buffer.Pointer, bytesPerRow); return ImageData.FromCompositionSurface(new SkiaCompositionSurface(image)); } @@ -32,13 +31,9 @@ private static ImageData Open(byte[] buffer, int bufferLength, int width, int he { return ImageData.FromError(error); } - finally - { - bufferHandle.Free(); - } } - private static (int ByteCount, int Width, int Height) RenderAsBgra8_Premul(UIElement element, ref byte[]? buffer, Size? scaledSize = null) + private static (int ByteCount, int Width, int Height) RenderAsBgra8_Premul(UIElement element, ref UnmanagedArrayOfBytes? buffer, Size? scaledSize = null) { var renderSize = element.RenderSize; var visual = element.Visual; @@ -74,7 +69,10 @@ private static (int ByteCount, int Width, int Height) RenderAsBgra8_Premul(UIEle var byteCount = bitmap.ByteCount; EnsureBuffer(ref buffer, byteCount); - bitmap.GetPixelSpan().CopyTo(buffer); + unsafe + { + bitmap.GetPixelSpan().CopyTo(new Span(buffer!.Pointer.ToPointer(), byteCount)); + } bitmap?.Dispose(); return (byteCount, width, height); }