Browse Source

Merge branch 'main' into dependabot/github_actions/NuGet/setup-nuget-4

pull/3121/head
James Jackson-South 3 days ago
committed by GitHub
parent
commit
a7b9258349
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 9
      src/ImageSharp/Advanced/AdvancedImageExtensions.cs
  2. 21
      src/ImageSharp/Advanced/IImageFrameVisitor.cs
  3. 4
      src/ImageSharp/Advanced/IImageVisitor.cs
  4. 6
      src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs
  5. 6
      src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs
  6. 49
      src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs
  7. 6
      src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs
  8. 153
      src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs
  9. 8
      src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs
  10. 37
      src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs
  11. 11
      src/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs
  12. 49
      src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs
  13. 10
      src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs
  14. 13
      src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs
  15. 6
      src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs
  16. 22
      src/ImageSharp/Formats/Exr/ExrDecoderCore.cs
  17. 4
      src/ImageSharp/Formats/Exr/ExrEncoderCore.cs
  18. 2
      src/ImageSharp/Formats/Gif/GifEncoderCore.cs
  19. 13
      src/ImageSharp/ImageFrame.cs
  20. 15
      src/ImageSharp/ImageFrame{TPixel}.cs
  21. 1
      src/ImageSharp/Image{TPixel}.cs
  22. 180
      src/ImageSharp/Memory/Allocators/MemoryAllocator.cs
  23. 30
      src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs
  24. 40
      src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs
  25. 58
      src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs
  26. 24
      src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs
  27. 50
      src/ImageSharp/Memory/Buffer2DExtensions.cs
  28. 60
      src/ImageSharp/Memory/Buffer2DRegion{T}.cs
  29. 21
      src/ImageSharp/Memory/Buffer2D{T}.cs
  30. 8
      src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs
  31. 16
      src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs
  32. 5
      src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs
  33. 38
      src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs
  34. 2
      src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs
  35. 26
      tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs
  36. 2
      tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs
  37. 44
      tests/ImageSharp.Tests/Memory/Allocators/SimpleGcMemoryAllocatorTests.cs
  38. 56
      tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs
  39. 36
      tests/ImageSharp.Tests/Memory/Buffer2DTests.SwapOrCopyContent.cs
  40. 8
      tests/ImageSharp.Tests/Memory/BufferAreaTests.cs
  41. 10
      tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.Allocate.cs
  42. 2
      tests/ImageSharp.Tests/Processing/IntegralImageTests.cs
  43. 5
      tests/ImageSharp.Tests/TestImages.cs
  44. 2
      tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs
  45. 3
      tests/Images/External/ReferenceOutput/ExrDecoderTests/ExrDecoder_CanDecode_Pxr24Compressed_ExrPixelType_Uint_Rgba32_Calliphora_uint_pxr24.png
  46. 3
      tests/Images/Input/Exr/Calliphora_float_pxr24.exr
  47. 3
      tests/Images/Input/Exr/Calliphora_float_uncompressed.exr
  48. 3
      tests/Images/Input/Exr/Calliphora_half_pxr24.exr
  49. 3
      tests/Images/Input/Exr/Calliphora_uint_pxr24.exr
  50. 3
      tests/Images/Input/Exr/rgb_float32_uncompressed.exr

9
src/ImageSharp/Advanced/AdvancedImageExtensions.cs

@ -76,6 +76,15 @@ public static class AdvancedImageExtensions
public static Task AcceptVisitorAsync(this Image source, IImageVisitorAsync visitor, CancellationToken cancellationToken = default)
=> source.AcceptAsync(visitor, cancellationToken);
/// <summary>
/// Accepts a <see cref="IImageVisitor"/> to implement a double-dispatch pattern in order to
/// apply pixel-specific operations on non-generic <see cref="Image"/> instances
/// </summary>
/// <param name="source">The source image frame.</param>
/// <param name="visitor">The image visitor.</param>
public static void AcceptVisitor(this ImageFrame source, IImageFrameVisitor visitor)
=> source.Accept(visitor);
/// <summary>
/// Gets the representation of the pixels as a <see cref="IMemoryGroup{T}"/> containing the backing pixel data of the image
/// stored in row major order, as a list of contiguous <see cref="Memory{T}"/> blocks in the source image's pixel format.

21
src/ImageSharp/Advanced/IImageFrameVisitor.cs

@ -0,0 +1,21 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.PixelFormats;
namespace SixLabors.ImageSharp.Advanced;
/// <summary>
/// A visitor to implement a double-dispatch pattern in order to apply pixel-specific operations
/// on non-generic <see cref="ImageFrame"/> instances.
/// </summary>
public interface IImageFrameVisitor
{
/// <summary>
/// Provides a pixel-specific implementation for a given operation.
/// </summary>
/// <param name="frame">The image frame.</param>
/// <typeparam name="TPixel">The pixel type.</typeparam>
public void Visit<TPixel>(ImageFrame<TPixel> frame)
where TPixel : unmanaged, IPixel<TPixel>;
}

4
src/ImageSharp/Advanced/IImageVisitor.cs

@ -16,7 +16,7 @@ public interface IImageVisitor
/// </summary>
/// <param name="image">The image.</param>
/// <typeparam name="TPixel">The pixel type.</typeparam>
void Visit<TPixel>(Image<TPixel> image)
public void Visit<TPixel>(Image<TPixel> image)
where TPixel : unmanaged, IPixel<TPixel>;
}
@ -33,6 +33,6 @@ public interface IImageVisitorAsync
/// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
/// <typeparam name="TPixel">The pixel type.</typeparam>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task VisitAsync<TPixel>(Image<TPixel> image, CancellationToken cancellationToken)
public Task VisitAsync<TPixel>(Image<TPixel> image, CancellationToken cancellationToken)
where TPixel : unmanaged, IPixel<TPixel>;
}

6
src/ImageSharp/Formats/Exr/Compression/Compressors/NoneExrCompressor.cs

@ -17,8 +17,10 @@ internal class NoneExrCompressor : ExrBaseCompressor
/// <param name="allocator">The memory allocator.</param>
/// <param name="bytesPerBlock">Bytes per row block.</param>
/// <param name="bytesPerRow">Bytes per pixel row.</param>
public NoneExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow)
: base(output, allocator, bytesPerBlock, bytesPerRow)
/// <param name="rowsPerBlock">The pixel rows per block.</param>
/// <param name="width">The witdh of one row in pixels.</param>
public NoneExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width)
: base(output, allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width)
{
}

6
src/ImageSharp/Formats/Exr/Compression/Compressors/ZipExrCompressor.cs

@ -24,9 +24,11 @@ internal class ZipExrCompressor : ExrBaseCompressor
/// <param name="allocator">The memory allocator.</param>
/// <param name="bytesPerBlock">The bytes per block.</param>
/// <param name="bytesPerRow">The bytes per row.</param>
/// <param name="rowsPerBlock">The pixel rows per block.</param>
/// <param name="width">The witdh of one row in pixels.</param>
/// <param name="compressionLevel">The compression level for deflate compression.</param>
public ZipExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, DeflateCompressionLevel compressionLevel)
: base(output, allocator, bytesPerBlock, bytesPerRow)
public ZipExrCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width, DeflateCompressionLevel compressionLevel)
: base(output, allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width)
{
this.compressionLevel = compressionLevel;
this.buffer = allocator.Allocate<byte>((int)bytesPerBlock);

49
src/ImageSharp/Formats/Exr/Compression/Decompressors/B44ExrCompression.cs

@ -13,10 +13,6 @@ namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors;
/// </summary>
internal class B44ExrCompression : ExrBaseDecompressor
{
private readonly int width;
private readonly uint rowsPerBlock;
private readonly int channelCount;
private readonly byte[] scratch = new byte[14];
@ -31,14 +27,12 @@ internal class B44ExrCompression : ExrBaseDecompressor
/// <param name="allocator">The memory allocator.</param>
/// <param name="bytesPerBlock">The bytes per pixel row block.</param>
/// <param name="bytesPerRow">The bytes per row.</param>
/// <param name="rowsPerBlock">The rows per block.</param>
/// <param name="rowsPerBlock">The pixel rows per block.</param>
/// <param name="width">The width of a pixel row in pixels.</param>
/// <param name="channelCount">The number of channels of the image.</param>
public B44ExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width, int channelCount)
: base(allocator, bytesPerBlock, bytesPerRow)
: base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width)
{
this.width = width;
this.rowsPerBlock = rowsPerBlock;
this.channelCount = channelCount;
this.tmpBuffer = allocator.Allocate<ushort>((int)(width * rowsPerBlock * channelCount));
}
@ -52,19 +46,19 @@ internal class B44ExrCompression : ExrBaseDecompressor
int bytesLeft = (int)compressedBytes;
for (int i = 0; i < this.channelCount && bytesLeft > 0; i++)
{
for (int y = 0; y < this.rowsPerBlock; y += 4)
for (int y = 0; y < this.RowsPerBlock; y += 4)
{
Span<ushort> row0 = decompressed.Slice(outputOffset, this.width);
outputOffset += this.width;
Span<ushort> row1 = decompressed.Slice(outputOffset, this.width);
outputOffset += this.width;
Span<ushort> row2 = decompressed.Slice(outputOffset, this.width);
outputOffset += this.width;
Span<ushort> row3 = decompressed.Slice(outputOffset, this.width);
outputOffset += this.width;
Span<ushort> row0 = decompressed.Slice(outputOffset, this.Width);
outputOffset += this.Width;
Span<ushort> row1 = decompressed.Slice(outputOffset, this.Width);
outputOffset += this.Width;
Span<ushort> row2 = decompressed.Slice(outputOffset, this.Width);
outputOffset += this.Width;
Span<ushort> row3 = decompressed.Slice(outputOffset, this.Width);
outputOffset += this.Width;
int rowOffset = 0;
for (int x = 0; x < this.width && bytesLeft > 0; x += 4)
for (int x = 0; x < this.Width && bytesLeft > 0; x += 4)
{
int bytesRead = stream.Read(this.scratch, 0, 3);
if (bytesRead == 0)
@ -72,6 +66,7 @@ internal class B44ExrCompression : ExrBaseDecompressor
ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data from the stream!");
}
// Check if 3-byte encoded flat field.
if (this.scratch[2] >= 13 << 2)
{
Unpack3(this.scratch, this.s);
@ -89,8 +84,8 @@ internal class B44ExrCompression : ExrBaseDecompressor
bytesLeft -= 14;
}
int n = x + 3 < this.width ? 4 : this.width - x;
if (y + 3 < this.rowsPerBlock)
int n = x + 3 < this.Width ? 4 : this.Width - x;
if (y + 3 < this.RowsPerBlock)
{
this.s.AsSpan(0, n).CopyTo(row0[rowOffset..]);
this.s.AsSpan(4, n).CopyTo(row1[rowOffset..]);
@ -100,12 +95,12 @@ internal class B44ExrCompression : ExrBaseDecompressor
else
{
this.s.AsSpan(0, n).CopyTo(row0[rowOffset..]);
if (y + 1 < this.rowsPerBlock)
if (y + 1 < this.RowsPerBlock)
{
this.s.AsSpan(4, n).CopyTo(row1[rowOffset..]);
}
if (y + 2 < this.rowsPerBlock)
if (y + 2 < this.RowsPerBlock)
{
this.s.AsSpan(8, n).CopyTo(row2[rowOffset..]);
}
@ -124,16 +119,16 @@ internal class B44ExrCompression : ExrBaseDecompressor
// Rearrange the decompressed data such that the data for each scan line form a contiguous block.
int offsetDecompressed = 0;
int offsetOutput = 0;
int blockSize = (int)(this.width * this.rowsPerBlock);
for (int y = 0; y < this.rowsPerBlock; y++)
int blockSize = (int)(this.Width * this.RowsPerBlock);
for (int y = 0; y < this.RowsPerBlock; y++)
{
for (int i = 0; i < this.channelCount; i++)
{
decompressed.Slice(offsetDecompressed + (i * blockSize), this.width).CopyTo(outputBuffer[offsetOutput..]);
offsetOutput += this.width;
decompressed.Slice(offsetDecompressed + (i * blockSize), this.Width).CopyTo(outputBuffer[offsetOutput..]);
offsetOutput += this.Width;
}
offsetDecompressed += this.width;
offsetDecompressed += this.Width;
}
}

6
src/ImageSharp/Formats/Exr/Compression/Decompressors/NoneExrCompression.cs

@ -17,8 +17,10 @@ internal class NoneExrCompression : ExrBaseDecompressor
/// <param name="allocator">The memory allocator.</param>
/// <param name="bytesPerBlock">The bytes per pixel row block.</param>
/// <param name="bytesPerRow">The bytes per pixel row.</param>
public NoneExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow)
: base(allocator, bytesPerBlock, bytesPerRow)
/// <param name="rowsPerBlock">The pixel rows per block.</param>
/// <param name="width">The number of pixels per row.</param>
public NoneExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width)
: base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width)
{
}

153
src/ImageSharp/Formats/Exr/Compression/Decompressors/Pxr24Compression.cs

@ -0,0 +1,153 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Formats.Exr.Constants;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
namespace SixLabors.ImageSharp.Formats.Exr.Compression.Decompressors;
/// <summary>
/// Implementation of PXR24 decompressor for EXR image data.
/// </summary>
internal class Pxr24Compression : ExrBaseDecompressor
{
private readonly IMemoryOwner<byte> tmpBuffer;
private readonly int channelCount;
private readonly ExrPixelType pixelType;
/// <summary>
/// Initializes a new instance of the <see cref="Pxr24Compression" /> class.
/// </summary>
/// <param name="allocator">The memory allocator.</param>
/// <param name="bytesPerBlock">The bytes per pixel row block.</param>
/// <param name="bytesPerRow">The bytes per pixel row.</param>
/// <param name="rowsPerBlock">The pixel rows per block.</param>
/// <param name="width">The witdh of one row in pixels.</param>
/// <param name="channelCount">The number of channels for a pixel.</param>
/// <param name="pixelType">The pixel type.</param>
public Pxr24Compression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width, int channelCount, ExrPixelType pixelType)
: base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width)
{
this.tmpBuffer = allocator.Allocate<byte>((int)bytesPerBlock);
this.channelCount = channelCount;
this.pixelType = pixelType;
}
/// <inheritdoc/>
public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span<byte> buffer)
{
Span<byte> uncompressed = this.tmpBuffer.GetSpan();
Span<ushort> outputBufferHalf = MemoryMarshal.Cast<byte, ushort>(buffer);
Span<uint> outputBufferFloat = MemoryMarshal.Cast<byte, uint>(buffer);
Span<uint> outputBufferUint = MemoryMarshal.Cast<byte, uint>(buffer);
uint uncompressedBytes = this.BytesPerBlock;
UndoZipCompression(stream, compressedBytes, uncompressed, uncompressedBytes);
int lastIn = 0;
int outputOffset = 0;
for (int y = 0; y < this.RowsPerBlock; y++)
{
for (int c = 0; c < this.channelCount; c++)
{
switch (this.pixelType)
{
case ExrPixelType.UnsignedInt:
{
int offsetT0 = lastIn;
lastIn += this.Width;
int offsetT1 = lastIn;
lastIn += this.Width;
int offsetT2 = lastIn;
lastIn += this.Width;
int offsetT3 = lastIn;
lastIn += this.Width;
uint pixel = 0;
for (int x = 0; x < this.Width; x++)
{
uint t0 = uncompressed[offsetT0];
uint t1 = uncompressed[offsetT1];
uint t2 = uncompressed[offsetT2];
uint t3 = uncompressed[offsetT3];
uint diff = (t0 << 24) | (t1 << 16) | (t2 << 8) | t3;
pixel += diff;
outputBufferUint[outputOffset] = pixel;
offsetT0++;
offsetT1++;
offsetT2++;
offsetT3++;
outputOffset++;
}
break;
}
case ExrPixelType.Half:
{
int offsetT0 = lastIn;
lastIn += this.Width;
int offsetT1 = lastIn;
lastIn += this.Width;
uint pixel = 0;
for (int x = 0; x < this.Width; x++)
{
uint t0 = uncompressed[offsetT0];
uint t1 = uncompressed[offsetT1];
uint diff = (t0 << 8) | t1;
pixel += diff;
outputBufferHalf[outputOffset] = (ushort)pixel;
offsetT0++;
offsetT1++;
outputOffset++;
}
break;
}
case ExrPixelType.Float:
{
int offsetT0 = lastIn;
lastIn += this.Width;
int offsetT1 = lastIn;
lastIn += this.Width;
int offsetT2 = lastIn;
lastIn += this.Width;
uint pixel = 0;
for (int x = 0; x < this.Width; x++)
{
uint t0 = uncompressed[offsetT0];
uint t1 = uncompressed[offsetT1];
uint t2 = uncompressed[offsetT2];
uint diff = (t0 << 24) | (t1 << 16) | (t2 << 8);
pixel += diff;
outputBufferFloat[outputOffset] = pixel;
offsetT0++;
offsetT1++;
offsetT2++;
outputOffset++;
}
break;
}
}
}
}
}
/// <inheritdoc/>
protected override void Dispose(bool disposing) => this.tmpBuffer.Dispose();
}

8
src/ImageSharp/Formats/Exr/Compression/Decompressors/RunLengthExrCompression.cs

@ -14,16 +14,16 @@ internal class RunLengthExrCompression : ExrBaseDecompressor
{
private readonly IMemoryOwner<byte> tmpBuffer;
private readonly ushort[] s = new ushort[16];
/// <summary>
/// Initializes a new instance of the <see cref="RunLengthExrCompression" /> class.
/// </summary>
/// <param name="allocator">The memory allocator.</param>
/// <param name="bytesPerBlock">The bytes per pixel row block.</param>
/// <param name="bytesPerRow">The bytes per row.</param>
public RunLengthExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow)
: base(allocator, bytesPerBlock, bytesPerRow) => this.tmpBuffer = allocator.Allocate<byte>((int)bytesPerBlock);
/// <param name="rowsPerBlock">The pixel rows per block.</param>
/// <param name="width">The witdh of one row in pixels.</param>
public RunLengthExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width)
: base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) => this.tmpBuffer = allocator.Allocate<byte>((int)bytesPerBlock);
/// <inheritdoc/>
public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span<byte> buffer)

37
src/ImageSharp/Formats/Exr/Compression/Decompressors/ZipExrCompression.cs

@ -2,8 +2,6 @@
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.IO.Compression;
using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
@ -22,41 +20,18 @@ internal class ZipExrCompression : ExrBaseDecompressor
/// <param name="allocator">The memory allocator.</param>
/// <param name="bytesPerBlock">The bytes per pixel row block.</param>
/// <param name="bytesPerRow">The bytes per pixel row.</param>
public ZipExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow)
: base(allocator, bytesPerBlock, bytesPerRow) => this.tmpBuffer = allocator.Allocate<byte>((int)bytesPerBlock);
/// <param name="rowsPerBlock">The pixel rows per block.</param>
/// <param name="width">The witdh of one row in pixels.</param>
public ZipExrCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width)
: base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width) => this.tmpBuffer = allocator.Allocate<byte>((int)bytesPerBlock);
/// <inheritdoc/>
public override void Decompress(BufferedReadStream stream, uint compressedBytes, Span<byte> buffer)
{
Span<byte> uncompressed = this.tmpBuffer.GetSpan();
long pos = stream.Position;
using ZlibInflateStream inflateStream = new(
stream,
() =>
{
int left = (int)(compressedBytes - (stream.Position - pos));
return left > 0 ? left : 0;
});
inflateStream.AllocateNewBytes((int)this.BytesPerBlock, true);
using DeflateStream dataStream = inflateStream.CompressedStream!;
int totalRead = 0;
while (totalRead < buffer.Length)
{
int bytesRead = dataStream.Read(uncompressed, totalRead, buffer.Length - totalRead);
if (bytesRead <= 0)
{
break;
}
totalRead += bytesRead;
}
if (totalRead == 0)
{
ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data for zip compressed image data!");
}
uint uncompressedBytes = (uint)buffer.Length;
int totalRead = UndoZipCompression(stream, compressedBytes, uncompressed, uncompressedBytes);
Reconstruct(uncompressed, (uint)totalRead);
Interleave(uncompressed, (uint)totalRead, buffer);

11
src/ImageSharp/Formats/Exr/Compression/ExrBaseCompression.cs

@ -18,11 +18,15 @@ internal abstract class ExrBaseCompression : IDisposable
/// <param name="allocator">The memory allocator.</param>
/// <param name="bytesPerBlock">The bytes per block.</param>
/// <param name="bytesPerRow">The bytes per row.</param>
protected ExrBaseCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow)
/// <param name="rowsPerBlock">The number of pixel rows per block.</param>
/// <param name="width">The number of pixels of a row.</param>
protected ExrBaseCompression(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width)
{
this.Allocator = allocator;
this.BytesPerBlock = bytesPerBlock;
this.BytesPerRow = bytesPerRow;
this.RowsPerBlock = rowsPerBlock;
this.Width = width;
}
/// <summary>
@ -45,6 +49,11 @@ internal abstract class ExrBaseCompression : IDisposable
/// </summary>
public uint BytesPerBlock { get; }
/// <summary>
/// Gets the number of pixel rows per block.
/// </summary>
public uint RowsPerBlock { get; }
/// <summary>
/// Gets the image width.
/// </summary>

49
src/ImageSharp/Formats/Exr/Compression/ExrBaseDecompressor.cs

@ -1,6 +1,8 @@
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using System.IO.Compression;
using SixLabors.ImageSharp.Compression.Zlib;
using SixLabors.ImageSharp.IO;
using SixLabors.ImageSharp.Memory;
@ -17,8 +19,10 @@ internal abstract class ExrBaseDecompressor : ExrBaseCompression
/// <param name="allocator">The memory allocator.</param>
/// <param name="bytesPerBlock">The bytes per row block.</param>
/// <param name="bytesPerRow">The bytes per row.</param>
protected ExrBaseDecompressor(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow)
: base(allocator, bytesPerBlock, bytesPerRow)
/// <param name="rowsPerBlock">The pixel rows per block.</param>
/// <param name="width">The number of pixels per row.</param>
protected ExrBaseDecompressor(MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width)
: base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width)
{
}
@ -30,6 +34,47 @@ internal abstract class ExrBaseDecompressor : ExrBaseCompression
/// <param name="buffer">The buffer to write the decompressed data to.</param>
public abstract void Decompress(BufferedReadStream stream, uint compressedBytes, Span<byte> buffer);
/// <summary>
/// Decompresses zip compressed data.
/// </summary>
/// <param name="stream">The buffered stream to decompress.</param>
/// <param name="compressedBytes">The compressed bytes.</param>
/// <param name="uncompressed">The buffer to write the uncompressed data to.</param>
/// <param name="uncompressedBytes">The uncompressed bytes.</param>
/// <returns>The total bytes read from the stream.</returns>
protected static int UndoZipCompression(BufferedReadStream stream, uint compressedBytes, Span<byte> uncompressed, uint uncompressedBytes)
{
long pos = stream.Position;
using ZlibInflateStream inflateStream = new(
stream,
() =>
{
int left = (int)(compressedBytes - (stream.Position - pos));
return left > 0 ? left : 0;
});
inflateStream.AllocateNewBytes((int)compressedBytes, true);
using DeflateStream dataStream = inflateStream.CompressedStream!;
int totalRead = 0;
while (totalRead < uncompressedBytes)
{
int bytesRead = dataStream.Read(uncompressed, totalRead, (int)uncompressedBytes - totalRead);
if (bytesRead <= 0)
{
break;
}
totalRead += bytesRead;
}
if (totalRead == 0)
{
ExrThrowHelper.ThrowInvalidImageContentException("Could not read enough data for zip compressed EXR image data!");
}
return totalRead;
}
/// <summary>
/// Integrate over all differences to the previous value in order to
/// reconstruct sample values.

10
src/ImageSharp/Formats/Exr/Compression/ExrCompressorFactory.cs

@ -21,6 +21,8 @@ internal static class ExrCompressorFactory
/// <param name="output">The output stream.</param>
/// <param name="bytesPerBlock">The bytes per block.</param>
/// <param name="bytesPerRow">The bytes per row.</param>
/// <param name="rowsPerBlock">The pixel rows per block.</param>
/// <param name="width">The witdh of one row in pixels.</param>
/// <param name="compressionLevel">The deflate compression level.</param>
/// <returns>A compressor for EXR image data.</returns>
public static ExrBaseCompressor Create(
@ -29,11 +31,13 @@ internal static class ExrCompressorFactory
Stream output,
uint bytesPerBlock,
uint bytesPerRow,
uint rowsPerBlock,
int width,
DeflateCompressionLevel compressionLevel = DeflateCompressionLevel.DefaultCompression) => method switch
{
ExrCompression.None => new NoneExrCompressor(output, allocator, bytesPerBlock, bytesPerRow),
ExrCompression.Zips => new ZipExrCompressor(output, allocator, bytesPerBlock, bytesPerRow, compressionLevel),
ExrCompression.Zip => new ZipExrCompressor(output, allocator, bytesPerBlock, bytesPerRow, compressionLevel),
ExrCompression.None => new NoneExrCompressor(output, allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width),
ExrCompression.Zips => new ZipExrCompressor(output, allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width, compressionLevel),
ExrCompression.Zip => new ZipExrCompressor(output, allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width, compressionLevel),
_ => throw ExrThrowHelper.NotSupportedCompressor(method.ToString()),
};
}

13
src/ImageSharp/Formats/Exr/Compression/ExrDecompressorFactory.cs

@ -22,6 +22,7 @@ internal static class ExrDecompressorFactory
/// <param name="bytesPerRow">The bytes per row.</param>
/// <param name="rowsPerBlock">The rows per block.</param>
/// <param name="channelCount">The number of image channels.</param>
/// <param name="pixelType">The pixel type.</param>
/// <returns>Decompressor for EXR image data.</returns>
public static ExrBaseDecompressor Create(
ExrCompression method,
@ -30,13 +31,15 @@ internal static class ExrDecompressorFactory
uint bytesPerBlock,
uint bytesPerRow,
uint rowsPerBlock,
int channelCount) => method switch
int channelCount,
ExrPixelType pixelType) => method switch
{
ExrCompression.None => new NoneExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow),
ExrCompression.Zips => new ZipExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow),
ExrCompression.Zip => new ZipExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow),
ExrCompression.RunLengthEncoded => new RunLengthExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow),
ExrCompression.None => new NoneExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width),
ExrCompression.Zips => new ZipExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width),
ExrCompression.Zip => new ZipExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width),
ExrCompression.RunLengthEncoded => new RunLengthExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width),
ExrCompression.B44 => new B44ExrCompression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width, channelCount),
ExrCompression.Pxr24 => new Pxr24Compression(memoryAllocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width, channelCount, pixelType),
_ => throw ExrThrowHelper.NotSupportedDecompressor(nameof(method)),
};
}

6
src/ImageSharp/Formats/Exr/ExrBaseCompressor.cs

@ -14,8 +14,10 @@ internal abstract class ExrBaseCompressor : ExrBaseCompression
/// <param name="allocator">The memory allocator.</param>
/// <param name="bytesPerBlock">Bytes per row block.</param>
/// <param name="bytesPerRow">Bytes per pixel row.</param>
protected ExrBaseCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow)
: base(allocator, bytesPerBlock, bytesPerRow)
/// <param name="rowsPerBlock">The pixel rows per block.</param>
/// <param name="width">The number of pixels per row.</param>
protected ExrBaseCompressor(Stream output, MemoryAllocator allocator, uint bytesPerBlock, uint bytesPerRow, uint rowsPerBlock, int width)
: base(allocator, bytesPerBlock, bytesPerRow, rowsPerBlock, width)
=> this.Output = output;
/// <summary>

22
src/ImageSharp/Formats/Exr/ExrDecoderCore.cs

@ -154,7 +154,15 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
Span<float> bluePixelData = rowBuffer.GetSpan().Slice(width * 2, width);
Span<float> alphaPixelData = rowBuffer.GetSpan().Slice(width * 3, width);
using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, width, bytesPerBlock, bytesPerRow, rowsPerBlock, channelCount);
using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(
this.Compression,
this.memoryAllocator,
width,
bytesPerBlock,
bytesPerRow,
rowsPerBlock,
channelCount,
this.PixelType);
int decodedRows = 0;
while (decodedRows < height)
@ -219,7 +227,15 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
Span<uint> bluePixelData = rowBuffer.GetSpan().Slice(width * 2, width);
Span<uint> alphaPixelData = rowBuffer.GetSpan().Slice(width * 3, width);
using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(this.Compression, this.memoryAllocator, width, bytesPerBlock, bytesPerRow, rowsPerBlock, channelCount);
using ExrBaseDecompressor decompressor = ExrDecompressorFactory.Create(
this.Compression,
this.memoryAllocator,
width,
bytesPerBlock,
bytesPerRow,
rowsPerBlock,
channelCount,
this.PixelType);
int decodedRows = 0;
while (decodedRows < height)
@ -846,7 +862,7 @@ internal sealed class ExrDecoderCore : ImageDecoderCore
/// <returns> True if the compression is supported; otherwise, false>. </returns>
private bool IsSupportedCompression() => this.Compression switch
{
ExrCompression.None or ExrCompression.Zip or ExrCompression.Zips or ExrCompression.RunLengthEncoded or ExrCompression.B44 => true,
ExrCompression.None or ExrCompression.Zip or ExrCompression.Zips or ExrCompression.RunLengthEncoded or ExrCompression.B44 or ExrCompression.Pxr24 => true,
_ => false,
};

4
src/ImageSharp/Formats/Exr/ExrEncoderCore.cs

@ -181,7 +181,7 @@ internal sealed class ExrEncoderCore
Span<float> blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width);
Span<float> alphaBuffer = rgbBuffer.GetSpan().Slice(width * 3, width);
using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, bytesPerBlock, bytesPerRow);
using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, bytesPerBlock, bytesPerRow, rowsPerBlock, width);
ulong[] rowOffsets = new ulong[height];
for (uint y = 0; y < height; y += rowsPerBlock)
@ -273,7 +273,7 @@ internal sealed class ExrEncoderCore
Span<uint> blueBuffer = rgbBuffer.GetSpan().Slice(width * 2, width);
Span<uint> alphaBuffer = rgbBuffer.GetSpan().Slice(width * 3, width);
using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, bytesPerBlock, bytesPerRow);
using ExrBaseCompressor compressor = ExrCompressorFactory.Create(compression, this.memoryAllocator, stream, bytesPerBlock, bytesPerRow, rowsPerBlock, width);
Rgba128 rgb = default;
ulong[] rowOffsets = new ulong[height];

2
src/ImageSharp/Formats/Gif/GifEncoderCore.cs

@ -303,7 +303,7 @@ internal sealed class GifEncoderCore
this.WriteGraphicalControlExtension(metadata, stream);
Buffer2D<byte> indices = ((IPixelSource)quantized).PixelBuffer;
Rectangle interest = indices.FullRectangle();
Rectangle interest = indices.Bounds;
bool useLocal = this.colorTableMode == FrameColorTableMode.Local || (metadata.ColorTableMode == FrameColorTableMode.Local);
int bitDepth = ColorNumerics.GetBitsNeededForColorDepth(quantized.Palette.Length);

13
src/ImageSharp/ImageFrame.cs

@ -71,6 +71,19 @@ public abstract partial class ImageFrame : IConfigurationProvider, IDisposable
/// <param name="disposing">Whether to dispose of managed and unmanaged objects.</param>
protected abstract void Dispose(bool disposing);
/// <summary>
/// Accepts a <see cref="IImageFrameVisitor"/>.
/// Implemented by <see cref="ImageFrame{TPixel}"/> invoking <see cref="IImageFrameVisitor.Visit{TPixel}"/>
/// with the pixel type of the image.
/// </summary>
/// <param name="visitor">The visitor.</param>
internal abstract void Accept(IImageFrameVisitor visitor);
/// <summary>
/// Copies the pixel data of the image frame to a <see cref="Buffer2D{TDestinationPixel}"/> of a specific pixel type.
/// </summary>
/// <typeparam name="TDestinationPixel">The pixel type of the destination buffer.</typeparam>
/// <param name="destination">The buffer to copy the pixel data to.</param>
internal abstract void CopyPixelsTo<TDestinationPixel>(Buffer2D<TDestinationPixel> destination)
where TDestinationPixel : unmanaged, IPixel<TDestinationPixel>;

15
src/ImageSharp/ImageFrame{TPixel}.cs

@ -339,6 +339,10 @@ public sealed class ImageFrame<TPixel> : ImageFrame, IPixelSource<TPixel>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal ref TPixel GetPixelReference(int x, int y) => ref this.PixelBuffer[x, y];
/// <inheritdoc />
internal override void Accept(IImageFrameVisitor visitor)
=> visitor.Visit(this);
/// <summary>
/// Copies the pixels to a <see cref="Buffer2D{TPixel}"/> of the same size.
/// </summary>
@ -346,7 +350,7 @@ public sealed class ImageFrame<TPixel> : ImageFrame, IPixelSource<TPixel>
/// <exception cref="ArgumentException">ImageFrame{TPixel}.CopyTo(): target must be of the same size!</exception>
internal void CopyTo(Buffer2D<TPixel> target)
{
if (this.Size != target.Size())
if (this.Size != target.Size)
{
throw new ArgumentException("ImageFrame<TPixel>.CopyTo(): target must be of the same size!", nameof(target));
}
@ -363,8 +367,8 @@ public sealed class ImageFrame<TPixel> : ImageFrame, IPixelSource<TPixel>
{
Guard.NotNull(source, nameof(source));
Buffer2D<TPixel>.SwapOrCopyContent(this.PixelBuffer, source.PixelBuffer);
this.UpdateSize(this.PixelBuffer.Size());
_ = Buffer2D<TPixel>.SwapOrCopyContent(this.PixelBuffer, source.PixelBuffer);
this.UpdateSize(this.PixelBuffer.Size);
}
/// <summary>
@ -475,10 +479,7 @@ public sealed class ImageFrame<TPixel> : ImageFrame, IPixelSource<TPixel>
/// Clears the bitmap.
/// </summary>
/// <param name="value">The value to initialize the bitmap with.</param>
internal void Clear(TPixel value)
{
this.PixelBuffer.Clear(value);
}
internal void Clear(TPixel value) => this.PixelBuffer.Clear(value);
[MethodImpl(InliningOptions.ShortMethod)]
private void VerifyCoords(int x, int y)

1
src/ImageSharp/Image{TPixel}.cs

@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License.
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using SixLabors.ImageSharp.Advanced;
using SixLabors.ImageSharp.Memory;
using SixLabors.ImageSharp.Metadata;

180
src/ImageSharp/Memory/Allocators/MemoryAllocator.cs

@ -12,8 +12,6 @@ namespace SixLabors.ImageSharp.Memory;
public abstract class MemoryAllocator
{
private const int OneGigabyte = 1 << 30;
private long accumulativeAllocatedBytes;
private int trackingSuppressionCount;
/// <summary>
/// Gets the default platform-specific global <see cref="MemoryAllocator"/> instance that
@ -25,44 +23,9 @@ public abstract class MemoryAllocator
/// </summary>
public static MemoryAllocator Default { get; } = Create();
/// <summary>
/// Gets the maximum number of bytes that can be allocated by a memory group.
/// </summary>
/// <remarks>
/// The allocation limit is determined by the process architecture: 4 GB for 64-bit processes and
/// 1 GB for 32-bit processes.
/// </remarks>
internal long MemoryGroupAllocationLimitBytes { get; private protected set; } = Environment.Is64BitProcess ? 4L * OneGigabyte : OneGigabyte;
/// <summary>
/// Gets the maximum allowed total allocation size, in bytes, for the current process.
/// </summary>
/// <remarks>
/// Defaults to <see cref="long.MaxValue"/>, effectively imposing no limit on total allocations.
/// This property can be set to enforce a cap on total memory usage across all allocations made through this allocator instance, providing
/// a safeguard against excessive memory consumption.<br/>
/// When the cumulative size of active allocations exceeds this limit, an <see cref="InvalidMemoryOperationException"/> will be thrown to
/// prevent further allocations and signal that the limit has been breached.
/// </remarks>
internal long AccumulativeAllocationLimitBytes { get; private protected set; } = long.MaxValue;
internal long MemoryGroupAllocationLimitBytes { get; private set; } = Environment.Is64BitProcess ? 4L * OneGigabyte : OneGigabyte;
/// <summary>
/// Gets the maximum size, in bytes, that can be allocated for a single buffer.
/// </summary>
/// <remarks>
/// The single buffer allocation limit is set to 1 GB by default.
/// </remarks>
internal int SingleBufferAllocationLimitBytes { get; private protected set; } = OneGigabyte;
/// <summary>
/// Gets a value indicating whether change tracking is currently suppressed for this instance.
/// </summary>
/// <remarks>
/// When change tracking is suppressed, modifications to the object will not be recorded or
/// trigger change notifications. This property is used internally to temporarily disable tracking during
/// batch updates or initialization.
/// </remarks>
private bool IsTrackingSuppressed => Volatile.Read(ref this.trackingSuppressionCount) > 0;
internal int SingleBufferAllocationLimitBytes { get; private set; } = OneGigabyte;
/// <summary>
/// Gets the length of the largest contiguous buffer that can be handled by this allocator instance in bytes.
@ -90,11 +53,6 @@ public abstract class MemoryAllocator
allocator.SingleBufferAllocationLimitBytes = (int)Math.Min(allocator.SingleBufferAllocationLimitBytes, allocator.MemoryGroupAllocationLimitBytes);
}
if (options.AccumulativeAllocationLimitMegabytes.HasValue)
{
allocator.AccumulativeAllocationLimitBytes = options.AccumulativeAllocationLimitMegabytes.Value * 1024L * 1024L;
}
return allocator;
}
@ -114,10 +72,6 @@ public abstract class MemoryAllocator
/// Releases all retained resources not being in use.
/// Eg: by resetting array pools and letting GC to free the arrays.
/// </summary>
/// <remarks>
/// This does not dispose active allocations; callers are responsible for disposing all
/// <see cref="IMemoryOwner{T}"/> instances to release memory.
/// </remarks>
public virtual void ReleaseRetainedResources()
{
}
@ -148,137 +102,11 @@ public abstract class MemoryAllocator
InvalidMemoryOperationException.ThrowAllocationOverLimitException(totalLengthInBytes, this.MemoryGroupAllocationLimitBytes);
}
long totalLengthInBytesLong = (long)totalLengthInBytes;
this.ReserveAllocation(totalLengthInBytesLong);
using (this.SuppressTracking())
{
try
{
MemoryGroup<T> group = this.AllocateGroupCore<T>(totalLength, totalLengthInBytesLong, bufferAlignment, options);
group.SetAllocationTracking(this, totalLengthInBytesLong);
return group;
}
catch
{
this.ReleaseAccumulatedBytes(totalLengthInBytesLong);
throw;
}
}
// Cast to long is safe because we already checked that the total length is within the limit.
return this.AllocateGroupCore<T>(totalLength, (long)totalLengthInBytes, bufferAlignment, options);
}
internal virtual MemoryGroup<T> AllocateGroupCore<T>(long totalLengthInElements, long totalLengthInBytes, int bufferAlignment, AllocationOptions options)
where T : struct
=> MemoryGroup<T>.Allocate(this, totalLengthInElements, bufferAlignment, options);
/// <summary>
/// Tracks the allocation of an <see cref="IMemoryOwner{T}" /> instance after reserving bytes.
/// </summary>
/// <typeparam name="T">Type of the data stored in the buffer.</typeparam>
/// <param name="owner">The allocation to track.</param>
/// <param name="lengthInBytes">The allocation size in bytes.</param>
/// <returns>The tracked allocation.</returns>
protected IMemoryOwner<T> TrackAllocation<T>(IMemoryOwner<T> owner, ulong lengthInBytes)
where T : struct
{
if (this.IsTrackingSuppressed || lengthInBytes == 0)
{
return owner;
}
return new TrackingMemoryOwner<T>(owner, this, (long)lengthInBytes);
}
/// <summary>
/// Reserves accumulative allocation bytes before creating the underlying buffer.
/// </summary>
/// <param name="lengthInBytes">The number of bytes to reserve.</param>
protected void ReserveAllocation(long lengthInBytes)
{
if (this.IsTrackingSuppressed || lengthInBytes <= 0)
{
return;
}
long total = Interlocked.Add(ref this.accumulativeAllocatedBytes, lengthInBytes);
if (total > this.AccumulativeAllocationLimitBytes)
{
_ = Interlocked.Add(ref this.accumulativeAllocatedBytes, -lengthInBytes);
InvalidMemoryOperationException.ThrowAllocationOverLimitException((ulong)lengthInBytes, this.AccumulativeAllocationLimitBytes);
}
}
/// <summary>
/// Releases accumulative allocation bytes previously tracked by this allocator.
/// </summary>
/// <param name="lengthInBytes">The number of bytes to release.</param>
internal void ReleaseAccumulatedBytes(long lengthInBytes)
{
if (lengthInBytes <= 0)
{
return;
}
_ = Interlocked.Add(ref this.accumulativeAllocatedBytes, -lengthInBytes);
}
/// <summary>
/// Suppresses accumulative allocation tracking for the lifetime of the returned scope.
/// </summary>
/// <returns>An <see cref="IDisposable"/> that restores tracking when disposed.</returns>
internal IDisposable SuppressTracking() => new TrackingSuppressionScope(this);
/// <summary>
/// Temporarily suppresses accumulative allocation tracking within a scope.
/// </summary>
private sealed class TrackingSuppressionScope : IDisposable
{
private MemoryAllocator? allocator;
public TrackingSuppressionScope(MemoryAllocator allocator)
{
this.allocator = allocator;
_ = Interlocked.Increment(ref allocator.trackingSuppressionCount);
}
public void Dispose()
{
if (this.allocator != null)
{
_ = Interlocked.Decrement(ref this.allocator.trackingSuppressionCount);
this.allocator = null;
}
}
}
/// <summary>
/// Wraps an <see cref="IMemoryOwner{T}"/> to release accumulative tracking on dispose.
/// </summary>
private sealed class TrackingMemoryOwner<T> : IMemoryOwner<T>
where T : struct
{
private IMemoryOwner<T>? owner;
private readonly MemoryAllocator allocator;
private readonly long lengthInBytes;
public TrackingMemoryOwner(IMemoryOwner<T> owner, MemoryAllocator allocator, long lengthInBytes)
{
this.owner = owner;
this.allocator = allocator;
this.lengthInBytes = lengthInBytes;
}
public Memory<T> Memory => this.owner?.Memory ?? Memory<T>.Empty;
public void Dispose()
{
// Ensure only one caller disposes the inner owner and releases the reservation.
IMemoryOwner<T>? inner = Interlocked.Exchange(ref this.owner, null);
if (inner != null)
{
inner.Dispose();
this.allocator.ReleaseAccumulatedBytes(this.lengthInBytes);
}
}
}
}

30
src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs

@ -10,7 +10,6 @@ public struct MemoryAllocatorOptions
{
private int? maximumPoolSizeMegabytes;
private int? allocationLimitMegabytes;
private int? accumulativeAllocationLimitMegabytes;
/// <summary>
/// Gets or sets a value defining the maximum size of the <see cref="MemoryAllocator"/>'s internal memory pool
@ -18,7 +17,7 @@ public struct MemoryAllocatorOptions
/// </summary>
public int? MaximumPoolSizeMegabytes
{
readonly get => this.maximumPoolSizeMegabytes;
get => this.maximumPoolSizeMegabytes;
set
{
if (value.HasValue)
@ -36,7 +35,7 @@ public struct MemoryAllocatorOptions
/// </summary>
public int? AllocationLimitMegabytes
{
readonly get => this.allocationLimitMegabytes;
get => this.allocationLimitMegabytes;
set
{
if (value.HasValue)
@ -47,29 +46,4 @@ public struct MemoryAllocatorOptions
this.allocationLimitMegabytes = value;
}
}
/// <summary>
/// Gets or sets a value defining the maximum total size that can be allocated by the allocator in Megabytes.
/// <see langword="null"/> means platform default: 2GB on 32-bit processes, 8GB on 64-bit processes.
/// </summary>
public int? AccumulativeAllocationLimitMegabytes
{
readonly get => this.accumulativeAllocationLimitMegabytes;
set
{
if (value.HasValue)
{
Guard.MustBeGreaterThan(value.Value, 0, nameof(this.AccumulativeAllocationLimitMegabytes));
if (this.AllocationLimitMegabytes.HasValue)
{
Guard.MustBeGreaterThanOrEqualTo(
value.Value,
this.AllocationLimitMegabytes.Value,
nameof(this.AccumulativeAllocationLimitMegabytes));
}
}
this.accumulativeAllocationLimitMegabytes = value;
}
}
}

40
src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs

@ -12,32 +12,6 @@ namespace SixLabors.ImageSharp.Memory;
/// </summary>
public sealed class SimpleGcMemoryAllocator : MemoryAllocator
{
/// <summary>
/// Initializes a new instance of the <see cref="SimpleGcMemoryAllocator"/> class with default limits.
/// </summary>
public SimpleGcMemoryAllocator()
: this(default)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SimpleGcMemoryAllocator"/> class with custom limits.
/// </summary>
/// <param name="options">The <see cref="MemoryAllocatorOptions"/> to apply.</param>
public SimpleGcMemoryAllocator(MemoryAllocatorOptions options)
{
if (options.AllocationLimitMegabytes.HasValue)
{
this.MemoryGroupAllocationLimitBytes = options.AllocationLimitMegabytes.Value * 1024L * 1024L;
this.SingleBufferAllocationLimitBytes = (int)Math.Min(this.SingleBufferAllocationLimitBytes, this.MemoryGroupAllocationLimitBytes);
}
if (options.AccumulativeAllocationLimitMegabytes.HasValue)
{
this.AccumulativeAllocationLimitBytes = options.AccumulativeAllocationLimitMegabytes.Value * 1024L * 1024L;
}
}
/// <inheritdoc />
protected internal override int GetBufferCapacityInBytes() => int.MaxValue;
@ -55,18 +29,6 @@ public sealed class SimpleGcMemoryAllocator : MemoryAllocator
InvalidMemoryOperationException.ThrowAllocationOverLimitException(lengthInBytes, this.SingleBufferAllocationLimitBytes);
}
long lengthInBytesLong = (long)lengthInBytes;
this.ReserveAllocation(lengthInBytesLong);
try
{
IMemoryOwner<T> buffer = new BasicArrayBuffer<T>(new T[length]);
return this.TrackAllocation(buffer, lengthInBytes);
}
catch
{
this.ReleaseAccumulatedBytes(lengthInBytesLong);
throw;
}
return new BasicArrayBuffer<T>(new T[length]);
}
}

58
src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs

@ -92,24 +92,13 @@ internal sealed class UniformUnmanagedMemoryPoolMemoryAllocator : MemoryAllocato
if (lengthInBytes <= (ulong)this.sharedArrayPoolThresholdInBytes)
{
long lengthInBytesLong = (long)lengthInBytes;
this.ReserveAllocation(lengthInBytesLong);
try
{
SharedArrayPoolBuffer<T> buffer = new(length);
if (options.Has(AllocationOptions.Clean))
{
buffer.GetSpan().Clear();
}
return this.TrackAllocation(buffer, lengthInBytes);
}
catch
SharedArrayPoolBuffer<T> buffer = new(length);
if (options.Has(AllocationOptions.Clean))
{
this.ReleaseAccumulatedBytes(lengthInBytesLong);
throw;
buffer.GetSpan().Clear();
}
return buffer;
}
if (lengthInBytes <= (ulong)this.poolBufferSizeInBytes)
@ -117,38 +106,12 @@ internal sealed class UniformUnmanagedMemoryPoolMemoryAllocator : MemoryAllocato
UnmanagedMemoryHandle mem = this.pool.Rent();
if (mem.IsValid)
{
long lengthInBytesLong = (long)lengthInBytes;
this.ReserveAllocation(lengthInBytesLong);
try
{
UnmanagedBuffer<T> buffer = this.pool.CreateGuardedBuffer<T>(mem, length, options.Has(AllocationOptions.Clean));
return this.TrackAllocation(buffer, lengthInBytes);
}
catch
{
this.ReleaseAccumulatedBytes(lengthInBytesLong);
throw;
}
UnmanagedBuffer<T> buffer = this.pool.CreateGuardedBuffer<T>(mem, length, options.Has(AllocationOptions.Clean));
return buffer;
}
}
long nonPooledLengthInBytesLong = (long)lengthInBytes;
this.ReserveAllocation(nonPooledLengthInBytesLong);
try
{
using (this.nonPoolAllocator.SuppressTracking())
{
IMemoryOwner<T> nonPooled = this.nonPoolAllocator.Allocate<T>(length, options);
return this.TrackAllocation(nonPooled, lengthInBytes);
}
}
catch
{
this.ReleaseAccumulatedBytes(nonPooledLengthInBytesLong);
throw;
}
return this.nonPoolAllocator.Allocate<T>(length, options);
}
/// <inheritdoc />
@ -181,10 +144,7 @@ internal sealed class UniformUnmanagedMemoryPoolMemoryAllocator : MemoryAllocato
return poolGroup;
}
using (this.nonPoolAllocator.SuppressTracking())
{
return MemoryGroup<T>.Allocate(this.nonPoolAllocator, totalLengthInElements, bufferAlignment, options);
}
return MemoryGroup<T>.Allocate(this.nonPoolAllocator, totalLengthInElements, bufferAlignment, options);
}
public override void ReleaseRetainedResources() => this.pool.Release();

24
src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs

@ -2,7 +2,6 @@
// Licensed under the Six Labors Split License.
using System.Buffers;
using System.Runtime.CompilerServices;
using SixLabors.ImageSharp.Memory.Internals;
namespace SixLabors.ImageSharp.Memory;
@ -20,26 +19,13 @@ internal class UnmanagedMemoryAllocator : MemoryAllocator
protected internal override int GetBufferCapacityInBytes() => this.bufferCapacityInBytes;
public override IMemoryOwner<T> Allocate<T>(int length, AllocationOptions options = AllocationOptions.None)
where T : struct
{
ulong lengthInBytes = (ulong)length * (ulong)Unsafe.SizeOf<T>();
long lengthInBytesLong = (long)lengthInBytes;
this.ReserveAllocation(lengthInBytesLong);
try
UnmanagedBuffer<T> buffer = UnmanagedBuffer<T>.Allocate(length);
if (options.Has(AllocationOptions.Clean))
{
UnmanagedBuffer<T> buffer = UnmanagedBuffer<T>.Allocate(length);
if (options.Has(AllocationOptions.Clean))
{
buffer.GetSpan().Clear();
}
return this.TrackAllocation(buffer, lengthInBytes);
}
catch
{
this.ReleaseAccumulatedBytes(lengthInBytesLong);
throw;
buffer.GetSpan().Clear();
}
return buffer;
}
}

50
src/ImageSharp/Memory/Buffer2DExtensions.cs

@ -104,60 +104,40 @@ public static class Buffer2DExtensions
}
/// <summary>
/// Returns a <see cref="Rectangle"/> representing the full area of the buffer.
/// </summary>
/// <typeparam name="T">The element type</typeparam>
/// <param name="buffer">The <see cref="Buffer2D{T}"/></param>
/// <returns>The <see cref="Rectangle"/></returns>
internal static Rectangle FullRectangle<T>(this Buffer2D<T> buffer)
where T : struct
=> new(0, 0, buffer.Width, buffer.Height);
/// <summary>
/// Return a <see cref="Buffer2DRegion{T}"/> to the subregion represented by 'rectangle'
/// Return a <see cref="Buffer2DRegion{T}"/> to the subregion represented by <paramref name="rectangle"/>.
/// </summary>
/// <typeparam name="T">The element type</typeparam>
/// <param name="buffer">The <see cref="Buffer2D{T}"/></param>
/// <param name="rectangle">The rectangle subregion</param>
/// <returns>The <see cref="Buffer2DRegion{T}"/></returns>
internal static Buffer2DRegion<T> GetRegion<T>(this Buffer2D<T> buffer, Rectangle rectangle)
public static Buffer2DRegion<T> GetRegion<T>(this Buffer2D<T> buffer, Rectangle rectangle)
where T : unmanaged =>
new(buffer, rectangle);
internal static Buffer2DRegion<T> GetRegion<T>(this Buffer2D<T> buffer, int x, int y, int width, int height)
/// <summary>
/// Return a <see cref="Buffer2DRegion{T}"/> to the specified area of <paramref name="buffer"/>.
/// </summary>
/// <typeparam name="T">The element type.</typeparam>
/// <param name="buffer">The <see cref="Buffer2D{T}"/>.</param>
/// <param name="x">The X coordinate of the region.</param>
/// <param name="y">The Y coordinate of the region.</param>
/// <param name="width">The region width.</param>
/// <param name="height">The region height.</param>
/// <returns>The <see cref="Buffer2DRegion{T}"/>.</returns>
public static Buffer2DRegion<T> GetRegion<T>(this Buffer2D<T> buffer, int x, int y, int width, int height)
where T : unmanaged =>
new(buffer, new Rectangle(x, y, width, height));
/// <summary>
/// Return a <see cref="Buffer2DRegion{T}"/> to the whole area of 'buffer'
/// Return a <see cref="Buffer2DRegion{T}"/> to the whole area of <paramref name="buffer"/>.
/// </summary>
/// <typeparam name="T">The element type</typeparam>
/// <param name="buffer">The <see cref="Buffer2D{T}"/></param>
/// <returns>The <see cref="Buffer2DRegion{T}"/></returns>
internal static Buffer2DRegion<T> GetRegion<T>(this Buffer2D<T> buffer)
public static Buffer2DRegion<T> GetRegion<T>(this Buffer2D<T> buffer)
where T : unmanaged =>
new(buffer);
/// <summary>
/// Returns the size of the buffer.
/// </summary>
/// <typeparam name="T">The element type</typeparam>
/// <param name="buffer">The <see cref="Buffer2D{T}"/></param>
/// <returns>The <see cref="Size{T}"/> of the buffer</returns>
internal static Size Size<T>(this Buffer2D<T> buffer)
where T : struct =>
new(buffer.Width, buffer.Height);
/// <summary>
/// Gets the bounds of the buffer.
/// </summary>
/// <typeparam name="T">The element type</typeparam>
/// <param name="buffer">The <see cref="Buffer2D{T}"/></param>
/// <returns>The <see cref="Rectangle"/></returns>
internal static Rectangle Bounds<T>(this Buffer2D<T> buffer)
where T : struct =>
new(0, 0, buffer.Width, buffer.Height);
[Conditional("DEBUG")]
private static void CheckColumnRegionsDoNotOverlap<T>(
Buffer2D<T> buffer,

60
src/ImageSharp/Memory/Buffer2DRegion{T}.cs

@ -15,17 +15,17 @@ public readonly struct Buffer2DRegion<T>
/// Initializes a new instance of the <see cref="Buffer2DRegion{T}"/> struct.
/// </summary>
/// <param name="buffer">The <see cref="Buffer2D{T}"/>.</param>
/// <param name="rectangle">The <see cref="Rectangle"/> defining a rectangular area within the buffer.</param>
/// <param name="bounds">The <see cref="Bounds"/> defining a rectangular area within the buffer.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Buffer2DRegion(Buffer2D<T> buffer, Rectangle rectangle)
public Buffer2DRegion(Buffer2D<T> buffer, Rectangle bounds)
{
DebugGuard.MustBeGreaterThanOrEqualTo(rectangle.X, 0, nameof(rectangle));
DebugGuard.MustBeGreaterThanOrEqualTo(rectangle.Y, 0, nameof(rectangle));
DebugGuard.MustBeLessThanOrEqualTo(rectangle.Width, buffer.Width, nameof(rectangle));
DebugGuard.MustBeLessThanOrEqualTo(rectangle.Height, buffer.Height, nameof(rectangle));
DebugGuard.MustBeGreaterThanOrEqualTo(bounds.X, 0, nameof(bounds));
DebugGuard.MustBeGreaterThanOrEqualTo(bounds.Y, 0, nameof(bounds));
DebugGuard.MustBeLessThanOrEqualTo(bounds.Width, buffer.Width, nameof(bounds));
DebugGuard.MustBeLessThanOrEqualTo(bounds.Height, buffer.Height, nameof(bounds));
this.Buffer = buffer;
this.Rectangle = rectangle;
this.Bounds = bounds;
}
/// <summary>
@ -34,15 +34,10 @@ public readonly struct Buffer2DRegion<T>
/// <param name="buffer">The <see cref="Buffer2D{T}"/>.</param>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Buffer2DRegion(Buffer2D<T> buffer)
: this(buffer, buffer.FullRectangle())
: this(buffer, buffer.Bounds)
{
}
/// <summary>
/// Gets the rectangle specifying the boundaries of the area in <see cref="Buffer"/>.
/// </summary>
public Rectangle Rectangle { get; }
/// <summary>
/// Gets the <see cref="Buffer2D{T}"/> being pointed by this instance.
/// </summary>
@ -51,12 +46,12 @@ public readonly struct Buffer2DRegion<T>
/// <summary>
/// Gets the width
/// </summary>
public int Width => this.Rectangle.Width;
public int Width => this.Bounds.Width;
/// <summary>
/// Gets the height
/// </summary>
public int Height => this.Rectangle.Height;
public int Height => this.Bounds.Height;
/// <summary>
/// Gets the number of elements between row starts in <see cref="Buffer"/>.
@ -66,12 +61,17 @@ public readonly struct Buffer2DRegion<T>
/// <summary>
/// Gets the size of the area.
/// </summary>
internal Size Size => this.Rectangle.Size;
public Size Size => this.Bounds.Size;
/// <summary>
/// Gets the rectangle specifying the boundaries of the area in <see cref="Buffer"/>.
/// </summary>
public Rectangle Bounds { get; }
/// <summary>
/// Gets a value indicating whether the area refers to the entire <see cref="Buffer"/>
/// </summary>
internal bool IsFullBufferArea => this.Size == this.Buffer.Size();
internal bool IsFullBufferArea => this.Size == this.Buffer.Size;
/// <summary>
/// Gets or sets a value at the given index.
@ -79,7 +79,7 @@ public readonly struct Buffer2DRegion<T>
/// <param name="x">The position inside a row</param>
/// <param name="y">The row index</param>
/// <returns>The reference to the value</returns>
internal ref T this[int x, int y] => ref this.Buffer[x + this.Rectangle.X, y + this.Rectangle.Y];
internal ref T this[int x, int y] => ref this.Buffer[x + this.Bounds.X, y + this.Bounds.Y];
/// <summary>
/// Gets a span to row 'y' inside this area.
@ -89,9 +89,9 @@ public readonly struct Buffer2DRegion<T>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Span<T> DangerousGetRowSpan(int y)
{
int yy = this.Rectangle.Y + y;
int xx = this.Rectangle.X;
int width = this.Rectangle.Width;
int yy = this.Bounds.Y + y;
int xx = this.Bounds.X;
int width = this.Bounds.Width;
return this.Buffer.DangerousGetRowSpan(yy).Slice(xx, width);
}
@ -114,16 +114,16 @@ public readonly struct Buffer2DRegion<T>
/// <summary>
/// Returns a subregion as <see cref="Buffer2DRegion{T}"/>. (Similar to <see cref="Span{T}.Slice(int, int)"/>.)
/// </summary>
/// <param name="rectangle">The <see cref="Rectangle"/> specifying the boundaries of the subregion</param>
/// <param name="rectangle">The <see cref="Bounds"/> specifying the boundaries of the subregion</param>
/// <returns>The subregion</returns>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public Buffer2DRegion<T> GetSubRegion(Rectangle rectangle)
{
DebugGuard.MustBeLessThanOrEqualTo(rectangle.Width, this.Rectangle.Width, nameof(rectangle));
DebugGuard.MustBeLessThanOrEqualTo(rectangle.Height, this.Rectangle.Height, nameof(rectangle));
DebugGuard.MustBeLessThanOrEqualTo(rectangle.Width, this.Bounds.Width, nameof(rectangle));
DebugGuard.MustBeLessThanOrEqualTo(rectangle.Height, this.Bounds.Height, nameof(rectangle));
int x = this.Rectangle.X + rectangle.X;
int y = this.Rectangle.Y + rectangle.Y;
int x = this.Bounds.X + rectangle.X;
int y = this.Bounds.Y + rectangle.Y;
rectangle = new Rectangle(x, y, rectangle.Width, rectangle.Height);
return new Buffer2DRegion<T>(this.Buffer, rectangle);
}
@ -135,8 +135,8 @@ public readonly struct Buffer2DRegion<T>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal ref T GetReferenceToOrigin()
{
int y = this.Rectangle.Y;
int x = this.Rectangle.X;
int y = this.Bounds.Y;
int x = this.Bounds.X;
return ref this.Buffer.DangerousGetRowSpan(y)[x];
}
@ -152,7 +152,7 @@ public readonly struct Buffer2DRegion<T>
return;
}
for (int y = 0; y < this.Rectangle.Height; y++)
for (int y = 0; y < this.Bounds.Height; y++)
{
Span<T> row = this.DangerousGetRowSpan(y);
row.Clear();
@ -172,7 +172,7 @@ public readonly struct Buffer2DRegion<T>
return;
}
for (int y = 0; y < this.Rectangle.Height; y++)
for (int y = 0; y < this.Bounds.Height; y++)
{
Span<T> row = this.DangerousGetRowSpan(y);
row.Fill(value);

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

@ -39,20 +39,30 @@ public sealed class Buffer2D<T> : IDisposable
Guard.MustBeGreaterThanOrEqualTo(rowStride, width, nameof(rowStride));
this.FastMemoryGroup = memoryGroup;
this.Width = width;
this.Height = height;
this.Size = new Size(width, height);
this.RowStride = rowStride;
}
/// <summary>
/// Gets the width.
/// </summary>
public int Width { get; private set; }
public int Width => this.Size.Width;
/// <summary>
/// Gets the height.
/// </summary>
public int Height { get; private set; }
public int Height => this.Size.Height;
/// <summary>
/// Gets the size of the buffer.
/// </summary>
public Size Size { get; private set; }
/// <summary>
/// Gets the bounds of the buffer.
/// </summary>
/// <returns>The <see cref="Rectangle"/></returns>
public Rectangle Bounds => new(0, 0, this.Width, this.Height);
/// <summary>
/// Gets the number of elements between row starts in the backing memory.
@ -402,8 +412,7 @@ public sealed class Buffer2D<T> : IDisposable
source.CopyTo(destination);
}
(destination.Width, source.Width) = (source.Width, destination.Width);
(destination.Height, source.Height) = (source.Height, destination.Height);
(destination.Size, source.Size) = (source.Size, destination.Size);
(destination.RowStride, source.RowStride) = (source.RowStride, destination.RowStride);
return swapped;
}

8
src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs

@ -15,12 +15,12 @@ public interface IMemoryGroup<T> : IReadOnlyList<Memory<T>>
/// Gets the number of elements per contiguous sub-buffer preceding the last buffer.
/// The last buffer is allowed to be smaller.
/// </summary>
public int BufferLength { get; }
int BufferLength { get; }
/// <summary>
/// Gets the aggregate number of elements in the group.
/// </summary>
public long TotalLength { get; }
long TotalLength { get; }
/// <summary>
/// Gets a value indicating whether the group has been invalidated.
@ -29,7 +29,7 @@ public interface IMemoryGroup<T> : IReadOnlyList<Memory<T>>
/// Invalidation usually occurs when an image processor capable to alter the image dimensions replaces
/// the image buffers internally.
/// </remarks>
public bool IsValid { get; }
bool IsValid { get; }
/// <summary>
/// Returns a value-type implementing an allocation-free enumerator of the memory groups in the current
@ -39,5 +39,5 @@ public interface IMemoryGroup<T> : IReadOnlyList<Memory<T>>
/// implementation, which is still available when casting to one of the underlying interfaces.
/// </summary>
/// <returns>A new <see cref="MemoryGroupEnumerator{T}"/> instance mapping the current <see cref="Memory{T}"/> values in use.</returns>
public new MemoryGroupEnumerator<T> GetEnumerator();
new MemoryGroupEnumerator<T> GetEnumerator();
}

16
src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs

@ -31,23 +31,23 @@ internal abstract partial class MemoryGroup<T>
/// <inheritdoc/>
[MethodImpl(InliningOptions.ShortMethod)]
public override MemoryGroupEnumerator<T> GetEnumerator() => new(this);
public override MemoryGroupEnumerator<T> GetEnumerator()
{
return new MemoryGroupEnumerator<T>(this);
}
/// <inheritdoc/>
IEnumerator<Memory<T>> IEnumerable<Memory<T>>.GetEnumerator()
{
/* The runtime sees the Array class as if it implemented the
* type-generic collection interfaces explicitly, so here we
* can just cast the source array to IList<Memory<T>> (or to
* an equivalent type), and invoke the generic GetEnumerator
* method directly from that interface reference. This saves
* having to create our own iterator block here. */
=> ((IList<Memory<T>>)this.source).GetEnumerator();
public override void Dispose()
{
this.View.Invalidate();
this.ReleaseAllocationTracking();
return ((IList<Memory<T>>)this.source).GetEnumerator();
}
public override void Dispose() => this.View.Invalidate();
}
}

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

@ -73,8 +73,8 @@ internal abstract partial class MemoryGroup<T>
result[i] = currentBuffer;
}
ObservedBuffer lastBuffer = ObservedBuffer.Create(pooledBuffers[^1], sizeOfLastBuffer, options);
result[^1] = lastBuffer;
ObservedBuffer lastBuffer = ObservedBuffer.Create(pooledBuffers[pooledBuffers.Length - 1], sizeOfLastBuffer, options);
result[result.Length - 1] = lastBuffer;
return result;
}
@ -155,7 +155,6 @@ internal abstract partial class MemoryGroup<T>
}
}
this.ReleaseAllocationTracking();
this.memoryOwners = null;
this.IsValid = false;
this.groupLifetimeGuard = null;

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

@ -22,9 +22,6 @@ internal abstract partial class MemoryGroup<T> : IMemoryGroup<T>, IDisposable
private static readonly int ElementSize = Unsafe.SizeOf<T>();
private MemoryGroupSpanCache memoryGroupSpanCache;
private MemoryAllocator? trackingAllocator;
private long trackingLengthInBytes;
private int trackingReleased;
private MemoryGroup(int bufferLength, long totalLength)
{
@ -55,45 +52,16 @@ internal abstract partial class MemoryGroup<T> : IMemoryGroup<T>, IDisposable
/// <inheritdoc />
public abstract MemoryGroupEnumerator<T> GetEnumerator();
/// <summary>
/// Configures allocation tracking by specifying the allocator and the length, in bytes, to be tracked.
/// </summary>
/// <param name="allocator">The memory allocator to use for tracking allocations.</param>
/// <param name="lengthInBytes">The length, in bytes, of the memory region to track. Must be greater than or equal to zero.</param>
/// <remarks>
/// Intended for initialization; callers should avoid changing tracking state concurrently with disposal.
/// </remarks>
internal void SetAllocationTracking(MemoryAllocator allocator, long lengthInBytes)
{
this.trackingAllocator = allocator;
this.trackingLengthInBytes = lengthInBytes;
}
/// <summary>
/// Releases any resources or tracking information associated with allocation tracking for this instance.
/// </summary>
/// <remarks>
/// This method is intended to be called when allocation tracking is no longer needed. It is safe
/// to call multiple times; subsequent calls after the first have no effect, even when called concurrently.
/// </remarks>
internal void ReleaseAllocationTracking()
{
if (Interlocked.Exchange(ref this.trackingReleased, 1) == 0 && this.trackingAllocator != null)
{
this.trackingAllocator.ReleaseAccumulatedBytes(this.trackingLengthInBytes);
this.trackingAllocator = null;
}
}
/// <inheritdoc />
IEnumerator<Memory<T>> IEnumerable<Memory<T>>.GetEnumerator()
{
/* This method is implemented in each derived class.
* Implementing the method here as non-abstract and throwing,
* then reimplementing it explicitly in each derived class, is
* a workaround for the lack of support for abstract explicit
* interface method implementations in C#. */
=> throw new NotImplementedException($"The type {this.GetType()} needs to override IEnumerable<Memory<T>>.GetEnumerator()");
throw new NotImplementedException($"The type {this.GetType()} needs to override IEnumerable<Memory<T>>.GetEnumerator()");
}
/// <inheritdoc />
IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable<Memory<T>>)this).GetEnumerator();

2
src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs

@ -59,7 +59,7 @@ internal sealed class ResizeWorker<TPixel> : IDisposable
{
this.configuration = configuration;
this.source = source;
this.sourceRectangle = source.Rectangle;
this.sourceRectangle = source.Bounds;
this.conversionModifiers = conversionModifiers;
this.horizontalKernelMap = horizontalKernelMap;
this.verticalKernelMap = verticalKernelMap;

26
tests/ImageSharp.Tests/Formats/Exr/ExrDecoderTests.cs

@ -36,8 +36,7 @@ public class ExrDecoderTests
ExrMetadata exrMetaData = image.Metadata.GetExrMetadata();
image.DebugSave(provider);
// There is a 0,0059% difference to the Reference decoder.
image.CompareToOriginal(provider, ImageComparer.Tolerant(0.0005f), ReferenceDecoder);
image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder);
Assert.Equal(ExrPixelType.Float, exrMetaData.PixelType);
}
@ -119,6 +118,29 @@ public class ExrDecoderTests
image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder);
}
[Theory]
[WithFile(TestImages.Exr.Pxr24Half, PixelTypes.Rgba32)]
[WithFile(TestImages.Exr.Pxr24Float, PixelTypes.Rgba32)]
public void ExrDecoder_CanDecode_Pxr24Compressed<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(ExrDecoder.Instance);
image.DebugSave(provider);
image.CompareToOriginal(provider, ImageComparer.Exact, ReferenceDecoder);
}
[Theory]
[WithFile(TestImages.Exr.Pxr24Uint, PixelTypes.Rgba32)]
public void ExrDecoder_CanDecode_Pxr24Compressed_ExrPixelType_Uint<TPixel>(TestImageProvider<TPixel> provider)
where TPixel : unmanaged, IPixel<TPixel>
{
using Image<TPixel> image = provider.GetImage(ExrDecoder.Instance);
image.DebugSave(provider);
// Compare to Reference here instead using the reference decoder, since uint pixel type is not supported by the Reference decoder.
image.CompareToReferenceOutput(provider);
}
[Theory]
[WithFile(TestImages.Exr.B44, PixelTypes.Rgba32)]
public void ExrDecoder_CanDecode_B44Compressed<TPixel>(TestImageProvider<TPixel> provider)

2
tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs

@ -119,7 +119,7 @@ public class SpectralJpegTests
this.Output.WriteLine($"Component{i}: [total: {total} | average: {average}]");
averageDifference += average;
totalDifference += total;
Size s = libJpegComponent.SpectralBlocks.Size();
Size s = libJpegComponent.SpectralBlocks.Size;
tolerance += s.Width * s.Height;
}

44
tests/ImageSharp.Tests/Memory/Allocators/SimpleGcMemoryAllocatorTests.cs

@ -48,50 +48,6 @@ public class SimpleGcMemoryAllocatorTests
}
}
[Fact]
public void Allocate_AccumulativeLimit_ReleasesOnOwnerDispose()
{
SimpleGcMemoryAllocator allocator = new(new MemoryAllocatorOptions
{
AccumulativeAllocationLimitMegabytes = 1
});
const int oneMb = 1 << 20;
// Reserve the full limit with a single owner.
IMemoryOwner<byte> b0 = allocator.Allocate<byte>(oneMb);
// Additional allocation should exceed the limit while the owner is live.
Assert.Throws<InvalidMemoryOperationException>(() => allocator.Allocate<byte>(1));
// Disposing the owner releases the reservation.
b0.Dispose();
// Allocation should succeed after the reservation is released.
allocator.Allocate<byte>(oneMb).Dispose();
}
[Fact]
public void AllocateGroup_AccumulativeLimit_ReleasesOnGroupDispose()
{
SimpleGcMemoryAllocator allocator = new(new MemoryAllocatorOptions
{
AccumulativeAllocationLimitMegabytes = 1
});
const int oneMb = 1 << 20;
// Reserve the full limit with a single group.
MemoryGroup<byte> g0 = allocator.AllocateGroup<byte>(oneMb, 1024);
// Additional allocation should exceed the limit while the group is live.
Assert.Throws<InvalidMemoryOperationException>(() => allocator.AllocateGroup<byte>(1, 1024));
// Disposing the group releases the reservation.
g0.Dispose();
// Allocation should succeed after the reservation is released.
allocator.AllocateGroup<byte>(oneMb, 1024).Dispose();
}
[StructLayout(LayoutKind.Explicit, Size = 512)]
private struct BigStruct
{

56
tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs

@ -16,8 +16,8 @@ public class UniformUnmanagedPoolMemoryAllocatorTests
{
public class BufferTests1 : BufferTestSuite
{
private static UniformUnmanagedMemoryPoolMemoryAllocator CreateMemoryAllocator() =>
new(
private static MemoryAllocator CreateMemoryAllocator() =>
new UniformUnmanagedMemoryPoolMemoryAllocator(
sharedArrayPoolThresholdInBytes: 1024,
poolBufferSizeInBytes: 2048,
maxPoolSizeInBytes: 2048 * 4,
@ -31,8 +31,8 @@ public class UniformUnmanagedPoolMemoryAllocatorTests
public class BufferTests2 : BufferTestSuite
{
private static UniformUnmanagedMemoryPoolMemoryAllocator CreateMemoryAllocator() =>
new(
private static MemoryAllocator CreateMemoryAllocator() =>
new UniformUnmanagedMemoryPoolMemoryAllocator(
sharedArrayPoolThresholdInBytes: 512,
poolBufferSizeInBytes: 1024,
maxPoolSizeInBytes: 1024 * 4,
@ -179,8 +179,8 @@ public class UniformUnmanagedPoolMemoryAllocatorTests
g1.Dispose();
// Do some unmanaged allocations to make sure new non-pooled unmanaged allocations will grab different memory:
IntPtr dummy1 = Marshal.AllocHGlobal(checked((IntPtr)B(8)));
IntPtr dummy2 = Marshal.AllocHGlobal(checked((IntPtr)B(8)));
IntPtr dummy1 = Marshal.AllocHGlobal((IntPtr)B(8));
IntPtr dummy2 = Marshal.AllocHGlobal((IntPtr)B(8));
using MemoryGroup<byte> g2 = allocator.AllocateGroup<byte>(B(8), 1024);
using MemoryGroup<byte> g3 = allocator.AllocateGroup<byte>(B(8), 1024);
@ -433,50 +433,6 @@ public class UniformUnmanagedPoolMemoryAllocatorTests
Assert.Throws<InvalidMemoryOperationException>(() => allocator.AllocateGroup<byte>(5 * oneMb, 1024));
}
[Fact]
public void Allocate_AccumulativeLimit_ReleasesOnOwnerDispose()
{
MemoryAllocator allocator = MemoryAllocator.Create(new MemoryAllocatorOptions
{
AccumulativeAllocationLimitMegabytes = 1
});
const int oneMb = 1 << 20;
// Reserve the full limit with a single owner.
IMemoryOwner<byte> b0 = allocator.Allocate<byte>(oneMb);
// Additional allocation should exceed the limit while the owner is live.
Assert.Throws<InvalidMemoryOperationException>(() => allocator.Allocate<byte>(1));
// Disposing the owner releases the reservation.
b0.Dispose();
// Allocation should succeed after the reservation is released.
allocator.Allocate<byte>(oneMb).Dispose();
}
[Fact]
public void AllocateGroup_AccumulativeLimit_ReleasesOnGroupDispose()
{
MemoryAllocator allocator = MemoryAllocator.Create(new MemoryAllocatorOptions
{
AccumulativeAllocationLimitMegabytes = 1
});
const int oneMb = 1 << 20;
// Reserve the full limit with a single group.
MemoryGroup<byte> g0 = allocator.AllocateGroup<byte>(oneMb, 1024);
// Additional allocation should exceed the limit while the group is live.
Assert.Throws<InvalidMemoryOperationException>(() => allocator.AllocateGroup<byte>(1, 1024));
// Disposing the group releases the reservation.
g0.Dispose();
// Allocation should succeed after the reservation is released.
allocator.AllocateGroup<byte>(oneMb, 1024).Dispose();
}
[ConditionalFact(typeof(Environment), nameof(Environment.Is64BitProcess))]
public void MemoryAllocator_Create_SetHighLimit()
{

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

@ -1,4 +1,4 @@
// Copyright (c) Six Labors.
// Copyright (c) Six Labors.
// Licensed under the Six Labors Split License.
using SixLabors.ImageSharp.Memory;
@ -15,26 +15,24 @@ public partial class Buffer2DTests
[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;
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();
Memory<int> aa = a.FastMemoryGroup.Single();
Memory<int> bb = b.FastMemoryGroup.Single();
Buffer2D<int>.SwapOrCopyContent(a, b);
Buffer2D<int>.SwapOrCopyContent(a, b);
Assert.Equal(bb, a.FastMemoryGroup.Single());
Assert.Equal(aa, b.FastMemoryGroup.Single());
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(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]);
}
Assert.Equal(666, b[1, 3]);
Assert.Equal(444, a[1, 3]);
}
[Fact]
@ -171,7 +169,7 @@ public partial class Buffer2DTests
bool swap = Buffer2D<int>.SwapOrCopyContent(dest, source);
Assert.False(swap);
Assert.Equal(new Size(3, 2), dest.Size());
Assert.Equal(new Size(3, 2), dest.Size);
Assert.Equal(6, dest[2, 1]);
}
@ -191,7 +189,7 @@ public partial class Buffer2DTests
bool swap = Buffer2D<int>.SwapOrCopyContent(dest, source);
Assert.False(swap);
Assert.Equal(new Size(1, 5), dest.Size());
Assert.Equal(new Size(1, 5), dest.Size);
Assert.Equal(1, dest[0, 0]);
Assert.Equal(2, dest[0, 1]);
Assert.Equal(3, dest[0, 2]);
@ -215,7 +213,7 @@ public partial class Buffer2DTests
bool swap = Buffer2D<int>.SwapOrCopyContent(dest, source);
Assert.False(swap);
Assert.Equal(new Size(2, 2), dest.Size());
Assert.Equal(new Size(2, 2), dest.Size);
Assert.Equal(1, dest[0, 0]);
Assert.Equal(2, dest[1, 0]);
Assert.Equal(3, dest[0, 1]);

8
tests/ImageSharp.Tests/Memory/BufferAreaTests.cs

@ -17,7 +17,7 @@ public class BufferAreaTests
Buffer2DRegion<int> area = new(buffer, rectangle);
Assert.Equal(buffer, area.Buffer);
Assert.Equal(rectangle, area.Rectangle);
Assert.Equal(rectangle, area.Bounds);
}
private Buffer2D<int> CreateTestBuffer(int w, int h)
@ -90,7 +90,7 @@ public class BufferAreaTests
Rectangle expectedRect = new(10, 12, 5, 5);
Assert.Equal(buffer, area1.Buffer);
Assert.Equal(expectedRect, area1.Rectangle);
Assert.Equal(expectedRect, area1.Bounds);
int value00 = (12 * 100) + 10;
Assert.Equal(value00, area1[0, 0]);
@ -147,9 +147,9 @@ public class BufferAreaTests
Assert.Equal(0, buffer[5, 5]);
Assert.Equal(0, buffer[14, 14]);
for (int y = region.Rectangle.Y; y < region.Rectangle.Bottom; y++)
for (int y = region.Bounds.Y; y < region.Bounds.Bottom; y++)
{
Span<int> span = buffer.DangerousGetRowSpan(y).Slice(region.Rectangle.X, region.Width);
Span<int> span = buffer.DangerousGetRowSpan(y).Slice(region.Bounds.X, region.Width);
Assert.True(span.SequenceEqual(new int[region.Width]));
}
}

10
tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.Allocate.cs

@ -98,15 +98,7 @@ public partial class MemoryGroupTests
[InlineData(AllocationOptions.Clean)]
public unsafe void Allocate_FromPool_AllocationOptionsAreApplied(AllocationOptions options)
{
// Disable trimming to avoid buffers being freed between Return and TryAllocate by the
// trim timer or the Gen2 GC callback.
UniformUnmanagedMemoryPool pool = new(
10,
5,
new UniformUnmanagedMemoryPool.TrimSettings
{
Rate = 0
});
UniformUnmanagedMemoryPool pool = new(10, 5);
UnmanagedMemoryHandle[] buffers = pool.Rent(5);
foreach (UnmanagedMemoryHandle b in buffers)
{

2
tests/ImageSharp.Tests/Processing/IntegralImageTests.cs

@ -76,7 +76,7 @@ public class IntegralImageTests : BaseImageOperationsExtensionTest
Buffer2D<ulong> integralBuffer,
Func<TPixel, ulong> getPixel)
where TPixel : unmanaged, IPixel<TPixel>
=> VerifySumValues(provider, integralBuffer, integralBuffer.Bounds(), getPixel);
=> VerifySumValues(provider, integralBuffer, integralBuffer.Bounds, getPixel);
private static void VerifySumValues<TPixel>(
TestImageProvider<TPixel> provider,

5
tests/ImageSharp.Tests/TestImages.cs

@ -1388,13 +1388,16 @@ public static class TestImages
public const string Benchmark = "Exr/Calliphora_benchmark.exr";
public const string Uncompressed = "Exr/Calliphora_uncompressed.exr";
public const string UncompressedRgba = "Exr/Calliphora_uncompressed_rgba.exr";
public const string UncompressedFloatRgb = "Exr/rgb_float32_uncompressed.exr";
public const string UncompressedFloatRgb = "Exr/Calliphora_float_uncompressed.exr";
public const string UncompressedUintRgb = "Exr/Calliphora_uint32_uncompressed.exr";
public const string UintRgba = "Exr/rgba_uint_uncompressed.exr";
public const string Zip = "Exr/Calliphora_zip.exr";
public const string Zips = "Exr/Calliphora_zips.exr";
public const string Rle = "Exr/Calliphora_rle.exr";
public const string B44 = "Exr/Calliphora_b44.exr";
public const string Pxr24Half = "Exr/Calliphora_half_pxr24.exr";
public const string Pxr24Float = "Exr/Calliphora_float_pxr24.exr";
public const string Pxr24Uint = "Exr/Calliphora_uint_pxr24.exr";
public const string Rgb = "Exr/Calliphora_rgb.exr";
public const string Gray = "Exr/Calliphora_gray.exr";
}

2
tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs

@ -505,7 +505,7 @@ public static class TestImageExtensions
public static void CompareBuffers<T>(Buffer2D<T> expected, Buffer2D<T> actual)
where T : struct, IEquatable<T>
{
Assert.True(expected.Size() == actual.Size(), "Buffer sizes are not equal!");
Assert.True(expected.Size == actual.Size, "Buffer sizes are not equal!");
for (int y = 0; y < expected.Height; y++)
{

3
tests/Images/External/ReferenceOutput/ExrDecoderTests/ExrDecoder_CanDecode_Pxr24Compressed_ExrPixelType_Uint_Rgba32_Calliphora_uint_pxr24.png

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:f66bf45b5d80e412314341567b8cf240d56b5872f01ce9f3b0d6034d85e8e942
size 163327

3
tests/Images/Input/Exr/Calliphora_float_pxr24.exr

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:ca6c29eb36395b923298d2bf4ba1f73fd7f081f7b1af2e72b94b92ad2acb638b
size 226997

3
tests/Images/Input/Exr/Calliphora_float_uncompressed.exr

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:926996c647c1c54921db0a623e58e5edeb92b447cb4eceb53b8c0e86fd24f11e
size 720281

3
tests/Images/Input/Exr/Calliphora_half_pxr24.exr

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:927778a73832b5ad68473e46df6be52076d98294271f2cd0ead66691db41b7bf
size 195063

3
tests/Images/Input/Exr/Calliphora_uint_pxr24.exr

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fc72685224ba818ac77d914f5e4c91162503f88f775e16b5c745baef6bc7b790
size 187914

3
tests/Images/Input/Exr/rgb_float32_uncompressed.exr

@ -1,3 +0,0 @@
version https://git-lfs.github.com/spec/v1
oid sha256:5ab8b71824ca384fc2cde1f74a6f34c38169fa328ccc67f17d08333e9de42f55
size 243558
Loading…
Cancel
Save