Browse Source

Merge remote-tracking branch 'upstream/master' into bigtiff

pull/1760/head
Ildar Khayrutdinov 5 years ago
parent
commit
d9081e0a7c
  1. 1
      src/ImageSharp/Common/Helpers/Numerics.cs
  2. 8
      src/ImageSharp/Formats/ImageExtensions.Save.cs
  3. 1
      src/ImageSharp/Formats/ImageExtensions.Save.tt
  4. 2
      src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs
  5. 6
      src/ImageSharp/Formats/Jpeg/Components/FastFloatingPointDCT.cs
  6. 135
      src/ImageSharp/Formats/Webp/Lossy/Vp8Residual.cs
  7. 11
      src/ImageSharp/ImageFrame{TPixel}.cs
  8. 4
      src/ImageSharp/Image{TPixel}.cs
  9. 17
      src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs
  10. 10
      src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.cs
  11. 2
      src/ImageSharp/Memory/Allocators/Internals/UnmanagedBuffer{T}.cs
  12. 65
      src/ImageSharp/Memory/Buffer2D{T}.cs
  13. 31
      src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs
  14. 52
      src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupSpanCache.cs
  15. 33
      src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs
  16. 83
      src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs
  17. 16
      src/ImageSharp/Memory/DiscontiguousBuffers/SpanCacheMode.cs
  18. 46
      tests/ImageSharp.Benchmarks/General/Buffer2D_DangerousGetRowSpan.cs
  19. 12
      tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs
  20. 37
      tests/ImageSharp.Tests/Formats/WebP/Vp8ResidualTests.cs
  21. 20
      tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs
  22. 160
      tests/ImageSharp.Tests/Memory/Buffer2DTests.SwapOrCopyContent.cs
  23. 108
      tests/ImageSharp.Tests/Memory/Buffer2DTests.cs
  24. 107
      tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.SwapOrCopyContent.cs
  25. 4
      tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.cs

1
src/ImageSharp/Common/Helpers/Numerics.cs

@ -970,6 +970,7 @@ namespace SixLabors.ImageSharp
/// <param name="value">Value.</param>
/// <param name="min">Mininum value, inclusive.</param>
/// <param name="max">Maximum value, inclusive.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static bool IsOutOfRange(int value, int min, int max)
=> (uint)(value - min) > (uint)(max - min);
}

8
src/ImageSharp/Formats/ImageExtensions.Save.cs

@ -105,7 +105,6 @@ namespace SixLabors.ImageSharp
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static void SaveAsBmp(this Image source, Stream stream, BmpEncoder encoder)
=> source.Save(
stream,
@ -208,7 +207,6 @@ namespace SixLabors.ImageSharp
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static void SaveAsGif(this Image source, Stream stream, GifEncoder encoder)
=> source.Save(
stream,
@ -311,7 +309,6 @@ namespace SixLabors.ImageSharp
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static void SaveAsJpeg(this Image source, Stream stream, JpegEncoder encoder)
=> source.Save(
stream,
@ -414,7 +411,6 @@ namespace SixLabors.ImageSharp
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static void SaveAsPbm(this Image source, Stream stream, PbmEncoder encoder)
=> source.Save(
stream,
@ -517,7 +513,6 @@ namespace SixLabors.ImageSharp
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static void SaveAsPng(this Image source, Stream stream, PngEncoder encoder)
=> source.Save(
stream,
@ -620,7 +615,6 @@ namespace SixLabors.ImageSharp
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static void SaveAsTga(this Image source, Stream stream, TgaEncoder encoder)
=> source.Save(
stream,
@ -723,7 +717,6 @@ namespace SixLabors.ImageSharp
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static void SaveAsWebp(this Image source, Stream stream, WebpEncoder encoder)
=> source.Save(
stream,
@ -826,7 +819,6 @@ namespace SixLabors.ImageSharp
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static void SaveAsTiff(this Image source, Stream stream, TiffEncoder encoder)
=> source.Save(
stream,

1
src/ImageSharp/Formats/ImageExtensions.Save.tt

@ -124,7 +124,6 @@ namespace SixLabors.ImageSharp
/// <param name="stream">The stream to save the image to.</param>
/// <param name="encoder">The encoder to save the image with.</param>
/// <exception cref="System.ArgumentNullException">Thrown if the stream is null.</exception>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public static void SaveAs<#= fmt #>(this Image source, Stream stream, <#= fmt #>Encoder encoder)
=> source.Save(
stream,

2
src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs

@ -179,7 +179,7 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder
// PackFromRgbPlanes expects the destination to be padded, so try to get padded span containing extra elements from the next row.
// If we can't get such a padded row because we are on a MemoryGroup boundary or at the last row,
// pack pixels to a temporary, padded proxy buffer, then copy the relevant values to the destination row.
if (this.pixelBuffer.TryGetPaddedRowSpan(yy, 3, out Span<TPixel> destRow))
if (this.pixelBuffer.DangerousTryGetPaddedRowSpan(yy, 3, out Span<TPixel> destRow))
{
PixelOperations<TPixel>.Instance.PackFromRgbPlanes(this.configuration, r, g, b, destRow);
}

6
src/ImageSharp/Formats/Jpeg/Components/FastFloatingPointDCT.cs

@ -146,11 +146,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components
/// <summary>
/// Apply floating point IDCT inplace using <see cref="Vector4"/> API.
/// </summary>
/// <remarks>
/// This method can be used even if there's no SIMD intrinsics available
/// as <see cref="Vector4"/> can be compiled to scalar instructions.
/// </remarks>
/// <param name="transposedBlock">Input block.</param>
private static void IDCT_Vector4(ref Block8x8F transposedBlock)
{
DebugGuard.IsTrue(Vector.IsHardwareAccelerated, "Scalar implementation should be called for non-accelerated hardware.");
// First pass - process columns
IDCT8x4_Vector4(ref transposedBlock.V0L);
IDCT8x4_Vector4(ref transposedBlock.V0R);

135
src/ImageSharp/Formats/Webp/Lossy/Vp8Residual.cs

@ -3,6 +3,11 @@
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
#if SUPPORTS_RUNTIME_INTRINSICS
using System.Runtime.Intrinsics;
using System.Runtime.Intrinsics.X86;
#endif
namespace SixLabors.ImageSharp.Formats.Webp.Lossy
{
@ -11,6 +16,16 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy
/// </summary>
internal class Vp8Residual
{
#if SUPPORTS_RUNTIME_INTRINSICS
private static readonly Vector256<byte> Cst2 = Vector256.Create((byte)2);
private static readonly Vector256<byte> Cst67 = Vector256.Create((byte)67);
#endif
private readonly byte[] scratch = new byte[32];
private readonly ushort[] scratchUShort = new ushort[16];
public int First { get; set; }
public int Last { get; set; }
@ -37,14 +52,39 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy
public void SetCoeffs(Span<short> coeffs)
{
int n;
this.Last = -1;
for (n = 15; n >= 0; --n)
#if SUPPORTS_RUNTIME_INTRINSICS
if (Sse2.IsSupported)
{
ref short coeffsRef = ref MemoryMarshal.GetReference(coeffs);
Vector128<byte> c0 = Unsafe.As<short, Vector128<byte>>(ref coeffsRef);
Vector128<byte> c1 = Unsafe.As<short, Vector128<byte>>(ref Unsafe.Add(ref coeffsRef, 8));
// Use SSE2 to compare 16 values with a single instruction.
Vector128<sbyte> m0 = Sse2.PackSignedSaturate(c0.AsInt16(), c1.AsInt16());
Vector128<sbyte> m1 = Sse2.CompareEqual(m0, Vector128<sbyte>.Zero);
// Get the comparison results as a bitmask into 16bits. Negate the mask to get
// the position of entries that are not equal to zero. We don't need to mask
// out least significant bits according to res->first, since coeffs[0] is 0
// if res->first > 0.
uint mask = 0x0000ffffu ^ (uint)Sse2.MoveMask(m1);
// The position of the most significant non-zero bit indicates the position of
// the last non-zero value.
this.Last = mask != 0 ? Numerics.Log2(mask) : -1;
}
else
#endif
{
if (coeffs[n] != 0)
int n;
this.Last = -1;
for (n = 15; n >= 0; --n)
{
this.Last = n;
break;
if (coeffs[n] != 0)
{
this.Last = n;
break;
}
}
}
@ -129,27 +169,78 @@ namespace SixLabors.ImageSharp.Formats.Webp.Lossy
return LossyUtils.Vp8BitCost(0, (byte)p0);
}
int v;
for (; n < this.Last; ++n)
#if SUPPORTS_RUNTIME_INTRINSICS
if (Avx2.IsSupported)
{
Span<byte> ctxs = this.scratch.AsSpan(0, 16);
Span<byte> levels = this.scratch.AsSpan(16, 16);
Span<ushort> absLevels = this.scratchUShort.AsSpan();
// Precompute clamped levels and contexts, packed to 8b.
ref short outputRef = ref MemoryMarshal.GetReference<short>(this.Coeffs);
Vector256<short> c0 = Unsafe.As<short, Vector256<byte>>(ref outputRef).AsInt16();
Vector256<short> d0 = Avx2.Subtract(Vector256<short>.Zero, c0);
Vector256<short> e0 = Avx2.Max(c0, d0); // abs(v), 16b
Vector256<sbyte> f = Avx2.PackSignedSaturate(e0, e0);
Vector256<byte> g = Avx2.Min(f.AsByte(), Cst2);
Vector256<byte> h = Avx2.Min(f.AsByte(), Cst67); // clampLevel in [0..67]
ref byte ctxsRef = ref MemoryMarshal.GetReference(ctxs);
ref byte levelsRef = ref MemoryMarshal.GetReference(levels);
ref ushort absLevelsRef = ref MemoryMarshal.GetReference(absLevels);
Unsafe.As<byte, Vector128<byte>>(ref ctxsRef) = g.GetLower();
Unsafe.As<byte, Vector128<byte>>(ref levelsRef) = h.GetLower();
Unsafe.As<ushort, Vector256<ushort>>(ref absLevelsRef) = e0.AsUInt16();
int level;
int flevel;
for (; n < this.Last; ++n)
{
int ctx = ctxs[n];
level = levels[n];
flevel = absLevels[n];
cost += WebpLookupTables.Vp8LevelFixedCosts[flevel] + t.Costs[level];
t = costs[n + 1].Costs[ctx];
}
// Last coefficient is always non-zero.
level = levels[n];
flevel = absLevels[n];
cost += WebpLookupTables.Vp8LevelFixedCosts[flevel] + t.Costs[level];
if (n < 15)
{
int b = WebpConstants.Vp8EncBands[n + 1];
int ctx = ctxs[n];
int lastP0 = this.Prob[b].Probabilities[ctx].Probabilities[0];
cost += LossyUtils.Vp8BitCost(0, (byte)lastP0);
}
return cost;
}
#endif
{
int v;
for (; n < this.Last; ++n)
{
v = Math.Abs(this.Coeffs[n]);
int ctx = v >= 2 ? 2 : v;
cost += LevelCost(t.Costs, v);
t = costs[n + 1].Costs[ctx];
}
// Last coefficient is always non-zero
v = Math.Abs(this.Coeffs[n]);
int ctx = v >= 2 ? 2 : v;
cost += LevelCost(t.Costs, v);
t = costs[n + 1].Costs[ctx];
}
if (n < 15)
{
int b = WebpConstants.Vp8EncBands[n + 1];
int ctx = v == 1 ? 1 : 2;
int lastP0 = this.Prob[b].Probabilities[ctx].Probabilities[0];
cost += LossyUtils.Vp8BitCost(0, (byte)lastP0);
}
// Last coefficient is always non-zero
v = Math.Abs(this.Coeffs[n]);
cost += LevelCost(t.Costs, v);
if (n < 15)
{
int b = WebpConstants.Vp8EncBands[n + 1];
int ctx = v == 1 ? 1 : 2;
int lastP0 = this.Prob[b].Probabilities[ctx].Probabilities[0];
cost += LossyUtils.Vp8BitCost(0, (byte)lastP0);
return cost;
}
return cost;
}
[MethodImpl(InliningOptions.ShortMethod)]

11
src/ImageSharp/ImageFrame{TPixel}.cs

@ -145,13 +145,8 @@ namespace SixLabors.ImageSharp
source.PixelBuffer.FastMemoryGroup.CopyTo(this.PixelBuffer.FastMemoryGroup);
}
/// <summary>
/// Gets the image pixels. Not private as Buffer2D requires an array in its constructor.
/// </summary>
internal Buffer2D<TPixel> PixelBuffer { get; private set; }
/// <inheritdoc/>
Buffer2D<TPixel> IPixelSource<TPixel>.PixelBuffer => this.PixelBuffer;
public Buffer2D<TPixel> PixelBuffer { get; private set; }
/// <summary>
/// Gets or sets the pixel at the specified position.
@ -443,12 +438,12 @@ namespace SixLabors.ImageSharp
[MethodImpl(InliningOptions.ShortMethod)]
private void VerifyCoords(int x, int y)
{
if (x < 0 || x >= this.Width)
if ((uint)x >= (uint)this.Width)
{
ThrowArgumentOutOfRangeException(nameof(x));
}
if (y < 0 || y >= this.Height)
if ((uint)y >= (uint)this.Height)
{
ThrowArgumentOutOfRangeException(nameof(y));
}

4
src/ImageSharp/Image{TPixel}.cs

@ -452,12 +452,12 @@ namespace SixLabors.ImageSharp
[MethodImpl(InliningOptions.ShortMethod)]
private void VerifyCoords(int x, int y)
{
if (x < 0 || x >= this.Width)
if ((uint)x >= (uint)this.Width)
{
ThrowArgumentOutOfRangeException(nameof(x));
}
if (y < 0 || y >= this.Height)
if ((uint)y >= (uint)this.Height)
{
ThrowArgumentOutOfRangeException(nameof(y));
}

17
src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs

@ -13,34 +13,35 @@ namespace SixLabors.ImageSharp.Memory.Internals
where T : struct
{
private readonly int lengthInBytes;
private byte[] array;
private LifetimeGuard lifetimeGuard;
public SharedArrayPoolBuffer(int lengthInElements)
{
this.lengthInBytes = lengthInElements * Unsafe.SizeOf<T>();
this.array = ArrayPool<byte>.Shared.Rent(this.lengthInBytes);
this.lifetimeGuard = new LifetimeGuard(this.array);
this.Array = ArrayPool<byte>.Shared.Rent(this.lengthInBytes);
this.lifetimeGuard = new LifetimeGuard(this.Array);
}
public byte[] Array { get; private set; }
protected override void Dispose(bool disposing)
{
if (this.array == null)
if (this.Array == null)
{
return;
}
this.lifetimeGuard.Dispose();
this.array = null;
this.Array = null;
}
public override Span<T> GetSpan()
{
this.CheckDisposed();
return MemoryMarshal.Cast<byte, T>(this.array.AsSpan(0, this.lengthInBytes));
return MemoryMarshal.Cast<byte, T>(this.Array.AsSpan(0, this.lengthInBytes));
}
protected override object GetPinnableObject() => this.array;
protected override object GetPinnableObject() => this.Array;
public void AddRef()
{
@ -53,7 +54,7 @@ namespace SixLabors.ImageSharp.Memory.Internals
[Conditional("DEBUG")]
private void CheckDisposed()
{
if (this.array == null)
if (this.Array == null)
{
throw new ObjectDisposedException("SharedArrayPoolBuffer");
}

10
src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.cs

@ -8,12 +8,10 @@ using System.Threading;
namespace SixLabors.ImageSharp.Memory.Internals
{
internal partial class UniformUnmanagedMemoryPool
#if !NETSTANDARD1_3
// In case UniformUnmanagedMemoryPool is finalized, we prefer to run its finalizer after the guard finalizers,
// but we should not rely on this.
: System.Runtime.ConstrainedExecution.CriticalFinalizerObject
#endif
// CriticalFinalizerObject:
// In case UniformUnmanagedMemoryPool is finalized, we prefer to run its finalizer after the guard finalizers,
// but we should not rely on this.
internal partial class UniformUnmanagedMemoryPool : System.Runtime.ConstrainedExecution.CriticalFinalizerObject
{
private static int minTrimPeriodMilliseconds = int.MaxValue;
private static readonly List<WeakReference<UniformUnmanagedMemoryPool>> AllPools = new();

2
src/ImageSharp/Memory/Allocators/Internals/UnmanagedBuffer{T}.cs

@ -31,7 +31,7 @@ namespace SixLabors.ImageSharp.Memory.Internals
this.lifetimeGuard = lifetimeGuard;
}
private void* Pointer => this.lifetimeGuard.Handle.Pointer;
public void* Pointer => this.lifetimeGuard.Handle.Pointer;
public override Span<T> GetSpan()
{

65
src/ImageSharp/Memory/Buffer2D{T}.cs

@ -56,7 +56,7 @@ namespace SixLabors.ImageSharp.Memory
/// It's public counterpart is <see cref="MemoryGroup"/>,
/// which only exposes the view of the MemoryGroup.
/// </remarks>
internal MemoryGroup<T> FastMemoryGroup { get; }
internal MemoryGroup<T> FastMemoryGroup { get; private set; }
/// <summary>
/// Gets a reference to the element at the specified position.
@ -97,35 +97,37 @@ namespace SixLabors.ImageSharp.Memory
[MethodImpl(InliningOptions.ShortMethod)]
public Span<T> DangerousGetRowSpan(int y)
{
DebugGuard.MustBeGreaterThanOrEqualTo(y, 0, nameof(y));
DebugGuard.MustBeLessThan(y, this.Height, nameof(y));
if ((uint)y >= (uint)this.Height)
{
this.ThrowYOutOfRangeException(y);
}
return this.GetRowMemoryCore(y).Span;
return this.FastMemoryGroup.GetRowSpanCoreUnsafe(y, this.Width);
}
internal bool TryGetPaddedRowSpan(int y, int padding, out Span<T> paddedSpan)
internal bool DangerousTryGetPaddedRowSpan(int y, int padding, out Span<T> paddedSpan)
{
DebugGuard.MustBeGreaterThanOrEqualTo(y, 0, nameof(y));
DebugGuard.MustBeLessThan(y, this.Height, nameof(y));
int stride = this.Width + padding;
Memory<T> memory = this.FastMemoryGroup.GetRemainingSliceOfBuffer(y * (long)this.Width);
Span<T> slice = this.FastMemoryGroup.GetRemainingSliceOfBuffer(y * (long)this.Width);
if (memory.Length < stride)
if (slice.Length < stride)
{
paddedSpan = default;
return false;
}
paddedSpan = memory.Span.Slice(0, stride);
paddedSpan = slice.Slice(0, stride);
return true;
}
[MethodImpl(InliningOptions.ShortMethod)]
internal ref T GetElementUnsafe(int x, int y)
{
Span<T> span = this.GetRowMemoryCore(y).Span;
Span<T> span = this.FastMemoryGroup.GetRowSpanCoreUnsafe(y, this.Width);
return ref span[x];
}
@ -139,7 +141,7 @@ namespace SixLabors.ImageSharp.Memory
{
DebugGuard.MustBeGreaterThanOrEqualTo(y, 0, nameof(y));
DebugGuard.MustBeLessThan(y, this.Height, nameof(y));
return this.FastMemoryGroup.View.GetBoundedSlice(y * (long)this.Width, this.Width);
return this.FastMemoryGroup.View.GetBoundedMemorySlice(y * (long)this.Width, this.Width);
}
/// <summary>
@ -168,25 +170,36 @@ namespace SixLabors.ImageSharp.Memory
/// Swaps the contents of 'destination' with 'source' if the buffers are owned (1),
/// copies the contents of 'source' to 'destination' otherwise (2). Buffers should be of same size in case 2!
/// </summary>
internal static void SwapOrCopyContent(Buffer2D<T> destination, Buffer2D<T> source)
{
MemoryGroup<T>.SwapOrCopyContent(destination.FastMemoryGroup, source.FastMemoryGroup);
SwapOwnData(destination, source);
}
[MethodImpl(InliningOptions.ShortMethod)]
private Memory<T> GetRowMemoryCore(int y) => this.FastMemoryGroup.GetBoundedSlice(y * (long)this.Width, this.Width);
private static void SwapOwnData(Buffer2D<T> a, Buffer2D<T> b)
internal static bool SwapOrCopyContent(Buffer2D<T> destination, Buffer2D<T> source)
{
Size aSize = a.Size();
Size bSize = b.Size();
bool swapped = false;
if (MemoryGroup<T>.CanSwapContent(destination.FastMemoryGroup, source.FastMemoryGroup))
{
(destination.FastMemoryGroup, source.FastMemoryGroup) =
(source.FastMemoryGroup, destination.FastMemoryGroup);
destination.FastMemoryGroup.RecreateViewAfterSwap();
source.FastMemoryGroup.RecreateViewAfterSwap();
swapped = true;
}
else
{
if (destination.FastMemoryGroup.TotalLength != source.FastMemoryGroup.TotalLength)
{
throw new InvalidMemoryOperationException(
"Trying to copy/swap incompatible buffers. This is most likely caused by applying an unsupported processor to wrapped-memory images.");
}
b.Width = aSize.Width;
b.Height = aSize.Height;
source.FastMemoryGroup.CopyTo(destination.MemoryGroup);
}
a.Width = bSize.Width;
a.Height = bSize.Height;
(destination.Width, source.Width) = (source.Width, destination.Width);
(destination.Height, source.Height) = (source.Height, destination.Height);
return swapped;
}
[MethodImpl(InliningOptions.ColdPath)]
private void ThrowYOutOfRangeException(int y) =>
throw new ArgumentOutOfRangeException(
$"DangerousGetRowSpan({y}). Y was out of range. Height={this.Height}");
}
}

31
src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs

@ -29,7 +29,7 @@ namespace SixLabors.ImageSharp.Memory
/// Returns a slice that is expected to be within the bounds of a single buffer.
/// Otherwise <see cref="ArgumentOutOfRangeException"/> is thrown.
/// </summary>
internal static Memory<T> GetBoundedSlice<T>(this IMemoryGroup<T> group, long start, int length)
internal static Memory<T> GetBoundedMemorySlice<T>(this IMemoryGroup<T> group, long start, int length)
where T : struct
{
Guard.NotNull(group, nameof(group));
@ -37,7 +37,8 @@ namespace SixLabors.ImageSharp.Memory
Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length));
Guard.MustBeLessThan(start, group.TotalLength, nameof(start));
int bufferIdx = (int)(start / group.BufferLength);
int bufferIdx = (int)Math.DivRem(start, group.BufferLength, out long bufferStartLong);
int bufferStart = (int)bufferStartLong;
// if (bufferIdx < 0 || bufferIdx >= group.Count)
if ((uint)bufferIdx >= group.Count)
@ -45,7 +46,6 @@ namespace SixLabors.ImageSharp.Memory
throw new ArgumentOutOfRangeException(nameof(start));
}
int bufferStart = (int)(start % group.BufferLength);
int bufferEnd = bufferStart + length;
Memory<T> memory = group[bufferIdx];
@ -57,31 +57,6 @@ namespace SixLabors.ImageSharp.Memory
return memory.Slice(bufferStart, length);
}
/// <summary>
/// Returns the slice of the buffer starting at global index <paramref name="start"/> that goes until the end of the buffer.
/// </summary>
internal static Memory<T> GetRemainingSliceOfBuffer<T>(this IMemoryGroup<T> group, long start)
where T : struct
{
Guard.NotNull(group, nameof(group));
Guard.IsTrue(group.IsValid, nameof(group), "Group must be valid!");
Guard.MustBeLessThan(start, group.TotalLength, nameof(start));
int bufferIdx = (int)(start / group.BufferLength);
// if (bufferIdx < 0 || bufferIdx >= group.Count)
if ((uint)bufferIdx >= group.Count)
{
throw new ArgumentOutOfRangeException(nameof(start));
}
int bufferStart = (int)(start % group.BufferLength);
Memory<T> memory = group[bufferIdx];
return memory.Slice(bufferStart);
}
internal static void CopyTo<T>(this IMemoryGroup<T> source, Span<T> target)
where T : struct
{

52
src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupSpanCache.cs

@ -0,0 +1,52 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Buffers;
using SixLabors.ImageSharp.Memory.Internals;
namespace SixLabors.ImageSharp.Memory
{
/// <summary>
/// Cached pointer or array data enabling fast <see cref="Span{T}"/> access from
/// known <see cref="IMemoryOwner{T}"/> implementations.
/// </summary>
internal unsafe struct MemoryGroupSpanCache
{
public SpanCacheMode Mode;
public byte[] SingleArray;
public void* SinglePointer;
public void*[] MultiPointer;
public static MemoryGroupSpanCache Create<T>(IMemoryOwner<T>[] memoryOwners)
where T : struct
{
IMemoryOwner<T> owner0 = memoryOwners[0];
MemoryGroupSpanCache memoryGroupSpanCache = default;
if (memoryOwners.Length == 1)
{
if (owner0 is SharedArrayPoolBuffer<T> sharedPoolBuffer)
{
memoryGroupSpanCache.Mode = SpanCacheMode.SingleArray;
memoryGroupSpanCache.SingleArray = sharedPoolBuffer.Array;
}
else if (owner0 is UnmanagedBuffer<T> unmanagedBuffer)
{
memoryGroupSpanCache.Mode = SpanCacheMode.SinglePointer;
memoryGroupSpanCache.SinglePointer = unmanagedBuffer.Pointer;
}
}
else if (owner0 is UnmanagedBuffer<T>)
{
memoryGroupSpanCache.Mode = SpanCacheMode.MultiPointer;
memoryGroupSpanCache.MultiPointer = new void*[memoryOwners.Length];
for (int i = 0; i < memoryOwners.Length; i++)
{
memoryGroupSpanCache.MultiPointer[i] = ((UnmanagedBuffer<T>)memoryOwners[i]).Pointer;
}
}
return memoryGroupSpanCache;
}
}
}

33
src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs

@ -26,6 +26,7 @@ namespace SixLabors.ImageSharp.Memory
this.memoryOwners = memoryOwners;
this.Swappable = swappable;
this.View = new MemoryGroupView<T>(this);
this.memoryGroupSpanCache = MemoryGroupSpanCache.Create(memoryOwners);
}
public Owned(
@ -122,6 +123,12 @@ namespace SixLabors.ImageSharp.Memory
}
}
public override void RecreateViewAfterSwap()
{
this.View.Invalidate();
this.View = new MemoryGroupView<T>(this);
}
/// <inheritdoc/>
IEnumerator<Memory<T>> IEnumerable<Memory<T>>.GetEnumerator()
{
@ -167,32 +174,6 @@ namespace SixLabors.ImageSharp.Memory
[MethodImpl(MethodImplOptions.NoInlining)]
private static void ThrowObjectDisposedException() => throw new ObjectDisposedException(nameof(MemoryGroup<T>));
internal static void SwapContents(Owned a, Owned b)
{
a.EnsureNotDisposed();
b.EnsureNotDisposed();
IMemoryOwner<T>[] tempOwners = a.memoryOwners;
long tempTotalLength = a.TotalLength;
int tempBufferLength = a.BufferLength;
RefCountedLifetimeGuard tempGroupOwner = a.groupLifetimeGuard;
a.memoryOwners = b.memoryOwners;
a.TotalLength = b.TotalLength;
a.BufferLength = b.BufferLength;
a.groupLifetimeGuard = b.groupLifetimeGuard;
b.memoryOwners = tempOwners;
b.TotalLength = tempTotalLength;
b.BufferLength = tempBufferLength;
b.groupLifetimeGuard = tempGroupOwner;
a.View.Invalidate();
b.View.Invalidate();
a.View = new MemoryGroupView<T>(a);
b.View = new MemoryGroupView<T>(b);
}
// When the MemoryGroup points to multiple buffers via `groupLifetimeGuard`,
// the lifetime of the individual buffers is managed by the guard.
// Group buffer IMemoryOwner<T>-s d not manage ownership.

83
src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs

@ -6,6 +6,8 @@ using System.Buffers;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using SixLabors.ImageSharp.Memory.Internals;
namespace SixLabors.ImageSharp.Memory
@ -21,6 +23,8 @@ namespace SixLabors.ImageSharp.Memory
{
private static readonly int ElementSize = Unsafe.SizeOf<T>();
private MemoryGroupSpanCache memoryGroupSpanCache;
private MemoryGroup(int bufferLength, long totalLength)
{
this.BufferLength = bufferLength;
@ -31,10 +35,10 @@ namespace SixLabors.ImageSharp.Memory
public abstract int Count { get; }
/// <inheritdoc />
public int BufferLength { get; private set; }
public int BufferLength { get; }
/// <inheritdoc />
public long TotalLength { get; private set; }
public long TotalLength { get; }
/// <inheritdoc />
public bool IsValid { get; private set; } = true;
@ -241,32 +245,62 @@ namespace SixLabors.ImageSharp.Memory
return new Owned(source, bufferLength, totalLength, false);
}
/// <summary>
/// Swaps the contents of 'target' with 'source' if the buffers are allocated (1),
/// copies the contents of 'source' to 'target' otherwise (2).
/// Groups should be of same TotalLength in case 2.
/// </summary>
public static bool SwapOrCopyContent(MemoryGroup<T> target, MemoryGroup<T> source)
[MethodImpl(InliningOptions.ShortMethod)]
public unsafe Span<T> GetRowSpanCoreUnsafe(int y, int width)
{
if (source is Owned ownedSrc && ownedSrc.Swappable &&
target is Owned ownedTarget && ownedTarget.Swappable)
switch (this.memoryGroupSpanCache.Mode)
{
Owned.SwapContents(ownedTarget, ownedSrc);
return true;
}
else
{
if (target.TotalLength != source.TotalLength)
case SpanCacheMode.SingleArray:
{
#if SUPPORTS_CREATESPAN
ref byte b0 = ref MemoryMarshal.GetReference<byte>(this.memoryGroupSpanCache.SingleArray);
ref T e0 = ref Unsafe.As<byte, T>(ref b0);
e0 = ref Unsafe.Add(ref e0, y * width);
return MemoryMarshal.CreateSpan(ref e0, width);
#else
return MemoryMarshal.Cast<byte, T>(this.memoryGroupSpanCache.SingleArray).Slice(y * width, width);
#endif
}
case SpanCacheMode.SinglePointer:
{
throw new InvalidMemoryOperationException(
"Trying to copy/swap incompatible buffers. This is most likely caused by applying an unsupported processor to wrapped-memory images.");
void* start = Unsafe.Add<T>(this.memoryGroupSpanCache.SinglePointer, y * width);
return new Span<T>(start, width);
}
source.CopyTo(target);
return false;
case SpanCacheMode.MultiPointer:
{
this.GetMultiBufferPosition(y, width, out int bufferIdx, out int bufferStart);
void* start = Unsafe.Add<T>(this.memoryGroupSpanCache.MultiPointer[bufferIdx], bufferStart);
return new Span<T>(start, width);
}
default:
{
this.GetMultiBufferPosition(y, width, out int bufferIdx, out int bufferStart);
return this[bufferIdx].Span.Slice(bufferStart, width);
}
}
}
/// <summary>
/// Returns the slice of the buffer starting at global index <paramref name="start"/> that goes until the end of the buffer.
/// </summary>
public Span<T> GetRemainingSliceOfBuffer(long start)
{
long bufferIdx = Math.DivRem(start, this.BufferLength, out long bufferStart);
Memory<T> memory = this[(int)bufferIdx];
return memory.Span.Slice((int)bufferStart);
}
public static bool CanSwapContent(MemoryGroup<T> target, MemoryGroup<T> source) =>
source is Owned { Swappable: true } && target is Owned { Swappable: true };
public virtual void RecreateViewAfterSwap()
{
}
public virtual void IncreaseRefCounts()
{
}
@ -274,5 +308,14 @@ namespace SixLabors.ImageSharp.Memory
public virtual void DecreaseRefCounts()
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void GetMultiBufferPosition(int y, int width, out int bufferIdx, out int bufferStart)
{
long start = y * (long)width;
long bufferIdxLong = Math.DivRem(start, this.BufferLength, out long bufferStartLong);
bufferIdx = (int)bufferIdxLong;
bufferStart = (int)bufferStartLong;
}
}
}

16
src/ImageSharp/Memory/DiscontiguousBuffers/SpanCacheMode.cs

@ -0,0 +1,16 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
namespace SixLabors.ImageSharp.Memory
{
/// <summary>
/// Selects active values in <see cref="MemoryGroupSpanCache"/>.
/// </summary>
internal enum SpanCacheMode
{
Default = default,
SingleArray,
SinglePointer,
MultiPointer
}
}

46
tests/ImageSharp.Benchmarks/General/Buffer2D_DangerousGetRowSpan.cs

@ -0,0 +1,46 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System.Runtime.CompilerServices;
using BenchmarkDotNet.Attributes;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Benchmarks.General
{
public class Buffer2D_DangerousGetRowSpan
{
private const int Height = 1024;
[Params(0.5, 2.0, 10.0)] public double SizeMegaBytes { get; set; }
private Buffer2D<Rgba32> buffer;
[GlobalSetup]
public unsafe void Setup()
{
int totalElements = (int)(1024 * 1024 * this.SizeMegaBytes) / sizeof(Rgba32);
int width = totalElements / Height;
MemoryAllocator allocator = Configuration.Default.MemoryAllocator;
this.buffer = allocator.Allocate2D<Rgba32>(width, Height);
}
[GlobalCleanup]
public void Cleanup() => this.buffer.Dispose();
[Benchmark]
public int DangerousGetRowSpan() =>
this.buffer.DangerousGetRowSpan(1).Length +
this.buffer.DangerousGetRowSpan(Height - 1).Length;
// BenchmarkDotNet=v0.13.0, OS=Windows 10.0.19044
// Intel Core i9-10900X CPU 3.70GHz, 1 CPU, 20 logical and 10 physical cores
//
// | Method | SizeMegaBytes | Mean | Error | StdDev |
// |-------------------- |-------------- |----------:|----------:|----------:|
// | DangerousGetRowSpan | 0.5 | 7.498 ns | 0.1784 ns | 0.3394 ns |
// | DangerousGetRowSpan | 2 | 6.542 ns | 0.1565 ns | 0.3659 ns |
// | DangerousGetRowSpan | 10 | 38.556 ns | 0.6604 ns | 0.8587 ns |
}
}

12
tests/ImageSharp.Tests/Formats/Jpg/DCTTests.cs

@ -149,12 +149,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
// 4 paths:
// 1. AllowAll - call avx/fma implementation
// 2. DisableFMA - call avx without fma implementation
// 3. DisableAvx - call sse Vector4 implementation
// 4. DisableHWIntrinsic - call scalar fallback implementation
// 3. DisableAvx - call sse implementation
// 4. DisableSIMD - call Vector4 fallback implementation
FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest,
seed,
HwIntrinsics.AllowAll | HwIntrinsics.DisableFMA | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
HwIntrinsics.AllowAll | HwIntrinsics.DisableFMA | HwIntrinsics.DisableAVX | HwIntrinsics.DisableSIMD);
}
// Forward transform
@ -197,12 +197,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg
// 4 paths:
// 1. AllowAll - call avx/fma implementation
// 2. DisableFMA - call avx without fma implementation
// 3. DisableAvx - call sse Vector4 implementation
// 4. DisableHWIntrinsic - call scalar fallback implementation
// 3. DisableAvx - call Vector4 implementation
// 4. DisableSIMD - call scalar fallback implementation
FeatureTestRunner.RunWithHwIntrinsicsFeature(
RunTest,
seed,
HwIntrinsics.AllowAll | HwIntrinsics.DisableFMA | HwIntrinsics.DisableAVX | HwIntrinsics.DisableHWIntrinsic);
HwIntrinsics.AllowAll | HwIntrinsics.DisableFMA | HwIntrinsics.DisableAVX | HwIntrinsics.DisableSIMD);
}
}
}

37
tests/ImageSharp.Tests/Formats/WebP/Vp8ResidualTests.cs

@ -0,0 +1,37 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using SixLabors.ImageSharp.Formats.Webp.Lossy;
using SixLabors.ImageSharp.Tests.TestUtilities;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Formats.WebP
{
[Trait("Format", "Webp")]
public class Vp8ResidualTests
{
private static void RunSetCoeffsTest()
{
// arrange
var residual = new Vp8Residual();
short[] coeffs = { 110, 0, -2, 0, 0, 0, 0, 0, 0, -1, 0, 0, 0, 0, 0, 0 };
// act
residual.SetCoeffs(coeffs);
// assert
Assert.Equal(9, residual.Last);
}
[Fact]
public void RunSetCoeffsTest_Works() => RunSetCoeffsTest();
#if SUPPORTS_RUNTIME_INTRINSICS
[Fact]
public void RunSetCoeffsTest_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunSetCoeffsTest, HwIntrinsics.AllowAll);
[Fact]
public void RunSetCoeffsTest_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunSetCoeffsTest, HwIntrinsics.DisableHWIntrinsic);
#endif
}
}

20
tests/ImageSharp.Tests/Formats/WebP/WebpEncoderTests.cs

@ -5,6 +5,7 @@ using System.IO;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Metadata;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Tests.TestUtilities;
using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison;
using Xunit;
using static SixLabors.ImageSharp.Tests.TestImages.Webp;
@ -14,6 +15,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp
[Trait("Format", "Webp")]
public class WebpEncoderTests
{
private static string TestImageLossyFullPath => Path.Combine(TestEnvironment.InputImagesDirectoryFullPath, Lossy.NoFilter06);
[Theory]
[WithFile(Flag, PixelTypes.Rgba32, WebpFileFormatType.Lossless)] // if its not a webp input image, it should default to lossless.
[WithFile(Lossless.NoTransform1, PixelTypes.Rgba32, WebpFileFormatType.Lossless)]
@ -288,6 +291,23 @@ namespace SixLabors.ImageSharp.Tests.Formats.Webp
image.VerifyEncoder(provider, "webp", string.Empty, encoder, ImageComparer.Tolerant(0.04f));
}
public static void RunEncodeLossy_WithPeakImage()
{
var provider = TestImageProvider<Rgba32>.File(TestImageLossyFullPath);
using Image<Rgba32> image = provider.GetImage();
var encoder = new WebpEncoder() { FileFormat = WebpFileFormatType.Lossy };
image.VerifyEncoder(provider, "webp", string.Empty, encoder, ImageComparer.Tolerant(0.04f));
}
#if SUPPORTS_RUNTIME_INTRINSICS
[Fact]
public void RunEncodeLossy_WithPeakImage_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunEncodeLossy_WithPeakImage, HwIntrinsics.AllowAll);
[Fact]
public void RunEncodeLossy_WithPeakImage_WithoutHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunEncodeLossy_WithPeakImage, HwIntrinsics.DisableHWIntrinsic);
#endif
private static ImageComparer GetComparer(int quality)
{
float tolerance = 0.01f; // ~1.0%

160
tests/ImageSharp.Tests/Memory/Buffer2DTests.SwapOrCopyContent.cs

@ -0,0 +1,160 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using System.Linq;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Memory
{
public partial class Buffer2DTests
{
public class SwapOrCopyContent
{
private readonly TestMemoryAllocator MemoryAllocator = new TestMemoryAllocator();
[Fact]
public void SwapOrCopyContent_WhenBothAllocated()
{
using (Buffer2D<int> a = this.MemoryAllocator.Allocate2D<int>(10, 5, AllocationOptions.Clean))
using (Buffer2D<int> b = this.MemoryAllocator.Allocate2D<int>(3, 7, AllocationOptions.Clean))
{
a[1, 3] = 666;
b[1, 3] = 444;
Memory<int> aa = a.FastMemoryGroup.Single();
Memory<int> bb = b.FastMemoryGroup.Single();
Buffer2D<int>.SwapOrCopyContent(a, b);
Assert.Equal(bb, a.FastMemoryGroup.Single());
Assert.Equal(aa, b.FastMemoryGroup.Single());
Assert.Equal(new Size(3, 7), a.Size());
Assert.Equal(new Size(10, 5), b.Size());
Assert.Equal(666, b[1, 3]);
Assert.Equal(444, a[1, 3]);
}
}
[Fact]
public void SwapOrCopyContent_WhenDestinationIsOwned_ShouldNotSwapInDisposedSourceBuffer()
{
using var destData = MemoryGroup<int>.Wrap(new int[100]);
using var dest = new Buffer2D<int>(destData, 10, 10);
using (Buffer2D<int> source = this.MemoryAllocator.Allocate2D<int>(10, 10, AllocationOptions.Clean))
{
source[0, 0] = 1;
dest[0, 0] = 2;
Buffer2D<int>.SwapOrCopyContent(dest, source);
}
int actual1 = dest.DangerousGetRowSpan(0)[0];
int actual2 = dest.DangerousGetRowSpan(0)[0];
int actual3 = dest.GetSafeRowMemory(0).Span[0];
int actual5 = dest[0, 0];
Assert.Equal(1, actual1);
Assert.Equal(1, actual2);
Assert.Equal(1, actual3);
Assert.Equal(1, actual5);
}
[Fact]
public void WhenBothAreMemoryOwners_ShouldSwap()
{
this.MemoryAllocator.BufferCapacityInBytes = sizeof(int) * 50;
using Buffer2D<int> a = this.MemoryAllocator.Allocate2D<int>(48, 2);
using Buffer2D<int> b = this.MemoryAllocator.Allocate2D<int>(50, 2);
Memory<int> a0 = a.FastMemoryGroup[0];
Memory<int> a1 = a.FastMemoryGroup[1];
Memory<int> b0 = b.FastMemoryGroup[0];
Memory<int> b1 = b.FastMemoryGroup[1];
bool swap = Buffer2D<int>.SwapOrCopyContent(a, b);
Assert.True(swap);
Assert.Equal(b0, a.FastMemoryGroup[0]);
Assert.Equal(b1, a.FastMemoryGroup[1]);
Assert.Equal(a0, b.FastMemoryGroup[0]);
Assert.Equal(a1, b.FastMemoryGroup[1]);
Assert.NotEqual(a.FastMemoryGroup[0], b.FastMemoryGroup[0]);
}
[Fact]
public void WhenBothAreMemoryOwners_ShouldReplaceViews()
{
using Buffer2D<int> a = this.MemoryAllocator.Allocate2D<int>(100, 1);
using Buffer2D<int> b = this.MemoryAllocator.Allocate2D<int>(100, 2);
a.FastMemoryGroup[0].Span[42] = 1;
b.FastMemoryGroup[0].Span[33] = 2;
MemoryGroupView<int> aView0 = (MemoryGroupView<int>)a.MemoryGroup;
MemoryGroupView<int> bView0 = (MemoryGroupView<int>)b.MemoryGroup;
Buffer2D<int>.SwapOrCopyContent(a, b);
Assert.False(aView0.IsValid);
Assert.False(bView0.IsValid);
Assert.ThrowsAny<InvalidOperationException>(() => _ = aView0[0].Span);
Assert.ThrowsAny<InvalidOperationException>(() => _ = bView0[0].Span);
Assert.True(a.MemoryGroup.IsValid);
Assert.True(b.MemoryGroup.IsValid);
Assert.Equal(2, a.MemoryGroup[0].Span[33]);
Assert.Equal(1, b.MemoryGroup[0].Span[42]);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void WhenDestIsNotAllocated_SameSize_ShouldCopy(bool sourceIsAllocated)
{
var data = new Rgba32[21];
var color = new Rgba32(1, 2, 3, 4);
using var destOwner = new TestMemoryManager<Rgba32>(data);
using var dest = new Buffer2D<Rgba32>(MemoryGroup<Rgba32>.Wrap(destOwner.Memory), 21, 1);
using Buffer2D<Rgba32> source = this.MemoryAllocator.Allocate2D<Rgba32>(21, 1);
source.FastMemoryGroup[0].Span[10] = color;
// Act:
bool swap = Buffer2D<Rgba32>.SwapOrCopyContent(dest, source);
// Assert:
Assert.False(swap);
Assert.Equal(color, dest.MemoryGroup[0].Span[10]);
Assert.NotEqual(source.FastMemoryGroup[0], dest.FastMemoryGroup[0]);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void WhenDestIsNotMemoryOwner_DifferentSize_Throws(bool sourceIsOwner)
{
var data = new Rgba32[21];
var color = new Rgba32(1, 2, 3, 4);
using var destOwner = new TestMemoryManager<Rgba32>(data);
using var dest = new Buffer2D<Rgba32>(MemoryGroup<Rgba32>.Wrap(destOwner.Memory), 21, 1);
using Buffer2D<Rgba32> source = this.MemoryAllocator.Allocate2D<Rgba32>(22, 1);
source.FastMemoryGroup[0].Span[10] = color;
// Act:
Assert.ThrowsAny<InvalidOperationException>(() => Buffer2D<Rgba32>.SwapOrCopyContent(dest, source));
Assert.Equal(color, source.MemoryGroup[0].Span[10]);
Assert.NotEqual(color, dest.MemoryGroup[0].Span[10]);
}
}
}
}

108
tests/ImageSharp.Tests/Memory/Buffer2DTests.cs

@ -4,7 +4,6 @@
using System;
using System.Buffers;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Memory;
@ -14,7 +13,7 @@ using Xunit;
// ReSharper disable InconsistentNaming
namespace SixLabors.ImageSharp.Tests.Memory
{
public class Buffer2DTests
public partial class Buffer2DTests
{
// ReSharper disable once ClassNeverInstantiated.Local
private class Assert : Xunit.Assert
@ -121,7 +120,7 @@ namespace SixLabors.ImageSharp.Tests.Memory
[InlineData(200, 100, 30, 1, 0)]
[InlineData(200, 100, 30, 2, 1)]
[InlineData(200, 100, 30, 4, 2)]
public unsafe void GetRowSpanY(int bufferCapacity, int width, int height, int y, int expectedBufferIndex)
public unsafe void DangerousGetRowSpan_TestAllocator(int bufferCapacity, int width, int height, int y, int expectedBufferIndex)
{
this.MemoryAllocator.BufferCapacityInBytes = sizeof(TestStructs.Foo) * bufferCapacity;
@ -136,6 +135,57 @@ namespace SixLabors.ImageSharp.Tests.Memory
}
}
[Theory]
[InlineData(100, 5)] // Within shared pool
[InlineData(77, 11)] // Within shared pool
[InlineData(100, 19)] // Single unmanaged pooled buffer
[InlineData(103, 17)] // Single unmanaged pooled buffer
[InlineData(100, 22)] // 2 unmanaged pooled buffers
[InlineData(100, 99)] // 9 unmanaged pooled buffers
[InlineData(100, 120)] // 2 unpooled buffers
public unsafe void DangerousGetRowSpan_UnmanagedAllocator(int width, int height)
{
const int sharedPoolThreshold = 1_000;
const int poolBufferSize = 2_000;
const int maxPoolSize = 10_000;
const int unpooledBufferSize = 8_000;
int elementSize = sizeof(TestStructs.Foo);
var allocator = new UniformUnmanagedMemoryPoolMemoryAllocator(
sharedPoolThreshold * elementSize,
poolBufferSize * elementSize,
maxPoolSize * elementSize,
unpooledBufferSize * elementSize);
using Buffer2D<TestStructs.Foo> buffer = allocator.Allocate2D<TestStructs.Foo>(width, height);
var rnd = new Random(42);
for (int y = 0; y < buffer.Height; y++)
{
Span<TestStructs.Foo> span = buffer.DangerousGetRowSpan(y);
for (int x = 0; x < span.Length; x++)
{
ref TestStructs.Foo e = ref span[x];
e.A = rnd.Next();
e.B = rnd.NextDouble();
}
}
// Re-seed
rnd = new Random(42);
for (int y = 0; y < buffer.Height; y++)
{
Span<TestStructs.Foo> span = buffer.GetSafeRowMemory(y).Span;
for (int x = 0; x < span.Length; x++)
{
ref TestStructs.Foo e = ref span[x];
Assert.True(rnd.Next() == e.A, $"Mismatch @ y={y} x={x}");
Assert.True(rnd.NextDouble() == e.B, $"Mismatch @ y={y} x={x}");
}
}
}
[Theory]
[InlineData(10, 0, 0, 0)]
[InlineData(10, 0, 2, 0)]
@ -153,7 +203,7 @@ namespace SixLabors.ImageSharp.Tests.Memory
using Buffer2D<byte> buffer = this.MemoryAllocator.Allocate2D<byte>(3, 5);
bool expectSuccess = expectedBufferIndex >= 0;
bool success = buffer.TryGetPaddedRowSpan(y, padding, out Span<byte> paddedSpan);
bool success = buffer.DangerousTryGetPaddedRowSpan(y, padding, out Span<byte> paddedSpan);
Xunit.Assert.Equal(expectSuccess, success);
if (success)
{
@ -229,56 +279,6 @@ namespace SixLabors.ImageSharp.Tests.Memory
}
}
[Fact]
public void SwapOrCopyContent_WhenBothAllocated()
{
using (Buffer2D<int> a = this.MemoryAllocator.Allocate2D<int>(10, 5, AllocationOptions.Clean))
using (Buffer2D<int> b = this.MemoryAllocator.Allocate2D<int>(3, 7, AllocationOptions.Clean))
{
a[1, 3] = 666;
b[1, 3] = 444;
Memory<int> aa = a.FastMemoryGroup.Single();
Memory<int> bb = b.FastMemoryGroup.Single();
Buffer2D<int>.SwapOrCopyContent(a, b);
Assert.Equal(bb, a.FastMemoryGroup.Single());
Assert.Equal(aa, b.FastMemoryGroup.Single());
Assert.Equal(new Size(3, 7), a.Size());
Assert.Equal(new Size(10, 5), b.Size());
Assert.Equal(666, b[1, 3]);
Assert.Equal(444, a[1, 3]);
}
}
[Fact]
public void SwapOrCopyContent_WhenDestinationIsOwned_ShouldNotSwapInDisposedSourceBuffer()
{
using var destData = MemoryGroup<int>.Wrap(new int[100]);
using var dest = new Buffer2D<int>(destData, 10, 10);
using (Buffer2D<int> source = this.MemoryAllocator.Allocate2D<int>(10, 10, AllocationOptions.Clean))
{
source[0, 0] = 1;
dest[0, 0] = 2;
Buffer2D<int>.SwapOrCopyContent(dest, source);
}
int actual1 = dest.DangerousGetRowSpan(0)[0];
int actual2 = dest.DangerousGetRowSpan(0)[0];
int actual3 = dest.GetSafeRowMemory(0).Span[0];
int actual5 = dest[0, 0];
Assert.Equal(1, actual1);
Assert.Equal(1, actual2);
Assert.Equal(1, actual3);
Assert.Equal(1, actual5);
}
[Theory]
[InlineData(100, 20, 0, 90, 10)]
[InlineData(100, 3, 0, 50, 50)]

107
tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.SwapOrCopyContent.cs

@ -1,107 +0,0 @@
// Copyright (c) Six Labors.
// Licensed under the Apache License, Version 2.0.
using System;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.PixelFormats;
using Xunit;
namespace SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers
{
public partial class MemoryGroupTests
{
public class SwapOrCopyContent : MemoryGroupTestsBase
{
[Fact]
public void WhenBothAreMemoryOwners_ShouldSwap()
{
this.MemoryAllocator.BufferCapacityInBytes = sizeof(int) * 50;
using MemoryGroup<int> a = this.MemoryAllocator.AllocateGroup<int>(100, 50);
using MemoryGroup<int> b = this.MemoryAllocator.AllocateGroup<int>(120, 50);
Memory<int> a0 = a[0];
Memory<int> a1 = a[1];
Memory<int> b0 = b[0];
Memory<int> b1 = b[1];
bool swap = MemoryGroup<int>.SwapOrCopyContent(a, b);
Assert.True(swap);
Assert.Equal(b0, a[0]);
Assert.Equal(b1, a[1]);
Assert.Equal(a0, b[0]);
Assert.Equal(a1, b[1]);
Assert.NotEqual(a[0], b[0]);
}
[Fact]
public void WhenBothAreMemoryOwners_ShouldReplaceViews()
{
using MemoryGroup<int> a = this.MemoryAllocator.AllocateGroup<int>(100, 100);
using MemoryGroup<int> b = this.MemoryAllocator.AllocateGroup<int>(120, 100);
a[0].Span[42] = 1;
b[0].Span[33] = 2;
MemoryGroupView<int> aView0 = a.View;
MemoryGroupView<int> bView0 = b.View;
MemoryGroup<int>.SwapOrCopyContent(a, b);
Assert.False(aView0.IsValid);
Assert.False(bView0.IsValid);
Assert.ThrowsAny<InvalidOperationException>(() => _ = aView0[0].Span);
Assert.ThrowsAny<InvalidOperationException>(() => _ = bView0[0].Span);
Assert.True(a.View.IsValid);
Assert.True(b.View.IsValid);
Assert.Equal(2, a.View[0].Span[33]);
Assert.Equal(1, b.View[0].Span[42]);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void WhenDestIsNotAllocated_SameSize_ShouldCopy(bool sourceIsAllocated)
{
var data = new Rgba32[21];
var color = new Rgba32(1, 2, 3, 4);
using var destOwner = new TestMemoryManager<Rgba32>(data);
using var dest = MemoryGroup<Rgba32>.Wrap(destOwner.Memory);
using MemoryGroup<Rgba32> source = this.MemoryAllocator.AllocateGroup<Rgba32>(21, 30);
source[0].Span[10] = color;
// Act:
bool swap = MemoryGroup<Rgba32>.SwapOrCopyContent(dest, source);
// Assert:
Assert.False(swap);
Assert.Equal(color, dest[0].Span[10]);
Assert.NotEqual(source[0], dest[0]);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public void WhenDestIsNotMemoryOwner_DifferentSize_Throws(bool sourceIsOwner)
{
var data = new Rgba32[21];
var color = new Rgba32(1, 2, 3, 4);
using var destOwner = new TestMemoryManager<Rgba32>(data);
var dest = MemoryGroup<Rgba32>.Wrap(destOwner.Memory);
using MemoryGroup<Rgba32> source = this.MemoryAllocator.AllocateGroup<Rgba32>(22, 30);
source[0].Span[10] = color;
// Act:
Assert.ThrowsAny<InvalidOperationException>(() => MemoryGroup<Rgba32>.SwapOrCopyContent(dest, source));
Assert.Equal(color, source[0].Span[10]);
Assert.NotEqual(color, dest[0].Span[10]);
}
}
}
}

4
tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.cs

@ -146,7 +146,7 @@ namespace SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers
{
using MemoryGroup<int> group = this.CreateTestGroup(totalLength, bufferLength, true);
Memory<int> slice = group.GetBoundedSlice(start, length);
Memory<int> slice = group.GetBoundedMemorySlice(start, length);
Assert.Equal(length, slice.Length);
@ -172,7 +172,7 @@ namespace SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers
public void GetBoundedSlice_WhenOverlapsBuffers_Throws(long totalLength, int bufferLength, long start, int length)
{
using MemoryGroup<int> group = this.CreateTestGroup(totalLength, bufferLength, true);
Assert.ThrowsAny<ArgumentOutOfRangeException>(() => group.GetBoundedSlice(start, length));
Assert.ThrowsAny<ArgumentOutOfRangeException>(() => group.GetBoundedMemorySlice(start, length));
}
[Fact]

Loading…
Cancel
Save