Skip to content

Commit

Permalink
Merge pull request unoplatform#14959 from Youssef1313/rendertargetbit…
Browse files Browse the repository at this point in the history
…map-unmanagedmem

perf: Avoid LOH allocations in RenderTargetBitmap
  • Loading branch information
jeromelaban authored Feb 1, 2024
2 parents f744f51 + a937936 commit c69f07c
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 35 deletions.
123 changes: 105 additions & 18 deletions src/Uno.UI/UI/Xaml/Media/Imaging/RenderTargetBitmap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<T> : MemoryManager<T>
where T : unmanaged
{
private readonly T* _pointer;
private readonly int _length;

/// <summary>
/// Create a new UnmanagedMemoryManager instance at the given pointer and size
/// </summary>
public UnmanagedMemoryManager(T* pointer, int length)
{
if (length < 0) throw new ArgumentOutOfRangeException(nameof(length));
_pointer = pointer;
_length = length;
}
/// <summary>
/// Obtains a span that represents the region
/// </summary>
public override Span<T> GetSpan() => new Span<T>(_pointer, _length);

/// <summary>
/// Provides access to a pointer that represents the data (note: no actual pin occurs)
/// </summary>
public override MemoryHandle Pin(int elementIndex = 0)
{
if (elementIndex < 0 || elementIndex >= _length)
throw new ArgumentOutOfRangeException(nameof(elementIndex));
return new MemoryHandle(_pointer + elementIndex);
}

/// <summary>
/// Has no effect
/// </summary>
public override void Unpin() { }

/// <summary>
/// Releases all resources associated with this object
/// </summary>
protected override void Dispose(bool disposing) { }
}
#endif

#if NOT_IMPLEMENTED
internal const bool IsImplemented = false;
#else
Expand Down Expand Up @@ -65,7 +142,11 @@ public int PixelHeight
}
#endregion

#if !__ANDROID__
private UnmanagedArrayOfBytes? _buffer;
#else
private byte[]? _buffer;
#endif
private int _bufferSize;

/// <inheritdoc />
Expand All @@ -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

Expand Down Expand Up @@ -156,36 +237,42 @@ public IAsyncOperation<IBuffer> GetPixelsAsync()
{
return Task.FromResult<IBuffer>(new Buffer(Array.Empty<byte>()));
}

#if !__ANDROID__
unsafe
{
var mem = new UnmanagedMemoryManager<byte>((byte*)_buffer.Pointer.ToPointer(), _bufferSize);
return Task.FromResult<IBuffer>(new Buffer(mem.Memory.Slice(0, _bufferSize)));
}
#else
return Task.FromResult<IBuffer>(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<byte>.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<byte>.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<byte>.Shared.Return(buffer);
buffer = ArrayPool<byte>.Shared.Rent(length);
buffer = new UnmanagedArrayOfBytes(length);
}
}
#endif
#endif
#endregion
}
Expand Down
8 changes: 4 additions & 4 deletions src/Uno.UI/UI/Xaml/Media/Imaging/RenderTargetBitmap.iOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,11 @@ partial class RenderTargetBitmap
private protected override bool IsSourceReady => _buffer != null;

/// <inheritdoc />
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,
Expand All @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 4 additions & 4 deletions src/Uno.UI/UI/Xaml/Media/Imaging/RenderTargetBitmap.macOS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ partial class RenderTargetBitmap
/// <inheritdoc />
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,
Expand All @@ -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);

Expand Down Expand Up @@ -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,
Expand Down
16 changes: 7 additions & 9 deletions src/Uno.UI/UI/Xaml/Media/Imaging/RenderTargetBitmap.skia.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,30 +15,25 @@ 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));
}
catch (Exception error)
{
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;
Expand Down Expand Up @@ -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<byte>(buffer!.Pointer.ToPointer(), byteCount));
}
bitmap?.Dispose();
return (byteCount, width, height);
}
Expand Down

0 comments on commit c69f07c

Please sign in to comment.