Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Image.WrapMemory<TPixel> APIs wrapping Memory<byte> #1314

Merged
merged 15 commits into from
Sep 1, 2020
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions src/ImageSharp/Image.WrapMemory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,5 +150,70 @@ public static Image<TPixel> WrapMemory<TPixel>(
int height)
where TPixel : unmanaged, IPixel<TPixel>
=> WrapMemory(Configuration.Default, pixelMemoryOwner, width, height);

/// <summary>
/// Wraps an existing contiguous memory area of 'width' x 'height' pixels,
/// allowing to view/manipulate it as an ImageSharp <see cref="Image{TPixel}"/> instance.
/// </summary>
/// <typeparam name="TPixel">The pixel type</typeparam>
/// <param name="configuration">The <see cref="Configuration"/></param>
/// <param name="byteMemory">The byte memory representing the pixel data.</param>
/// <param name="width">The width of the memory image.</param>
/// <param name="height">The height of the memory image.</param>
/// <param name="metadata">The <see cref="ImageMetadata"/>.</param>
/// <exception cref="ArgumentNullException">The configuration is null.</exception>
/// <exception cref="ArgumentNullException">The metadata is null.</exception>
/// <returns>An <see cref="Image{TPixel}"/> instance</returns>
public static Image<TPixel> WrapMemory<TPixel>(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://docs.microsoft.com/en-us/dotnet/standard/memory-and-spans/memory-t-usage-guidelines

Seems beneficial to consider exposing IMemoryOwner<T> overloads of this to allow ownership transfer?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't include them here on purpose as they weren't part of the initial API proposal in #1097.
@antonfirsov should this be added here or would that be for another PR?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No preference, other than it's always easier to review multiple smaller (subsequent) PR-s.

Copy link
Member Author

@Sergio0694 Sergio0694 Aug 13, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds like a preference to me 😄
Will keep this PR restricted to consumed buffers then, and we can create another one after this one is merged to also add support for owned ones through IMemoryOwner<byte>.

Configuration configuration,
Memory<byte> byteMemory,
int width,
int height,
Sergio0694 marked this conversation as resolved.
Show resolved Hide resolved
ImageMetadata metadata)
where TPixel : unmanaged, IPixel<TPixel>
{
Guard.NotNull(configuration, nameof(configuration));
Guard.NotNull(metadata, nameof(metadata));

var memoryManager = new ByteMemoryManager<TPixel>(byteMemory);
var memorySource = MemoryGroup<TPixel>.Wrap(memoryManager.Memory);
return new Image<TPixel>(configuration, memorySource, width, height, metadata);
}

/// <summary>
/// Wraps an existing contiguous memory area of 'width' x 'height' pixels,
/// allowing to view/manipulate it as an ImageSharp <see cref="Image{TPixel}"/> instance.
/// </summary>
/// <typeparam name="TPixel">The pixel type</typeparam>
/// <param name="configuration">The <see cref="Configuration"/></param>
/// <param name="byteMemory">The byte memory representing the pixel data.</param>
/// <param name="width">The width of the memory image.</param>
/// <param name="height">The height of the memory image.</param>
/// <exception cref="ArgumentNullException">The configuration is null.</exception>
/// <returns>An <see cref="Image{TPixel}"/> instance.</returns>
public static Image<TPixel> WrapMemory<TPixel>(
Configuration configuration,
Memory<byte> byteMemory,
int width,
int height)
where TPixel : unmanaged, IPixel<TPixel>
=> WrapMemory<TPixel>(configuration, byteMemory, width, height, new ImageMetadata());

/// <summary>
/// Wraps an existing contiguous memory area of 'width' x 'height' pixels,
/// allowing to view/manipulate it as an ImageSharp <see cref="Image{TPixel}"/> instance.
Sergio0694 marked this conversation as resolved.
Show resolved Hide resolved
/// The memory is being observed, the caller remains responsible for managing it's lifecycle.
/// </summary>
/// <typeparam name="TPixel">The pixel type.</typeparam>
/// <param name="byteMemory">The byte memory representing the pixel data.</param>
/// <param name="width">The width of the memory image.</param>
/// <param name="height">The height of the memory image.</param>
/// <returns>An <see cref="Image{TPixel}"/> instance.</returns>
public static Image<TPixel> WrapMemory<TPixel>(
Memory<byte> byteMemory,
int width,
int height)
where TPixel : unmanaged, IPixel<TPixel>
=> WrapMemory<TPixel>(Configuration.Default, byteMemory, width, height);
}
}
57 changes: 57 additions & 0 deletions src/ImageSharp/Memory/ByteMemoryManager{T}.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace SixLabors.ImageSharp.Memory
{
/// <summary>
/// A custom <see cref="MemoryManager{T}"/> that can wrap <see cref="Memory{T}"/> of <see cref="byte"/> instances
/// and cast them to be <see cref="Memory{T}"/> for any arbitrary unmanaged <typeparamref name="T"/> value type.
/// </summary>
/// <typeparam name="T">The value type to use when casting the wrapped <see cref="Memory{T}"/> instance.</typeparam>
internal sealed class ByteMemoryManager<T> : MemoryManager<T>
where T : unmanaged
{
/// <summary>
/// The wrapped <see cref="Memory{T}"/> of <see cref="byte"/> instance.
/// </summary>
private readonly Memory<byte> memory;

/// <summary>
/// Initializes a new instance of the <see cref="ByteMemoryManager{T}"/> class.
/// </summary>
/// <param name="memory">The <see cref="Memory{T}"/> of <see cref="byte"/> instance to wrap.</param>
public ByteMemoryManager(Memory<byte> memory)
{
this.memory = memory;
}

/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
}

/// <inheritdoc/>
public override Span<T> GetSpan()
{
Sergio0694 marked this conversation as resolved.
Show resolved Hide resolved
return MemoryMarshal.Cast<byte, T>(this.memory.Span);
}

/// <inheritdoc/>
public override MemoryHandle Pin(int elementIndex = 0)
{
// We need to adjust the offset into the wrapped byte segment,
// as the input index refers to the target-cast memory of T.
// We just have to shift this index by the byte size of T.
return this.memory.Slice(elementIndex * Unsafe.SizeOf<T>()).Pin();
}

/// <inheritdoc/>
public override void Unpin()
{
}
}
}
111 changes: 110 additions & 1 deletion tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Common.Helpers;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
using Xunit;
Expand Down Expand Up @@ -80,6 +80,48 @@ public override void Unpin()
}
}

public sealed class CastMemoryManager<TFrom, TTo> : MemoryManager<TTo>
where TFrom : unmanaged
where TTo : unmanaged
{
private readonly Memory<TFrom> memory;

public CastMemoryManager(Memory<TFrom> memory)
{
this.memory = memory;
}

/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
}

/// <inheritdoc/>
public override Span<TTo> GetSpan()
{
return MemoryMarshal.Cast<TFrom, TTo>(this.memory.Span);
}

/// <inheritdoc/>
public override MemoryHandle Pin(int elementIndex = 0)
{
int byteOffset = elementIndex * Unsafe.SizeOf<TTo>();
int shiftedOffset = Math.DivRem(byteOffset, Unsafe.SizeOf<TFrom>(), out int remainder);

if (remainder != 0)
{
ThrowHelper.ThrowArgumentException("The input index doesn't result in an aligned item access", nameof(elementIndex));
}

return this.memory.Slice(shiftedOffset).Pin();
}

/// <inheritdoc/>
public override void Unpin()
{
}
}

[Fact]
public void WrapMemory_CreatedImageIsCorrect()
{
Expand Down Expand Up @@ -173,6 +215,73 @@ public void WrapSystemDrawingBitmap_WhenOwned()
}
}

[Fact]
public void WrapMemory_FromBytes_CreatedImageIsCorrect()
{
Configuration cfg = Configuration.Default.Clone();
Sergio0694 marked this conversation as resolved.
Show resolved Hide resolved
var metaData = new ImageMetadata();

var array = new byte[25 * Unsafe.SizeOf<Rgba32>()];
var memory = new Memory<byte>(array);

using (var image = Image.WrapMemory<Rgba32>(cfg, memory, 5, 5, metaData))
{
Assert.True(image.TryGetSinglePixelSpan(out Span<Rgba32> imageSpan));
ref Rgba32 pixel0 = ref imageSpan[0];
Assert.True(Unsafe.AreSame(ref Unsafe.As<byte, Rgba32>(ref array[0]), ref pixel0));

Assert.Equal(cfg, image.GetConfiguration());
Assert.Equal(metaData, image.Metadata);
}
}

[Fact]
public void WrapSystemDrawingBitmap_FromBytes_WhenObserved()
{
if (ShouldSkipBitmapTest)
{
return;
}

using (var bmp = new Bitmap(51, 23))
{
using (var memoryManager = new BitmapMemoryManager(bmp))
{
Memory<Bgra32> pixelMemory = memoryManager.Memory;
Memory<byte> byteMemory = new CastMemoryManager<Bgra32, byte>(pixelMemory).Memory;
Bgra32 bg = Color.Red;
Bgra32 fg = Color.Green;

using (var image = Image.WrapMemory<Bgra32>(byteMemory, bmp.Width, bmp.Height))
{
Span<Bgra32> pixelSpan = pixelMemory.Span;
Span<Bgra32> imageSpan = image.GetRootFramePixelBuffer().GetSingleMemory().Span;

// We can't compare the two Memory<T> instances directly as they wrap different memory managers.
// To check that the underlying data matches, we can just manually check their lenth, and the
// fact that a reference to the first pixel in both spans is actually the same memory location.
Assert.Equal(pixelSpan.Length, imageSpan.Length);
Assert.True(Unsafe.AreSame(ref pixelSpan.GetPinnableReference(), ref imageSpan.GetPinnableReference()));

Assert.True(image.TryGetSinglePixelSpan(out imageSpan));
imageSpan.Fill(bg);
for (var i = 10; i < 20; i++)
{
image.GetPixelRowSpan(i).Slice(10, 10).Fill(fg);
}
}

Assert.False(memoryManager.IsDisposed);
}

string fn = System.IO.Path.Combine(
TestEnvironment.ActualOutputDirectoryFullPath,
$"{nameof(this.WrapSystemDrawingBitmap_WhenObserved)}.bmp");

bmp.Save(fn, ImageFormat.Bmp);
}
}

private static bool ShouldSkipBitmapTest =>
!TestEnvironment.Is64BitProcess || (TestHelpers.ImageSharpBuiltAgainst != "netcoreapp3.1" && TestHelpers.ImageSharpBuiltAgainst != "netcoreapp2.1");
}
Expand Down