diff --git a/shared-infrastructure b/shared-infrastructure index a75469fdb..36b2d55f5 160000 --- a/shared-infrastructure +++ b/shared-infrastructure @@ -1 +1 @@ -Subproject commit a75469fdb93fb89b39a5b0b7c01cb7432ceef98f +Subproject commit 36b2d55f5bb0d91024955bd26ba220ee41cc96e5 diff --git a/src/ImageSharp/Advanced/AdvancedImageExtensions.cs b/src/ImageSharp/Advanced/AdvancedImageExtensions.cs index d810296d6..a988e22b2 100644 --- a/src/ImageSharp/Advanced/AdvancedImageExtensions.cs +++ b/src/ImageSharp/Advanced/AdvancedImageExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Linq; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Memory; @@ -40,7 +41,7 @@ namespace SixLabors.ImageSharp.Advanced => GetConfiguration((IConfigurationProvider)source); /// - /// Gets the configuration . + /// Gets the configuration. /// /// The source image /// Returns the bounds of the image @@ -48,15 +49,58 @@ namespace SixLabors.ImageSharp.Advanced => source?.Configuration ?? Configuration.Default; /// - /// Gets the representation of the pixels as a of contiguous memory in the source image's pixel format - /// stored in row major order. + /// Gets the representation of the pixels as a containing the backing pixel data of the image + /// stored in row major order, as a list of contiguous blocks in the source image's pixel format. /// + /// The source image. /// The type of the pixel. - /// The source. + /// The . + /// + /// Certain Image Processors may invalidate the returned and all it's buffers, + /// therefore it's not recommended to mutate the image while holding a reference to it's . + /// + public static IMemoryGroup GetPixelMemoryGroup(this ImageFrame source) + where TPixel : struct, IPixel + => source?.PixelBuffer.FastMemoryGroup.View ?? throw new ArgumentNullException(nameof(source)); + + /// + /// Gets the representation of the pixels as a containing the backing pixel data of the image + /// stored in row major order, as a list of contiguous blocks in the source image's pixel format. + /// + /// The source image. + /// The type of the pixel. + /// The . + /// + /// Certain Image Processors may invalidate the returned and all it's buffers, + /// therefore it's not recommended to mutate the image while holding a reference to it's . + /// + public static IMemoryGroup GetPixelMemoryGroup(this Image source) + where TPixel : struct, IPixel + => source?.Frames.RootFrame.GetPixelMemoryGroup() ?? throw new ArgumentNullException(nameof(source)); + + /// + /// Gets the representation of the pixels as a in the source image's pixel format + /// stored in row major order, if the backing buffer is contiguous. + /// + /// The type of the pixel. + /// The source image. /// The + /// Thrown when the backing buffer is discontiguous. + [Obsolete( + @"GetPixelSpan might fail, because the backing buffer could be discontiguous for large images. Use GetPixelMemoryGroup or GetPixelRowSpan instead!")] public static Span GetPixelSpan(this ImageFrame source) where TPixel : struct, IPixel - => source.GetPixelMemory().Span; + { + Guard.NotNull(source, nameof(source)); + + IMemoryGroup mg = source.GetPixelMemoryGroup(); + if (mg.Count > 1) + { + throw new InvalidOperationException($"GetPixelSpan is invalid, since the backing buffer of this {source.Width}x{source.Height} sized image is discontiguous!"); + } + + return mg.Single().Span; + } /// /// Gets the representation of the pixels as a of contiguous memory in the source image's pixel format @@ -65,9 +109,16 @@ namespace SixLabors.ImageSharp.Advanced /// The type of the pixel. /// The source. /// The + /// Thrown when the backing buffer is discontiguous. + [Obsolete( + @"GetPixelSpan might fail, because the backing buffer could be discontiguous for large images. Use GetPixelMemoryGroup or GetPixelRowSpan instead!")] public static Span GetPixelSpan(this Image source) where TPixel : struct, IPixel - => source.Frames.RootFrame.GetPixelSpan(); + { + Guard.NotNull(source, nameof(source)); + + return source.Frames.RootFrame.GetPixelSpan(); + } /// /// Gets the representation of the pixels as a of contiguous memory @@ -79,7 +130,13 @@ namespace SixLabors.ImageSharp.Advanced /// The public static Span GetPixelRowSpan(this ImageFrame source, int rowIndex) where TPixel : struct, IPixel - => source.PixelBuffer.GetRowSpan(rowIndex); + { + Guard.NotNull(source, nameof(source)); + Guard.MustBeGreaterThanOrEqualTo(rowIndex, 0, nameof(rowIndex)); + Guard.MustBeLessThan(rowIndex, source.Height, nameof(rowIndex)); + + return source.PixelBuffer.GetRowSpan(rowIndex); + } /// /// Gets the representation of the pixels as of of contiguous memory @@ -91,58 +148,12 @@ namespace SixLabors.ImageSharp.Advanced /// The public static Span GetPixelRowSpan(this Image source, int rowIndex) where TPixel : struct, IPixel - => source.Frames.RootFrame.GetPixelRowSpan(rowIndex); - - /// - /// Returns a reference to the 0th element of the Pixel buffer, - /// allowing direct manipulation of pixel data through unsafe operations. - /// The pixel buffer is a contiguous memory area containing Width*Height TPixel elements laid out in row-major order. - /// - /// The Pixel format. - /// The source image frame - /// A pinnable reference the first root of the pixel buffer. - [Obsolete("This method will be removed in our next release! Please use MemoryMarshal.GetReference(source.GetPixelSpan())!")] - public static ref TPixel DangerousGetPinnableReferenceToPixelBuffer(this ImageFrame source) - where TPixel : struct, IPixel - => ref DangerousGetPinnableReferenceToPixelBuffer((IPixelSource)source); - - /// - /// Returns a reference to the 0th element of the Pixel buffer, - /// allowing direct manipulation of pixel data through unsafe operations. - /// The pixel buffer is a contiguous memory area containing Width*Height TPixel elements laid out in row-major order. - /// - /// The Pixel format. - /// The source image - /// A pinnable reference the first root of the pixel buffer. - [Obsolete("This method will be removed in our next release! Please use MemoryMarshal.GetReference(source.GetPixelSpan())!")] - public static ref TPixel DangerousGetPinnableReferenceToPixelBuffer(this Image source) - where TPixel : struct, IPixel - => ref source.Frames.RootFrame.DangerousGetPinnableReferenceToPixelBuffer(); - - /// - /// Gets the representation of the pixels as a of contiguous memory in the source image's pixel format - /// stored in row major order. - /// - /// The Pixel format. - /// The source - /// The - internal static Memory GetPixelMemory(this ImageFrame source) - where TPixel : struct, IPixel { - return source.PixelBuffer.MemorySource.Memory; - } + Guard.NotNull(source, nameof(source)); + Guard.MustBeGreaterThanOrEqualTo(rowIndex, 0, nameof(rowIndex)); + Guard.MustBeLessThan(rowIndex, source.Height, nameof(rowIndex)); - /// - /// Gets the representation of the pixels as a of contiguous memory in the source image's pixel format - /// stored in row major order. - /// - /// The Pixel format. - /// The source - /// The - internal static Memory GetPixelMemory(this Image source) - where TPixel : struct, IPixel - { - return source.Frames.RootFrame.GetPixelMemory(); + return source.Frames.RootFrame.PixelBuffer.GetRowSpan(rowIndex); } /// @@ -153,9 +164,15 @@ namespace SixLabors.ImageSharp.Advanced /// The source. /// The row. /// The - internal static Memory GetPixelRowMemory(this ImageFrame source, int rowIndex) + public static Memory GetPixelRowMemory(this ImageFrame source, int rowIndex) where TPixel : struct, IPixel - => source.PixelBuffer.GetRowMemory(rowIndex); + { + Guard.NotNull(source, nameof(source)); + Guard.MustBeGreaterThanOrEqualTo(rowIndex, 0, nameof(rowIndex)); + Guard.MustBeLessThan(rowIndex, source.Height, nameof(rowIndex)); + + return source.PixelBuffer.GetSafeRowMemory(rowIndex); + } /// /// Gets the representation of the pixels as of of contiguous memory @@ -165,9 +182,15 @@ namespace SixLabors.ImageSharp.Advanced /// The source. /// The row. /// The - internal static Memory GetPixelRowMemory(this Image source, int rowIndex) + public static Memory GetPixelRowMemory(this Image source, int rowIndex) where TPixel : struct, IPixel - => source.Frames.RootFrame.GetPixelRowMemory(rowIndex); + { + Guard.NotNull(source, nameof(source)); + Guard.MustBeGreaterThanOrEqualTo(rowIndex, 0, nameof(rowIndex)); + Guard.MustBeLessThan(rowIndex, source.Height, nameof(rowIndex)); + + return source.Frames.RootFrame.PixelBuffer.GetSafeRowMemory(rowIndex); + } /// /// Gets the assigned to 'source'. @@ -176,15 +199,5 @@ namespace SixLabors.ImageSharp.Advanced /// Returns the configuration. internal static MemoryAllocator GetMemoryAllocator(this IConfigurationProvider source) => GetConfiguration(source).MemoryAllocator; - - /// - /// Returns a reference to the 0th element of the Pixel buffer. - /// Such a reference can be used for pinning but must never be dereferenced. - /// - /// The source image frame - /// A reference to the element. - private static ref TPixel DangerousGetPinnableReferenceToPixelBuffer(IPixelSource source) - where TPixel : struct, IPixel - => ref MemoryMarshal.GetReference(source.PixelBuffer.GetSpan()); } } diff --git a/src/ImageSharp/Common/Exceptions/ImageFormatException.cs b/src/ImageSharp/Common/Exceptions/ImageFormatException.cs index 8b9dbe1b8..4028b70b0 100644 --- a/src/ImageSharp/Common/Exceptions/ImageFormatException.cs +++ b/src/ImageSharp/Common/Exceptions/ImageFormatException.cs @@ -7,7 +7,7 @@ namespace SixLabors.ImageSharp { /// /// The exception that is thrown when the library tries to load - /// an image, which has an invalid format. + /// an image, which has format or content that is invalid or unsupported by ImageSharp. /// public class ImageFormatException : Exception { diff --git a/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs index 619be880a..47c7c54ea 100644 --- a/src/ImageSharp/Configuration.cs +++ b/src/ImageSharp/Configuration.cs @@ -108,7 +108,8 @@ namespace SixLabors.ImageSharp /// The default value is 1MB. /// /// - /// Currently only used by Resize. + /// Currently only used by Resize. If the working buffer is expected to be discontiguous, + /// min(WorkingBufferSizeHintInBytes, BufferCapacityInBytes) should be used. /// internal int WorkingBufferSizeHintInBytes { get; set; } = 1 * 1024 * 1024; diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoder.cs b/src/ImageSharp/Formats/Bmp/BmpDecoder.cs index a404ab418..e5546b361 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoder.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoder.cs @@ -1,7 +1,8 @@ -// Copyright (c) Six Labors and contributors. +// Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. using System.IO; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Bmp @@ -32,7 +33,20 @@ namespace SixLabors.ImageSharp.Formats.Bmp { Guard.NotNull(stream, nameof(stream)); - return new BmpDecoderCore(configuration, this).Decode(stream); + var decoder = new BmpDecoderCore(configuration, this); + + try + { + return decoder.Decode(stream); + } + catch (InvalidMemoryOperationException ex) + { + Size dims = decoder.Dimensions; + + // TODO: use InvalidImageContentException here, if we decide to define it + // https://github.com/SixLabors/ImageSharp/issues/1110 + throw new ImageFormatException($"Can not decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}. This error can happen for very large RLE bitmaps, which are not supported.", ex); + } } /// diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index 8d82d28fb..80b20c025 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -114,6 +114,11 @@ namespace SixLabors.ImageSharp.Formats.Bmp this.options = options; } + /// + /// Gets the dimensions of the image. + /// + public Size Dimensions => new Size(this.infoHeader.Width, this.infoHeader.Height); + /// /// Decodes the image from the specified this._stream and sets /// the data to image. @@ -294,24 +299,27 @@ namespace SixLabors.ImageSharp.Formats.Bmp where TPixel : struct, IPixel { TPixel color = default; - using (Buffer2D buffer = this.memoryAllocator.Allocate2D(width, height, AllocationOptions.Clean)) - using (Buffer2D undefinedPixels = this.memoryAllocator.Allocate2D(width, height, AllocationOptions.Clean)) + using (IMemoryOwner buffer = this.memoryAllocator.Allocate(width * height, AllocationOptions.Clean)) + using (IMemoryOwner undefinedPixels = this.memoryAllocator.Allocate(width * height, AllocationOptions.Clean)) using (IMemoryOwner rowsWithUndefinedPixels = this.memoryAllocator.Allocate(height, AllocationOptions.Clean)) { Span rowsWithUndefinedPixelsSpan = rowsWithUndefinedPixels.Memory.Span; - if (compression == BmpCompression.RLE8) + Span undefinedPixelsSpan = undefinedPixels.Memory.Span; + Span bufferSpan = buffer.Memory.Span; + if (compression is BmpCompression.RLE8) { - this.UncompressRle8(width, buffer.GetSpan(), undefinedPixels.GetSpan(), rowsWithUndefinedPixelsSpan); + this.UncompressRle8(width, bufferSpan, undefinedPixelsSpan, rowsWithUndefinedPixelsSpan); } else { - this.UncompressRle4(width, buffer.GetSpan(), undefinedPixels.GetSpan(), rowsWithUndefinedPixelsSpan); + this.UncompressRle4(width, bufferSpan, undefinedPixelsSpan, rowsWithUndefinedPixelsSpan); } for (int y = 0; y < height; y++) { int newY = Invert(y, height, inverted); - Span bufferRow = buffer.GetRowSpan(y); + int rowStartIdx = y * width; + Span bufferRow = bufferSpan.Slice(rowStartIdx, width); Span pixelRow = pixels.GetRowSpan(newY); bool rowHasUndefinedPixels = rowsWithUndefinedPixelsSpan[y]; @@ -321,7 +329,7 @@ namespace SixLabors.ImageSharp.Formats.Bmp for (int x = 0; x < width; x++) { byte colorIdx = bufferRow[x]; - if (undefinedPixels[x, y]) + if (undefinedPixelsSpan[rowStartIdx + x]) { switch (this.options.RleSkippedPixelHandling) { @@ -372,12 +380,14 @@ namespace SixLabors.ImageSharp.Formats.Bmp { TPixel color = default; using (IMemoryOwner buffer = this.memoryAllocator.Allocate(width * height * 3, AllocationOptions.Clean)) - using (Buffer2D undefinedPixels = this.memoryAllocator.Allocate2D(width, height, AllocationOptions.Clean)) + using (IMemoryOwner undefinedPixels = this.memoryAllocator.Allocate(width * height, AllocationOptions.Clean)) using (IMemoryOwner rowsWithUndefinedPixels = this.memoryAllocator.Allocate(height, AllocationOptions.Clean)) { Span rowsWithUndefinedPixelsSpan = rowsWithUndefinedPixels.Memory.Span; + Span undefinedPixelsSpan = undefinedPixels.Memory.Span; Span bufferSpan = buffer.GetSpan(); - this.UncompressRle24(width, bufferSpan, undefinedPixels.GetSpan(), rowsWithUndefinedPixelsSpan); + + this.UncompressRle24(width, bufferSpan, undefinedPixelsSpan, rowsWithUndefinedPixelsSpan); for (int y = 0; y < height; y++) { int newY = Invert(y, height, inverted); @@ -386,11 +396,12 @@ namespace SixLabors.ImageSharp.Formats.Bmp if (rowHasUndefinedPixels) { // Slow path with undefined pixels. - int rowStartIdx = y * width * 3; + var yMulWidth = y * width; + int rowStartIdx = yMulWidth * 3; for (int x = 0; x < width; x++) { int idx = rowStartIdx + (x * 3); - if (undefinedPixels[x, y]) + if (undefinedPixelsSpan[yMulWidth + x]) { switch (this.options.RleSkippedPixelHandling) { diff --git a/src/ImageSharp/Formats/Gif/GifDecoder.cs b/src/ImageSharp/Formats/Gif/GifDecoder.cs index 7691ec1aa..24e3d8826 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoder.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoder.cs @@ -1,7 +1,9 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System; using System.IO; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -27,7 +29,19 @@ namespace SixLabors.ImageSharp.Formats.Gif where TPixel : struct, IPixel { var decoder = new GifDecoderCore(configuration, this); - return decoder.Decode(stream); + + try + { + return decoder.Decode(stream); + } + catch (InvalidMemoryOperationException ex) + { + Size dims = decoder.Dimensions; + + // TODO: use InvalidImageContentException here, if we decide to define it + // https://github.com/SixLabors/ImageSharp/issues/1110 + throw new ImageFormatException($"Can not decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex); + } } /// diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index 98dbddb48..bc508cba7 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -86,10 +86,15 @@ namespace SixLabors.ImageSharp.Formats.Gif public bool IgnoreMetadata { get; internal set; } /// - /// Gets the decoding mode for multi-frame images + /// Gets the decoding mode for multi-frame images. /// public FrameDecodingMode DecodingMode { get; } + /// + /// Gets the dimensions of the image. + /// + public Size Dimensions => new Size(this.imageDescriptor.Width, this.imageDescriptor.Height); + private MemoryAllocator MemoryAllocator => this.configuration.MemoryAllocator; /// diff --git a/src/ImageSharp/Formats/IImageDecoder.cs b/src/ImageSharp/Formats/IImageDecoder.cs index e8e84de7d..7188b57a6 100644 --- a/src/ImageSharp/Formats/IImageDecoder.cs +++ b/src/ImageSharp/Formats/IImageDecoder.cs @@ -18,6 +18,7 @@ namespace SixLabors.ImageSharp.Formats /// The configuration for the image. /// The containing image data. /// The . + // TODO: Document ImageFormatExceptions (https://github.com/SixLabors/ImageSharp/issues/1110) Image Decode(Configuration configuration, Stream stream) where TPixel : struct, IPixel; @@ -27,6 +28,7 @@ namespace SixLabors.ImageSharp.Formats /// The configuration for the image. /// The containing image data. /// The . + // TODO: Document ImageFormatExceptions (https://github.com/SixLabors/ImageSharp/issues/1110) Image Decode(Configuration configuration, Stream stream); } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.CopyTo.cs b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.CopyTo.cs index 6bf9c8483..64d1d68b7 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.CopyTo.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Block8x8F.CopyTo.cs @@ -139,4 +139,4 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components } } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs index 39c8be312..22bc8ccaa 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs @@ -31,12 +31,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder { this.Component = component; this.ImagePostProcessor = imagePostProcessor; - this.ColorBuffer = memoryAllocator.Allocate2D( + this.blockAreaSize = this.Component.SubSamplingDivisors * 8; + this.ColorBuffer = memoryAllocator.Allocate2DOveraligned( imagePostProcessor.PostProcessorBufferSize.Width, - imagePostProcessor.PostProcessorBufferSize.Height); + imagePostProcessor.PostProcessorBufferSize.Height, + this.blockAreaSize.Height); this.BlockRowsPerStep = JpegImagePostProcessor.BlockRowsPerStep / this.Component.SubSamplingDivisors.Height; - this.blockAreaSize = this.Component.SubSamplingDivisors * 8; } /// @@ -111,4 +112,4 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder this.currentComponentRowInBlocks += this.BlockRowsPerStep; } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter{TPixel}.cs index 92482de2a..9619a78fc 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter{TPixel}.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Encoder/YCbCrForwardConverter{TPixel}.cs @@ -55,9 +55,9 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder /// /// Converts a 8x8 image area inside 'pixels' at position (x,y) placing the result members of the structure (, , ) /// - public void Convert(ImageFrame frame, int x, int y) + public void Convert(ImageFrame frame, int x, int y, in RowOctet currentRows) { - this.pixelBlock.LoadAndStretchEdges(frame, x, y); + this.pixelBlock.LoadAndStretchEdges(frame.PixelBuffer, x, y, currentRows); Span rgbSpan = this.rgbBlock.AsSpanUnsafe(); PixelOperations.Instance.ToRgb24(frame.GetConfiguration(), this.pixelBlock.AsSpanUnsafe(), rgbSpan); diff --git a/src/ImageSharp/Formats/Jpeg/Components/GenericBlock8x8.cs b/src/ImageSharp/Formats/Jpeg/Components/GenericBlock8x8.cs index 3d1e22a99..534c66b99 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/GenericBlock8x8.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/GenericBlock8x8.cs @@ -54,24 +54,11 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components set => this[(y * 8) + x] = value; } - public void LoadAndStretchEdges(IPixelSource source, int sourceX, int sourceY) - where TPixel : struct, IPixel - { - if (source.PixelBuffer is Buffer2D buffer) - { - this.LoadAndStretchEdges(buffer, sourceX, sourceY); - } - else - { - throw new InvalidOperationException("LoadAndStretchEdges() is only valid for TPixel == T !"); - } - } - /// /// Load a 8x8 region of an image into the block. /// The "outlying" area of the block will be stretched out with pixels on the right and bottom edge of the image. /// - public void LoadAndStretchEdges(Buffer2D source, int sourceX, int sourceY) + public void LoadAndStretchEdges(Buffer2D source, int sourceX, int sourceY, in RowOctet currentRows) { int width = Math.Min(8, source.Width - sourceX); int height = Math.Min(8, source.Height - sourceY); @@ -85,15 +72,13 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components int remainderXCount = 8 - width; ref byte blockStart = ref Unsafe.As, byte>(ref this); - ref byte imageStart = ref Unsafe.As( - ref Unsafe.Add(ref MemoryMarshal.GetReference(source.GetRowSpan(sourceY)), sourceX)); - int blockRowSizeInBytes = 8 * Unsafe.SizeOf(); - int imageRowSizeInBytes = source.Width * Unsafe.SizeOf(); for (int y = 0; y < height; y++) { - ref byte s = ref Unsafe.Add(ref imageStart, y * imageRowSizeInBytes); + Span row = currentRows[y]; + + ref byte s = ref Unsafe.As(ref row[sourceX]); ref byte d = ref Unsafe.Add(ref blockStart, y * blockRowSizeInBytes); Unsafe.CopyBlock(ref d, ref s, byteWidth); @@ -127,4 +112,4 @@ namespace SixLabors.ImageSharp.Formats.Jpeg.Components /// public Span AsSpanUnsafe() => new Span(Unsafe.AsPointer(ref this), Size); } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Formats/Jpeg/Components/RowOctet.cs b/src/ImageSharp/Formats/Jpeg/Components/RowOctet.cs new file mode 100644 index 000000000..8c3daa4d5 --- /dev/null +++ b/src/ImageSharp/Formats/Jpeg/Components/RowOctet.cs @@ -0,0 +1,68 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Formats.Jpeg.Components +{ + /// + /// Cache 8 pixel rows on the stack, which may originate from different buffers of a . + /// + [StructLayout(LayoutKind.Sequential)] + internal readonly ref struct RowOctet + where T : struct + { + private readonly Span row0; + private readonly Span row1; + private readonly Span row2; + private readonly Span row3; + private readonly Span row4; + private readonly Span row5; + private readonly Span row6; + private readonly Span row7; + + public RowOctet(Buffer2D buffer, int startY) + { + int y = startY; + int height = buffer.Height; + this.row0 = y < height ? buffer.GetRowSpan(y++) : default; + this.row1 = y < height ? buffer.GetRowSpan(y++) : default; + this.row2 = y < height ? buffer.GetRowSpan(y++) : default; + this.row3 = y < height ? buffer.GetRowSpan(y++) : default; + this.row4 = y < height ? buffer.GetRowSpan(y++) : default; + this.row5 = y < height ? buffer.GetRowSpan(y++) : default; + this.row6 = y < height ? buffer.GetRowSpan(y++) : default; + this.row7 = y < height ? buffer.GetRowSpan(y) : default; + } + + public Span this[int y] + { + [MethodImpl(InliningOptions.ShortMethod)] + get + { + // No unsafe tricks, since Span can't be used as a generic argument + return y switch + { + 0 => this.row0, + 1 => this.row1, + 2 => this.row2, + 3 => this.row3, + 4 => this.row4, + 5 => this.row5, + 6 => this.row6, + 7 => this.row7, + _ => ThrowIndexOutOfRangeException() + }; + } + } + + [MethodImpl(InliningOptions.ColdPath)] + private static Span ThrowIndexOutOfRangeException() + { + throw new IndexOutOfRangeException(); + } + } +} diff --git a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs index 4e1c0c1be..31085dbaa 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegDecoder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Jpeg @@ -22,10 +23,19 @@ namespace SixLabors.ImageSharp.Formats.Jpeg { Guard.NotNull(stream, nameof(stream)); - using (var decoder = new JpegDecoderCore(configuration, this)) + using var decoder = new JpegDecoderCore(configuration, this); + try { return decoder.Decode(stream); } + catch (InvalidMemoryOperationException ex) + { + (int w, int h) = (decoder.ImageWidth, decoder.ImageHeight); + + // TODO: use InvalidImageContentException here, if we decide to define it + // https://github.com/SixLabors/ImageSharp/issues/1110 + throw new ImageFormatException($"Can not decode image. Failed to allocate buffers for possibly degenerate dimensions: {w}x{h}.", ex); + } } /// diff --git a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs index cd3c19aa3..dcf2d72a5 100644 --- a/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs +++ b/src/ImageSharp/Formats/Jpeg/JpegEncoderCore.cs @@ -9,6 +9,7 @@ using SixLabors.ImageSharp.Common.Helpers; using SixLabors.ImageSharp.Formats.Jpeg.Components; using SixLabors.ImageSharp.Formats.Jpeg.Components.Decoder; using SixLabors.ImageSharp.Formats.Jpeg.Components.Encoder; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.Metadata.Profiles.Exif; using SixLabors.ImageSharp.Metadata.Profiles.Icc; @@ -409,12 +410,16 @@ namespace SixLabors.ImageSharp.Formats.Jpeg int prevDCY = 0, prevDCCb = 0, prevDCCr = 0; var pixelConverter = YCbCrForwardConverter.Create(); + ImageFrame frame = pixels.Frames.RootFrame; + Buffer2D pixelBuffer = frame.PixelBuffer; for (int y = 0; y < pixels.Height; y += 8) { + var currentRows = new RowOctet(pixelBuffer, y); + for (int x = 0; x < pixels.Width; x += 8) { - pixelConverter.Convert(pixels.Frames.RootFrame, x, y); + pixelConverter.Convert(frame, x, y, currentRows); prevDCY = this.WriteBlock( QuantIndex.Luminance, @@ -935,6 +940,8 @@ namespace SixLabors.ImageSharp.Formats.Jpeg // ReSharper disable once InconsistentNaming int prevDCY = 0, prevDCCb = 0, prevDCCr = 0; + ImageFrame frame = pixels.Frames.RootFrame; + Buffer2D pixelBuffer = frame.PixelBuffer; for (int y = 0; y < pixels.Height; y += 16) { @@ -945,7 +952,10 @@ namespace SixLabors.ImageSharp.Formats.Jpeg int xOff = (i & 1) * 8; int yOff = (i & 2) * 4; - pixelConverter.Convert(pixels.Frames.RootFrame, x + xOff, y + yOff); + // TODO: Try pushing this to the outer loop! + var currentRows = new RowOctet(pixelBuffer, y + yOff); + + pixelConverter.Convert(frame, x + xOff, y + yOff, currentRows); cbPtr[i] = pixelConverter.Cb; crPtr[i] = pixelConverter.Cr; diff --git a/src/ImageSharp/Formats/Png/PngDecoder.cs b/src/ImageSharp/Formats/Png/PngDecoder.cs index eea9e54c0..3b41cfc6e 100644 --- a/src/ImageSharp/Formats/Png/PngDecoder.cs +++ b/src/ImageSharp/Formats/Png/PngDecoder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Png @@ -44,7 +45,19 @@ namespace SixLabors.ImageSharp.Formats.Png where TPixel : struct, IPixel { var decoder = new PngDecoderCore(configuration, this); - return decoder.Decode(stream); + + try + { + return decoder.Decode(stream); + } + catch (InvalidMemoryOperationException ex) + { + Size dims = decoder.Dimensions; + + // TODO: use InvalidImageContentException here, if we decide to define it + // https://github.com/SixLabors/ImageSharp/issues/1110 + throw new ImageFormatException($"Can not decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex); + } } /// diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index 69b341c8d..2701bd2a7 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -106,7 +106,7 @@ namespace SixLabors.ImageSharp.Formats.Png private int currentRow = Adam7.FirstRow[0]; /// - /// The current number of bytes read in the current scanline + /// The current number of bytes read in the current scanline. /// private int currentRowBytesRead; @@ -132,18 +132,23 @@ namespace SixLabors.ImageSharp.Formats.Png this.ignoreMetadata = options.IgnoreMetadata; } + /// + /// Gets the dimensions of the image. + /// + public Size Dimensions => new Size(this.header.Width, this.header.Height); + /// /// Decodes the stream to the image. /// /// The pixel format. - /// The stream containing image data. + /// The stream containing image data. /// /// Thrown if the stream does not contain and end chunk. /// /// /// Thrown if the image is larger than the maximum allowable size. /// - /// The decoded image + /// The decoded image. public Image Decode(Stream stream) where TPixel : struct, IPixel { diff --git a/src/ImageSharp/Formats/Tga/TgaDecoder.cs b/src/ImageSharp/Formats/Tga/TgaDecoder.cs index b97388773..a6de902b8 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoder.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoder.cs @@ -1,7 +1,9 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System; using System.IO; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Formats.Tga @@ -17,7 +19,20 @@ namespace SixLabors.ImageSharp.Formats.Tga { Guard.NotNull(stream, nameof(stream)); - return new TgaDecoderCore(configuration, this).Decode(stream); + var decoder = new TgaDecoderCore(configuration, this); + + try + { + return decoder.Decode(stream); + } + catch (InvalidMemoryOperationException ex) + { + Size dims = decoder.Dimensions; + + // TODO: use InvalidImageContentException here, if we decide to define it + // https://github.com/SixLabors/ImageSharp/issues/1110 + throw new ImageFormatException($"Can not decode image. Failed to allocate buffers for possibly degenerate dimensions: {dims.Width}x{dims.Height}.", ex); + } } /// diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index 91cc93e19..a86fd3bce 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -61,6 +61,11 @@ namespace SixLabors.ImageSharp.Formats.Tga this.options = options; } + /// + /// Gets the dimensions of the image. + /// + public Size Dimensions => new Size(this.fileHeader.Width, this.fileHeader.Height); + /// /// Decodes the image from the specified stream. /// diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index a4b141f38..d1ec2ed4c 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -102,7 +102,7 @@ namespace SixLabors.ImageSharp.Formats.Tga if (this.compression is TgaCompression.RunLength) { - this.WriteRunLengthEndcodedImage(stream, image.Frames.RootFrame); + this.WriteRunLengthEncodedImage(stream, image.Frames.RootFrame); } else { @@ -150,19 +150,20 @@ namespace SixLabors.ImageSharp.Formats.Tga /// The pixel type. /// The stream to write the image to. /// The image to encode. - private void WriteRunLengthEndcodedImage(Stream stream, ImageFrame image) + private void WriteRunLengthEncodedImage(Stream stream, ImageFrame image) where TPixel : struct, IPixel { Rgba32 color = default; Buffer2D pixels = image.PixelBuffer; - Span pixelSpan = pixels.GetSpan(); int totalPixels = image.Width * image.Height; int encodedPixels = 0; while (encodedPixels < totalPixels) { - TPixel currentPixel = pixelSpan[encodedPixels]; + int x = encodedPixels % pixels.Width; + int y = encodedPixels / pixels.Width; + TPixel currentPixel = pixels[x, y]; currentPixel.ToRgba32(ref color); - byte equalPixelCount = this.FindEqualPixels(pixelSpan.Slice(encodedPixels)); + byte equalPixelCount = this.FindEqualPixels(pixels, x, y); // Write the number of equal pixels, with the high bit set, indicating ist a compressed pixel run. stream.WriteByte((byte)(equalPixelCount | 128)); @@ -200,30 +201,40 @@ namespace SixLabors.ImageSharp.Formats.Tga } /// - /// Finds consecutive pixels, which have the same value starting from the pixel span offset 0. + /// Finds consecutive pixels which have the same value. /// /// The pixel type. - /// The pixel span to search in. + /// The pixels of the image. + /// X coordinate to start searching for the same pixels. + /// Y coordinate to start searching for the same pixels. /// The number of equal pixels. - private byte FindEqualPixels(Span pixelSpan) + private byte FindEqualPixels(Buffer2D pixels, int xStart, int yStart) where TPixel : struct, IPixel { - int idx = 0; byte equalPixelCount = 0; - while (equalPixelCount < 127 && idx < pixelSpan.Length - 1) + bool firstRow = true; + TPixel startPixel = pixels[xStart, yStart]; + for (int y = yStart; y < pixels.Height; y++) { - TPixel currentPixel = pixelSpan[idx]; - TPixel nextPixel = pixelSpan[idx + 1]; - if (currentPixel.Equals(nextPixel)) + for (int x = firstRow ? xStart + 1 : 0; x < pixels.Width; x++) { - equalPixelCount++; - } - else - { - return equalPixelCount; + TPixel nextPixel = pixels[x, y]; + if (startPixel.Equals(nextPixel)) + { + equalPixelCount++; + } + else + { + return equalPixelCount; + } + + if (equalPixelCount >= 127) + { + return equalPixelCount; + } } - idx++; + firstRow = false; } return equalPixelCount; diff --git a/src/ImageSharp/Image.Decode.cs b/src/ImageSharp/Image.Decode.cs index e1376b4a2..5c19a4239 100644 --- a/src/ImageSharp/Image.Decode.cs +++ b/src/ImageSharp/Image.Decode.cs @@ -36,7 +36,7 @@ namespace SixLabors.ImageSharp { Buffer2D uninitializedMemoryBuffer = configuration.MemoryAllocator.Allocate2D(width, height); - return new Image(configuration, uninitializedMemoryBuffer.MemorySource, width, height, metadata); + return new Image(configuration, uninitializedMemoryBuffer.FastMemoryGroup, width, height, metadata); } /// diff --git a/src/ImageSharp/Image.LoadPixelData.cs b/src/ImageSharp/Image.LoadPixelData.cs index eb7ce261f..c655a87d0 100644 --- a/src/ImageSharp/Image.LoadPixelData.cs +++ b/src/ImageSharp/Image.LoadPixelData.cs @@ -4,6 +4,7 @@ using System; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp @@ -118,10 +119,10 @@ namespace SixLabors.ImageSharp Guard.MustBeGreaterThanOrEqualTo(data.Length, count, nameof(data)); var image = new Image(config, width, height); - - data.Slice(0, count).CopyTo(image.Frames.RootFrame.GetPixelSpan()); + data = data.Slice(0, count); + data.CopyTo(image.Frames.RootFrame.PixelBuffer.FastMemoryGroup); return image; } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Image.WrapMemory.cs b/src/ImageSharp/Image.WrapMemory.cs index 095991b07..9bb40a78b 100644 --- a/src/ImageSharp/Image.WrapMemory.cs +++ b/src/ImageSharp/Image.WrapMemory.cs @@ -34,7 +34,7 @@ namespace SixLabors.ImageSharp ImageMetadata metadata) where TPixel : struct, IPixel { - var memorySource = new MemorySource(pixelMemory); + var memorySource = MemoryGroup.Wrap(pixelMemory); return new Image(config, memorySource, width, height, metadata); } @@ -99,7 +99,7 @@ namespace SixLabors.ImageSharp ImageMetadata metadata) where TPixel : struct, IPixel { - var memorySource = new MemorySource(pixelMemoryOwner, false); + var memorySource = MemoryGroup.Wrap(pixelMemoryOwner); return new Image(config, memorySource, width, height, metadata); } @@ -147,4 +147,4 @@ namespace SixLabors.ImageSharp return WrapMemory(Configuration.Default, pixelMemoryOwner, width, height); } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/ImageFrame.LoadPixelData.cs b/src/ImageSharp/ImageFrame.LoadPixelData.cs index 9e90aeaf5..837305d62 100644 --- a/src/ImageSharp/ImageFrame.LoadPixelData.cs +++ b/src/ImageSharp/ImageFrame.LoadPixelData.cs @@ -4,6 +4,7 @@ using System; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp @@ -43,7 +44,8 @@ namespace SixLabors.ImageSharp var image = new ImageFrame(configuration, width, height); - data.Slice(0, count).CopyTo(image.GetPixelSpan()); + data = data.Slice(0, count); + data.CopyTo(image.PixelBuffer.FastMemoryGroup); return image; } diff --git a/src/ImageSharp/ImageFrame.cs b/src/ImageSharp/ImageFrame.cs index 235840e77..cbd526662 100644 --- a/src/ImageSharp/ImageFrame.cs +++ b/src/ImageSharp/ImageFrame.cs @@ -3,6 +3,7 @@ using System; using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -78,7 +79,7 @@ namespace SixLabors.ImageSharp /// Whether to dispose of managed and unmanaged objects. protected abstract void Dispose(bool disposing); - internal abstract void CopyPixelsTo(Span destination) + internal abstract void CopyPixelsTo(MemoryGroup destination) where TDestinationPixel : struct, IPixel; /// diff --git a/src/ImageSharp/ImageFrameCollection{TPixel}.cs b/src/ImageSharp/ImageFrameCollection{TPixel}.cs index 722a4ddea..b7f1d1bb6 100644 --- a/src/ImageSharp/ImageFrameCollection{TPixel}.cs +++ b/src/ImageSharp/ImageFrameCollection{TPixel}.cs @@ -30,7 +30,7 @@ namespace SixLabors.ImageSharp this.frames.Add(new ImageFrame(parent.GetConfiguration(), width, height, backgroundColor)); } - internal ImageFrameCollection(Image parent, int width, int height, MemorySource memorySource) + internal ImageFrameCollection(Image parent, int width, int height, MemoryGroup memorySource) { this.parent = parent ?? throw new ArgumentNullException(nameof(parent)); @@ -351,7 +351,7 @@ namespace SixLabors.ImageSharp this.parent.GetConfiguration(), source.Size(), source.Metadata.DeepClone()); - source.CopyPixelsTo(result.PixelBuffer.GetSpan()); + source.CopyPixelsTo(result.PixelBuffer.FastMemoryGroup); return result; } } diff --git a/src/ImageSharp/ImageFrame{TPixel}.cs b/src/ImageSharp/ImageFrame{TPixel}.cs index a2de8d671..85488c12d 100644 --- a/src/ImageSharp/ImageFrame{TPixel}.cs +++ b/src/ImageSharp/ImageFrame{TPixel}.cs @@ -97,7 +97,7 @@ namespace SixLabors.ImageSharp /// The width of the image in pixels. /// The height of the image in pixels. /// The memory source. - internal ImageFrame(Configuration configuration, int width, int height, MemorySource memorySource) + internal ImageFrame(Configuration configuration, int width, int height, MemoryGroup memorySource) : this(configuration, width, height, memorySource, new ImageFrameMetadata()) { } @@ -110,7 +110,7 @@ namespace SixLabors.ImageSharp /// The height of the image in pixels. /// The memory source. /// The metadata. - internal ImageFrame(Configuration configuration, int width, int height, MemorySource memorySource, ImageFrameMetadata metadata) + internal ImageFrame(Configuration configuration, int width, int height, MemoryGroup memorySource, ImageFrameMetadata metadata) : base(configuration, width, height, metadata) { Guard.MustBeGreaterThan(width, 0, nameof(width)); @@ -131,7 +131,7 @@ namespace SixLabors.ImageSharp Guard.NotNull(source, nameof(source)); this.PixelBuffer = this.GetConfiguration().MemoryAllocator.Allocate2D(source.PixelBuffer.Width, source.PixelBuffer.Height); - source.PixelBuffer.GetSpan().CopyTo(this.PixelBuffer.GetSpan()); + source.PixelBuffer.FastMemoryGroup.CopyTo(this.PixelBuffer.FastMemoryGroup); } /// @@ -148,13 +148,22 @@ namespace SixLabors.ImageSharp /// The x-coordinate of the pixel. Must be greater than or equal to zero and less than the width of the image. /// The y-coordinate of the pixel. Must be greater than or equal to zero and less than the height of the image. /// The at the specified position. + /// Thrown when the provided (x,y) coordinates are outside the image boundary. public TPixel this[int x, int y] { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => this.PixelBuffer[x, y]; + [MethodImpl(InliningOptions.ShortMethod)] + get + { + this.VerifyCoords(x, y); + return this.PixelBuffer.GetElementUnsafe(x, y); + } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - set => this.PixelBuffer[x, y] = value; + [MethodImpl(InliningOptions.ShortMethod)] + set + { + this.VerifyCoords(x, y); + this.PixelBuffer.GetElementUnsafe(x, y) = value; + } } /// @@ -177,7 +186,7 @@ namespace SixLabors.ImageSharp throw new ArgumentException("ImageFrame.CopyTo(): target must be of the same size!", nameof(target)); } - this.GetPixelSpan().CopyTo(target.GetSpan()); + this.PixelBuffer.FastMemoryGroup.CopyTo(target.FastMemoryGroup); } /// @@ -209,15 +218,22 @@ namespace SixLabors.ImageSharp this.isDisposed = true; } - internal override void CopyPixelsTo(Span destination) + internal override void CopyPixelsTo(MemoryGroup destination) { if (typeof(TPixel) == typeof(TDestinationPixel)) { - Span dest1 = MemoryMarshal.Cast(destination); - this.PixelBuffer.GetSpan().CopyTo(dest1); + this.PixelBuffer.FastMemoryGroup.TransformTo(destination, (s, d) => + { + Span d1 = MemoryMarshal.Cast(d); + s.CopyTo(d1); + }); + return; } - PixelOperations.Instance.To(this.GetConfiguration(), this.PixelBuffer.GetSpan(), destination); + this.PixelBuffer.FastMemoryGroup.TransformTo(destination, (s, d) => + { + PixelOperations.Instance.To(this.GetConfiguration(), s, d); + }); } /// @@ -275,18 +291,38 @@ namespace SixLabors.ImageSharp /// The value to initialize the bitmap with. internal void Clear(TPixel value) { - Span span = this.GetPixelSpan(); + MemoryGroup group = this.PixelBuffer.FastMemoryGroup; if (value.Equals(default)) { - span.Clear(); + group.Clear(); } else { - span.Fill(value); + group.Fill(value); + } + } + + [MethodImpl(InliningOptions.ShortMethod)] + private void VerifyCoords(int x, int y) + { + if (x < 0 || x >= this.Width) + { + ThrowArgumentOutOfRangeException(nameof(x)); + } + + if (y < 0 || y >= this.Height) + { + ThrowArgumentOutOfRangeException(nameof(y)); } } + [MethodImpl(InliningOptions.ColdPath)] + private static void ThrowArgumentOutOfRangeException(string paramName) + { + throw new ArgumentOutOfRangeException(paramName); + } + /// /// A implementing the clone logic for . /// diff --git a/src/ImageSharp/ImageSharp.csproj.DotSettings b/src/ImageSharp/ImageSharp.csproj.DotSettings index 018ca75cd..6896e069c 100644 --- a/src/ImageSharp/ImageSharp.csproj.DotSettings +++ b/src/ImageSharp/ImageSharp.csproj.DotSettings @@ -2,6 +2,8 @@ True True True + True + True True True True diff --git a/src/ImageSharp/Image{TPixel}.cs b/src/ImageSharp/Image{TPixel}.cs index 87bdf90a1..83be52dd6 100644 --- a/src/ImageSharp/Image{TPixel}.cs +++ b/src/ImageSharp/Image{TPixel}.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Memory; @@ -74,22 +75,22 @@ namespace SixLabors.ImageSharp /// /// Initializes a new instance of the class - /// wrapping an external . + /// wrapping an external . /// /// The configuration providing initialization code which allows extending the library. - /// The memory source. + /// The memory source. /// The width of the image in pixels. /// The height of the image in pixels. /// The images metadata. internal Image( Configuration configuration, - MemorySource memorySource, + MemoryGroup memoryGroup, int width, int height, ImageMetadata metadata) : base(configuration, PixelTypeInfo.Create(), metadata, width, height) { - this.Frames = new ImageFrameCollection(this, width, height, memorySource); + this.Frames = new ImageFrameCollection(this, width, height, memoryGroup); } /// @@ -144,10 +145,22 @@ namespace SixLabors.ImageSharp /// The x-coordinate of the pixel. Must be greater than or equal to zero and less than the width of the image. /// The y-coordinate of the pixel. Must be greater than or equal to zero and less than the height of the image. /// The at the specified position. + /// Thrown when the provided (x,y) coordinates are outside the image boundary. public TPixel this[int x, int y] { - get => this.PixelSource.PixelBuffer[x, y]; - set => this.PixelSource.PixelBuffer[x, y] = value; + [MethodImpl(InliningOptions.ShortMethod)] + get + { + this.VerifyCoords(x, y); + return this.PixelSource.PixelBuffer.GetElementUnsafe(x, y); + } + + [MethodImpl(InliningOptions.ShortMethod)] + set + { + this.VerifyCoords(x, y); + this.PixelSource.PixelBuffer.GetElementUnsafe(x, y) = value; + } } /// @@ -265,5 +278,25 @@ namespace SixLabors.ImageSharp return rootSize; } + + [MethodImpl(InliningOptions.ShortMethod)] + private void VerifyCoords(int x, int y) + { + if (x < 0 || x >= this.Width) + { + ThrowArgumentOutOfRangeException(nameof(x)); + } + + if (y < 0 || y >= this.Height) + { + ThrowArgumentOutOfRangeException(nameof(y)); + } + } + + [MethodImpl(InliningOptions.ColdPath)] + private static void ThrowArgumentOutOfRangeException(string paramName) + { + throw new ArgumentOutOfRangeException(paramName); + } } } diff --git a/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.Buffer{T}.cs b/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.Buffer{T}.cs index 0d7e0b784..7a8b4f8bd 100644 --- a/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.Buffer{T}.cs +++ b/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.Buffer{T}.cs @@ -46,7 +46,15 @@ namespace SixLabors.ImageSharp.Memory protected byte[] Data { get; private set; } /// - public override Span GetSpan() => MemoryMarshal.Cast(this.Data.AsSpan()).Slice(0, this.length); + public override Span GetSpan() + { + if (this.Data == null) + { + throw new ObjectDisposedException("ArrayPoolMemoryAllocator.Buffer"); + } + + return MemoryMarshal.Cast(this.Data.AsSpan()).Slice(0, this.length); + } /// protected override void Dispose(bool disposing) diff --git a/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.CommonFactoryMethods.cs b/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.CommonFactoryMethods.cs index 1ce2525b8..5ef60c9ed 100644 --- a/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.CommonFactoryMethods.cs +++ b/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.CommonFactoryMethods.cs @@ -29,6 +29,9 @@ namespace SixLabors.ImageSharp.Memory /// private const int DefaultNormalPoolBucketCount = 16; + // TODO: This value should be determined by benchmarking + private const int DefaultBufferCapacityInBytes = int.MaxValue / 4; + /// /// This is the default. Should be good for most use cases. /// @@ -39,7 +42,8 @@ namespace SixLabors.ImageSharp.Memory DefaultMaxPooledBufferSizeInBytes, DefaultBufferSelectorThresholdInBytes, DefaultLargePoolBucketCount, - DefaultNormalPoolBucketCount); + DefaultNormalPoolBucketCount, + DefaultBufferCapacityInBytes); } /// @@ -69,4 +73,4 @@ namespace SixLabors.ImageSharp.Memory return new ArrayPoolMemoryAllocator(128 * 1024 * 1024, 32 * 1024 * 1024, 16, 32); } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.cs index c4d92ca3c..8043c1888 100644 --- a/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.cs +++ b/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.cs @@ -60,13 +60,41 @@ namespace SixLabors.ImageSharp.Memory /// The threshold to pool arrays in which has less buckets for memory safety. /// Max arrays per bucket for the large array pool. /// Max arrays per bucket for the normal array pool. - public ArrayPoolMemoryAllocator(int maxPoolSizeInBytes, int poolSelectorThresholdInBytes, int maxArraysPerBucketLargePool, int maxArraysPerBucketNormalPool) + public ArrayPoolMemoryAllocator( + int maxPoolSizeInBytes, + int poolSelectorThresholdInBytes, + int maxArraysPerBucketLargePool, + int maxArraysPerBucketNormalPool) + : this( + maxPoolSizeInBytes, + poolSelectorThresholdInBytes, + maxArraysPerBucketLargePool, + maxArraysPerBucketNormalPool, + DefaultBufferCapacityInBytes) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The maximum size of pooled arrays. Arrays over the thershold are gonna be always allocated. + /// The threshold to pool arrays in which has less buckets for memory safety. + /// Max arrays per bucket for the large array pool. + /// Max arrays per bucket for the normal array pool. + /// The length of the largest contiguous buffer that can be handled by this allocator instance. + public ArrayPoolMemoryAllocator( + int maxPoolSizeInBytes, + int poolSelectorThresholdInBytes, + int maxArraysPerBucketLargePool, + int maxArraysPerBucketNormalPool, + int bufferCapacityInBytes) { Guard.MustBeGreaterThan(maxPoolSizeInBytes, 0, nameof(maxPoolSizeInBytes)); Guard.MustBeLessThanOrEqualTo(poolSelectorThresholdInBytes, maxPoolSizeInBytes, nameof(poolSelectorThresholdInBytes)); this.MaxPoolSizeInBytes = maxPoolSizeInBytes; this.PoolSelectorThresholdInBytes = poolSelectorThresholdInBytes; + this.BufferCapacityInBytes = bufferCapacityInBytes; this.maxArraysPerBucketLargePool = maxArraysPerBucketLargePool; this.maxArraysPerBucketNormalPool = maxArraysPerBucketNormalPool; @@ -83,23 +111,30 @@ namespace SixLabors.ImageSharp.Memory /// public int PoolSelectorThresholdInBytes { get; } + /// + /// Gets the length of the largest contiguous buffer that can be handled by this allocator instance. + /// + public int BufferCapacityInBytes { get; internal set; } // Setter is internal for easy configuration in tests + /// public override void ReleaseRetainedResources() { this.InitArrayPools(); } + /// + protected internal override int GetBufferCapacityInBytes() => this.BufferCapacityInBytes; + /// public override IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None) { Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length)); int itemSizeBytes = Unsafe.SizeOf(); int bufferSizeInBytes = length * itemSizeBytes; - if (bufferSizeInBytes < 0) + if (bufferSizeInBytes < 0 || bufferSizeInBytes > this.BufferCapacityInBytes) { - throw new ArgumentOutOfRangeException( - nameof(length), - $"{nameof(ArrayPoolMemoryAllocator)} can not allocate {length} elements of {typeof(T).Name}."); + throw new InvalidMemoryOperationException( + $"Requested allocation: {length} elements of {typeof(T).Name} is over the capacity of the MemoryAllocator."); } ArrayPool pool = this.GetArrayPool(bufferSizeInBytes); @@ -147,4 +182,4 @@ namespace SixLabors.ImageSharp.Memory this.normalArrayPool = ArrayPool.Create(this.PoolSelectorThresholdInBytes, this.maxArraysPerBucketNormalPool); } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs index 20598c3e3..a4e1de197 100644 --- a/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs +++ b/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System; using System.Buffers; namespace SixLabors.ImageSharp.Memory @@ -10,6 +11,12 @@ namespace SixLabors.ImageSharp.Memory /// public abstract class MemoryAllocator { + /// + /// Gets the length of the largest contiguous buffer that can be handled by this allocator instance in bytes. + /// + /// The length of the largest contiguous buffer that can be handled by this allocator instance. + protected internal abstract int GetBufferCapacityInBytes(); + /// /// Allocates an , holding a of length . /// @@ -17,6 +24,8 @@ namespace SixLabors.ImageSharp.Memory /// Size of the buffer to allocate. /// The allocation options. /// A buffer of values of type . + /// When length is zero or negative. + /// When length is over the capacity of the allocator. public abstract IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None) where T : struct; @@ -26,6 +35,8 @@ namespace SixLabors.ImageSharp.Memory /// The requested buffer length. /// The allocation options. /// The . + /// When length is zero or negative. + /// When length is over the capacity of the allocator. public abstract IManagedByteBuffer AllocateManagedByteBuffer(int length, AllocationOptions options = AllocationOptions.None); /// diff --git a/src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs index 54b64b131..4c62e4ded 100644 --- a/src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs +++ b/src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs @@ -7,10 +7,13 @@ using SixLabors.ImageSharp.Memory.Internals; namespace SixLabors.ImageSharp.Memory { /// - /// Implements by newing up arrays by the GC on every allocation requests. + /// Implements by newing up managed arrays on every allocation request. /// public sealed class SimpleGcMemoryAllocator : MemoryAllocator { + /// + protected internal override int GetBufferCapacityInBytes() => int.MaxValue; + /// public override IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None) { @@ -27,4 +30,4 @@ namespace SixLabors.ImageSharp.Memory return new BasicByteBuffer(new byte[length]); } } -} \ No newline at end of file +} diff --git a/src/ImageSharp/Memory/Buffer2DExtensions.cs b/src/ImageSharp/Memory/Buffer2DExtensions.cs index ba4f9c925..8b0f3845e 100644 --- a/src/ImageSharp/Memory/Buffer2DExtensions.cs +++ b/src/ImageSharp/Memory/Buffer2DExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; +using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -14,62 +15,66 @@ namespace SixLabors.ImageSharp.Memory public static class Buffer2DExtensions { /// - /// Gets a to the backing buffer of . + /// Gets the backing . /// - /// The . - /// The value type. - /// The referencing the memory area. - public static Span GetSpan(this Buffer2D buffer) + /// The buffer. + /// The element type. + /// The MemoryGroup. + public static IMemoryGroup GetMemoryGroup(this Buffer2D buffer) where T : struct { Guard.NotNull(buffer, nameof(buffer)); - return buffer.MemorySource.GetSpan(); + return buffer.FastMemoryGroup.View; } /// - /// Gets the holding the backing buffer of . + /// Gets a to the backing data of + /// if the backing group consists of one single contiguous memory buffer. + /// Throws otherwise. /// /// The . /// The value type. - /// The . - public static Memory GetMemory(this Buffer2D buffer) + /// The referencing the memory area. + /// + /// Thrown when the backing group is discontiguous. + /// + internal static Span GetSingleSpan(this Buffer2D buffer) where T : struct { Guard.NotNull(buffer, nameof(buffer)); - return buffer.MemorySource.Memory; - } + if (buffer.FastMemoryGroup.Count > 1) + { + throw new InvalidOperationException("GetSingleSpan is only valid for a single-buffer group!"); + } - /// - /// Gets a to the row 'y' beginning from the pixel at the first pixel on that row. - /// - /// The buffer - /// The y (row) coordinate - /// The element type - /// The - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Span GetRowSpan(this Buffer2D buffer, int y) - where T : struct - { - Guard.NotNull(buffer, nameof(buffer)); - return buffer.GetSpan().Slice(y * buffer.Width, buffer.Width); + return buffer.FastMemoryGroup.Single().Span; } /// - /// Gets a to the row 'y' beginning from the pixel at the first pixel on that row. + /// Gets a to the backing data of + /// if the backing group consists of one single contiguous memory buffer. + /// Throws otherwise. /// - /// The buffer - /// The y (row) coordinate - /// The element type - /// The - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Memory GetRowMemory(this Buffer2D buffer, int y) + /// The . + /// The value type. + /// The . + /// + /// Thrown when the backing group is discontiguous. + /// + internal static Memory GetSingleMemory(this Buffer2D buffer) where T : struct { Guard.NotNull(buffer, nameof(buffer)); - return buffer.MemorySource.Memory.Slice(y * buffer.Width, buffer.Width); + if (buffer.FastMemoryGroup.Count > 1) + { + throw new InvalidOperationException("GetSingleMemory is only valid for a single-buffer group!"); + } + + return buffer.FastMemoryGroup.Single(); } /// + /// TODO: Does not work with multi-buffer groups, should be specific to Resize. /// Copy columns of inplace, /// from positions starting at to positions at . /// @@ -91,7 +96,7 @@ namespace SixLabors.ImageSharp.Memory int dOffset = destIndex * elementSize; long count = columnCount * elementSize; - Span span = MemoryMarshal.AsBytes(buffer.GetMemory().Span); + Span span = MemoryMarshal.AsBytes(buffer.GetSingleMemory().Span); fixed (byte* ptr = span) { @@ -145,15 +150,6 @@ namespace SixLabors.ImageSharp.Memory where T : struct => new BufferArea(buffer); - /// - /// Gets a span for all the pixels in defined by - /// - internal static Span GetMultiRowSpan(this Buffer2D buffer, in RowInterval rows) - where T : struct - { - return buffer.GetSpan().Slice(rows.Min * buffer.Width, rows.Height * buffer.Width); - } - /// /// Returns the size of the buffer. /// diff --git a/src/ImageSharp/Memory/Buffer2D{T}.cs b/src/ImageSharp/Memory/Buffer2D{T}.cs index 6b7f3bf42..f22b9a875 100644 --- a/src/ImageSharp/Memory/Buffer2D{T}.cs +++ b/src/ImageSharp/Memory/Buffer2D{T}.cs @@ -3,6 +3,7 @@ using System; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.Memory { @@ -14,23 +15,27 @@ namespace SixLabors.ImageSharp.Memory /// Before RC1, this class might be target of API changes, use it on your own risk! /// /// The value type. - // TODO: Consider moving this type to the SixLabors.ImageSharp.Memory namespace (SixLabors.Core). public sealed class Buffer2D : IDisposable where T : struct { - private MemorySource memorySource; + private Memory cachedMemory = default; /// /// Initializes a new instance of the class. /// - /// The buffer to wrap - /// The number of elements in a row - /// The number of rows - internal Buffer2D(MemorySource memorySource, int width, int height) + /// The to wrap. + /// The number of elements in a row. + /// The number of rows. + internal Buffer2D(MemoryGroup memoryGroup, int width, int height) { - this.memorySource = memorySource; + this.FastMemoryGroup = memoryGroup; this.Width = width; this.Height = height; + + if (memoryGroup.Count == 1) + { + this.cachedMemory = memoryGroup[0]; + } } /// @@ -44,9 +49,20 @@ namespace SixLabors.ImageSharp.Memory public int Height { get; private set; } /// - /// Gets the backing + /// Gets the backing . /// - internal MemorySource MemorySource => this.memorySource; + /// The MemoryGroup. + public IMemoryGroup MemoryGroup => this.FastMemoryGroup.View; + + /// + /// Gets the backing without the view abstraction. + /// + /// + /// This property has been kept internal intentionally. + /// It's public counterpart is , + /// which only exposes the view of the MemoryGroup. + /// + internal MemoryGroup FastMemoryGroup { get; } /// /// Gets a reference to the element at the specified position. @@ -54,16 +70,18 @@ namespace SixLabors.ImageSharp.Memory /// The x coordinate (row) /// The y coordinate (position at row) /// A reference to the element. - internal ref T this[int x, int y] + /// When index is out of range of the buffer. + public ref T this[int x, int y] { - [MethodImpl(MethodImplOptions.AggressiveInlining)] + [MethodImpl(InliningOptions.ShortMethod)] get { + DebugGuard.MustBeGreaterThanOrEqualTo(x, 0, nameof(x)); + DebugGuard.MustBeGreaterThanOrEqualTo(y, 0, nameof(y)); DebugGuard.MustBeLessThan(x, this.Width, nameof(x)); DebugGuard.MustBeLessThan(y, this.Height, nameof(y)); - Span span = this.GetSpan(); - return ref span[(this.Width * y) + x]; + return ref this.GetRowSpan(y)[x]; } } @@ -72,7 +90,72 @@ namespace SixLabors.ImageSharp.Memory /// public void Dispose() { - this.MemorySource.Dispose(); + this.FastMemoryGroup.Dispose(); + this.cachedMemory = default; + } + + /// + /// Gets a to the row 'y' beginning from the pixel at the first pixel on that row. + /// + /// + /// This method does not validate the y argument for performance reason, + /// is being propagated from lower levels. + /// + /// The row index. + /// The of the pixels in the row. + /// Thrown when row index is out of range. + [MethodImpl(InliningOptions.ShortMethod)] + public Span GetRowSpan(int y) + { + DebugGuard.MustBeGreaterThanOrEqualTo(y, 0, nameof(y)); + DebugGuard.MustBeLessThan(y, this.Height, nameof(y)); + + return this.cachedMemory.Length > 0 + ? this.cachedMemory.Span.Slice(y * this.Width, this.Width) + : this.GetRowMemorySlow(y).Span; + } + + [MethodImpl(InliningOptions.ShortMethod)] + internal ref T GetElementUnsafe(int x, int y) + { + if (this.cachedMemory.Length > 0) + { + Span span = this.cachedMemory.Span; + ref T start = ref MemoryMarshal.GetReference(span); + return ref Unsafe.Add(ref start, (y * this.Width) + x); + } + + return ref this.GetElementSlow(x, y); + } + + /// + /// Gets a to the row 'y' beginning from the pixel at the first pixel on that row. + /// This method is intended for internal use only, since it does not use the indirection provided by + /// . + /// + /// The y (row) coordinate. + /// The . + [MethodImpl(InliningOptions.ShortMethod)] + internal Memory GetFastRowMemory(int y) + { + DebugGuard.MustBeGreaterThanOrEqualTo(y, 0, nameof(y)); + DebugGuard.MustBeLessThan(y, this.Height, nameof(y)); + return this.cachedMemory.Length > 0 + ? this.cachedMemory.Slice(y * this.Width, this.Width) + : this.GetRowMemorySlow(y); + } + + /// + /// Gets a to the row 'y' beginning from the pixel at the first pixel on that row. + /// + /// The y (row) coordinate. + /// The . + [MethodImpl(InliningOptions.ShortMethod)] + internal Memory GetSafeRowMemory(int y) + { + DebugGuard.MustBeGreaterThanOrEqualTo(y, 0, nameof(y)); + DebugGuard.MustBeLessThan(y, this.Height, nameof(y)); + return this.FastMemoryGroup.View.GetBoundedSlice(y * this.Width, this.Width); } /// @@ -81,11 +164,21 @@ namespace SixLabors.ImageSharp.Memory /// internal static void SwapOrCopyContent(Buffer2D destination, Buffer2D source) { - MemorySource.SwapOrCopyContent(ref destination.memorySource, ref source.memorySource); - SwapDimensionData(destination, source); + bool swap = MemoryGroup.SwapOrCopyContent(destination.FastMemoryGroup, source.FastMemoryGroup); + SwapOwnData(destination, source, swap); + } + + [MethodImpl(InliningOptions.ColdPath)] + private Memory GetRowMemorySlow(int y) => this.FastMemoryGroup.GetBoundedSlice(y * this.Width, this.Width); + + [MethodImpl(InliningOptions.ColdPath)] + private ref T GetElementSlow(int x, int y) + { + Span span = this.GetRowMemorySlow(y).Span; + return ref span[x]; } - private static void SwapDimensionData(Buffer2D a, Buffer2D b) + private static void SwapOwnData(Buffer2D a, Buffer2D b, bool swapCachedMemory) { Size aSize = a.Size(); Size bSize = b.Size(); @@ -95,6 +188,13 @@ namespace SixLabors.ImageSharp.Memory a.Width = bSize.Width; a.Height = bSize.Height; + + if (swapCachedMemory) + { + Memory aCached = a.cachedMemory; + a.cachedMemory = b.cachedMemory; + b.cachedMemory = aCached; + } } } } diff --git a/src/ImageSharp/Memory/BufferArea{T}.cs b/src/ImageSharp/Memory/BufferArea{T}.cs index 08731846e..f5cbc6953 100644 --- a/src/ImageSharp/Memory/BufferArea{T}.cs +++ b/src/ImageSharp/Memory/BufferArea{T}.cs @@ -9,7 +9,7 @@ namespace SixLabors.ImageSharp.Memory /// Represents a rectangular area inside a 2D memory buffer (). /// This type is kind-of 2D Span, but it can live on heap. /// - /// The element type + /// The element type. internal readonly struct BufferArea where T : struct { @@ -72,7 +72,7 @@ namespace SixLabors.ImageSharp.Memory /// The position inside a row /// The row index /// The reference to the value - public ref T this[int x, int y] => ref this.DestinationBuffer.GetSpan()[this.GetIndexOf(x, y)]; + public ref T this[int x, int y] => ref this.DestinationBuffer[x + this.Rectangle.X, y + this.Rectangle.Y]; /// /// Gets a reference to the [0,0] element. @@ -80,7 +80,7 @@ namespace SixLabors.ImageSharp.Memory /// The reference to the [0,0] element [MethodImpl(MethodImplOptions.AggressiveInlining)] public ref T GetReferenceToOrigin() => - ref this.DestinationBuffer.GetSpan()[(this.Rectangle.Y * this.DestinationBuffer.Width) + this.Rectangle.X]; + ref this.GetRowSpan(0)[0]; /// /// Gets a span to row 'y' inside this area. @@ -94,16 +94,16 @@ namespace SixLabors.ImageSharp.Memory int xx = this.Rectangle.X; int width = this.Rectangle.Width; - return this.DestinationBuffer.GetSpan().Slice(yy + xx, width); + return this.DestinationBuffer.FastMemoryGroup.GetBoundedSlice(yy + xx, width).Span; } /// /// Returns a sub-area as . (Similar to .) /// - /// The x index at the subarea origo - /// The y index at the subarea origo - /// The desired width of the subarea - /// The desired height of the subarea + /// The x index at the subarea origin. + /// The y index at the subarea origin. + /// The desired width of the subarea. + /// The desired height of the subarea. /// The subarea [MethodImpl(MethodImplOptions.AggressiveInlining)] public BufferArea GetSubArea(int x, int y, int width, int height) @@ -129,14 +129,6 @@ namespace SixLabors.ImageSharp.Memory return new BufferArea(this.DestinationBuffer, rectangle); } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private int GetIndexOf(int x, int y) - { - int yy = this.GetRowIndex(y); - int xx = this.Rectangle.X + x; - return yy + xx; - } - [MethodImpl(MethodImplOptions.AggressiveInlining)] internal int GetRowIndex(int y) { @@ -148,7 +140,7 @@ namespace SixLabors.ImageSharp.Memory // Optimization for when the size of the area is the same as the buffer size. if (this.IsFullBufferArea) { - this.DestinationBuffer.GetSpan().Clear(); + this.DestinationBuffer.FastMemoryGroup.Clear(); return; } diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs new file mode 100644 index 000000000..2649b7fb1 --- /dev/null +++ b/src/ImageSharp/Memory/DiscontiguousBuffers/IMemoryGroup{T}.cs @@ -0,0 +1,37 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Memory +{ + /// + /// Represents discontiguous group of multiple uniformly-sized memory segments. + /// The last segment can be smaller than the preceding ones. + /// + /// The element type. + public interface IMemoryGroup : IReadOnlyList> + where T : struct + { + /// + /// Gets the number of elements per contiguous sub-buffer preceding the last buffer. + /// The last buffer is allowed to be smaller. + /// + public int BufferLength { get; } + + /// + /// Gets the aggregate number of elements in the group. + /// + public long TotalLength { get; } + + /// + /// Gets a value indicating whether the group has been invalidated. + /// + /// + /// Invalidation usually occurs when an image processor capable to alter the image dimensions replaces + /// the image buffers internally. + /// + bool IsValid { get; } + } +} diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs new file mode 100644 index 000000000..28da49263 --- /dev/null +++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupExtensions.cs @@ -0,0 +1,232 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Memory +{ + internal static class MemoryGroupExtensions + { + internal static void Fill(this IMemoryGroup group, T value) + where T : struct + { + foreach (Memory memory in group) + { + memory.Span.Fill(value); + } + } + + internal static void Clear(this IMemoryGroup group) + where T : struct + { + foreach (Memory memory in group) + { + memory.Span.Clear(); + } + } + + /// + /// Returns a slice that is expected to be within the bounds of a single buffer. + /// Otherwise is thrown. + /// + internal static Memory GetBoundedSlice(this IMemoryGroup group, long start, int length) + where T : struct + { + Guard.NotNull(group, nameof(group)); + Guard.IsTrue(group.IsValid, nameof(group), "Group must be valid!"); + Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length)); + Guard.MustBeLessThan(start, group.TotalLength, nameof(start)); + + int bufferIdx = (int)(start / group.BufferLength); + if (bufferIdx >= group.Count) + { + throw new ArgumentOutOfRangeException(nameof(start)); + } + + int bufferStart = (int)(start % group.BufferLength); + int bufferEnd = bufferStart + length; + Memory memory = group[bufferIdx]; + + if (bufferEnd > memory.Length) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + return memory.Slice(bufferStart, length); + } + + internal static void CopyTo(this IMemoryGroup source, Span target) + where T : struct + { + Guard.NotNull(source, nameof(source)); + Guard.MustBeGreaterThanOrEqualTo(target.Length, source.TotalLength, nameof(target)); + + var cur = new MemoryGroupCursor(source); + long position = 0; + while (position < source.TotalLength) + { + int fwd = Math.Min(cur.LookAhead(), target.Length); + cur.GetSpan(fwd).CopyTo(target); + + cur.Forward(fwd); + target = target.Slice(fwd); + position += fwd; + } + } + + internal static void CopyTo(this Span source, IMemoryGroup target) + where T : struct + => CopyTo((ReadOnlySpan)source, target); + + internal static void CopyTo(this ReadOnlySpan source, IMemoryGroup target) + where T : struct + { + Guard.NotNull(target, nameof(target)); + Guard.MustBeGreaterThanOrEqualTo(target.TotalLength, source.Length, nameof(target)); + + var cur = new MemoryGroupCursor(target); + + while (!source.IsEmpty) + { + int fwd = Math.Min(cur.LookAhead(), source.Length); + source.Slice(0, fwd).CopyTo(cur.GetSpan(fwd)); + cur.Forward(fwd); + source = source.Slice(fwd); + } + } + + internal static void CopyTo(this IMemoryGroup source, IMemoryGroup target) + where T : struct + { + Guard.NotNull(source, nameof(source)); + Guard.NotNull(target, nameof(target)); + Guard.IsTrue(source.IsValid, nameof(source), "Source group must be valid."); + Guard.IsTrue(target.IsValid, nameof(target), "Target group must be valid."); + Guard.MustBeLessThanOrEqualTo(source.TotalLength, target.TotalLength, "Destination buffer too short!"); + + if (source.IsEmpty()) + { + return; + } + + long position = 0; + var srcCur = new MemoryGroupCursor(source); + var trgCur = new MemoryGroupCursor(target); + + while (position < source.TotalLength) + { + int fwd = Math.Min(srcCur.LookAhead(), trgCur.LookAhead()); + Span srcSpan = srcCur.GetSpan(fwd); + Span trgSpan = trgCur.GetSpan(fwd); + srcSpan.CopyTo(trgSpan); + + srcCur.Forward(fwd); + trgCur.Forward(fwd); + position += fwd; + } + } + + internal static void TransformTo( + this IMemoryGroup source, + IMemoryGroup target, + TransformItemsDelegate transform) + where TSource : struct + where TTarget : struct + { + Guard.NotNull(source, nameof(source)); + Guard.NotNull(target, nameof(target)); + Guard.NotNull(transform, nameof(transform)); + Guard.IsTrue(source.IsValid, nameof(source), "Source group must be valid."); + Guard.IsTrue(target.IsValid, nameof(target), "Target group must be valid."); + Guard.MustBeLessThanOrEqualTo(source.TotalLength, target.TotalLength, "Destination buffer too short!"); + + if (source.IsEmpty()) + { + return; + } + + long position = 0; + var srcCur = new MemoryGroupCursor(source); + var trgCur = new MemoryGroupCursor(target); + + while (position < source.TotalLength) + { + int fwd = Math.Min(srcCur.LookAhead(), trgCur.LookAhead()); + Span srcSpan = srcCur.GetSpan(fwd); + Span trgSpan = trgCur.GetSpan(fwd); + transform(srcSpan, trgSpan); + + srcCur.Forward(fwd); + trgCur.Forward(fwd); + position += fwd; + } + } + + internal static void TransformInplace( + this IMemoryGroup memoryGroup, + TransformItemsInplaceDelegate transform) + where T : struct + { + foreach (Memory memory in memoryGroup) + { + transform(memory.Span); + } + } + + internal static bool IsEmpty(this IMemoryGroup group) + where T : struct + => group.Count == 0; + + private struct MemoryGroupCursor + where T : struct + { + private readonly IMemoryGroup memoryGroup; + + private int bufferIndex; + + private int elementIndex; + + public MemoryGroupCursor(IMemoryGroup memoryGroup) + { + this.memoryGroup = memoryGroup; + this.bufferIndex = 0; + this.elementIndex = 0; + } + + private bool IsAtLastBuffer => this.bufferIndex == this.memoryGroup.Count - 1; + + private int CurrentBufferLength => this.memoryGroup[this.bufferIndex].Length; + + public Span GetSpan(int length) + { + return this.memoryGroup[this.bufferIndex].Span.Slice(this.elementIndex, length); + } + + public int LookAhead() + { + return this.CurrentBufferLength - this.elementIndex; + } + + public void Forward(int steps) + { + int nextIdx = this.elementIndex + steps; + int currentBufferLength = this.CurrentBufferLength; + + if (nextIdx < currentBufferLength) + { + this.elementIndex = nextIdx; + } + else if (nextIdx == currentBufferLength) + { + this.bufferIndex++; + this.elementIndex = 0; + } + else + { + // If we get here, it indicates a bug in CopyTo: + throw new ArgumentException("Can't forward multiple buffers!", nameof(steps)); + } + } + } + } +} diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupView{T}.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupView{T}.cs new file mode 100644 index 000000000..3f39ba12f --- /dev/null +++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroupView{T}.cs @@ -0,0 +1,134 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Collections; +using System.Collections.Generic; + +namespace SixLabors.ImageSharp.Memory +{ + /// + /// Implements , defining a view for + /// rather than owning the segments. + /// + /// + /// This type provides an indirection, protecting the users of publicly exposed memory API-s + /// from internal memory-swaps. Whenever an internal swap happens, the + /// instance becomes invalid, throwing an exception on all operations. + /// + /// The element type. + internal class MemoryGroupView : IMemoryGroup + where T : struct + { + private MemoryGroup owner; + private readonly MemoryOwnerWrapper[] memoryWrappers; + + public MemoryGroupView(MemoryGroup owner) + { + this.owner = owner; + this.memoryWrappers = new MemoryOwnerWrapper[owner.Count]; + + for (int i = 0; i < owner.Count; i++) + { + this.memoryWrappers[i] = new MemoryOwnerWrapper(this, i); + } + } + + public int Count + { + get + { + this.EnsureIsValid(); + return this.owner.Count; + } + } + + public int BufferLength + { + get + { + this.EnsureIsValid(); + return this.owner.BufferLength; + } + } + + public long TotalLength + { + get + { + this.EnsureIsValid(); + return this.owner.TotalLength; + } + } + + public bool IsValid => this.owner != null; + + public Memory this[int index] + { + get + { + this.EnsureIsValid(); + return this.memoryWrappers[index].Memory; + } + } + + public IEnumerator> GetEnumerator() + { + this.EnsureIsValid(); + for (int i = 0; i < this.Count; i++) + { + yield return this.memoryWrappers[i].Memory; + } + } + + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + internal void Invalidate() + { + this.owner = null; + } + + private void EnsureIsValid() + { + if (!this.IsValid) + { + throw new InvalidMemoryOperationException("Can not access an invalidated MemoryGroupView!"); + } + } + + private class MemoryOwnerWrapper : MemoryManager + { + private readonly MemoryGroupView view; + + private readonly int index; + + public MemoryOwnerWrapper(MemoryGroupView view, int index) + { + this.view = view; + this.index = index; + } + + protected override void Dispose(bool disposing) + { + } + + public override Span GetSpan() + { + this.view.EnsureIsValid(); + return this.view.owner[this.index].Span; + } + + public override MemoryHandle Pin(int elementIndex = 0) + { + this.view.EnsureIsValid(); + return this.view.owner[this.index].Pin(); + } + + public override void Unpin() + { + throw new NotSupportedException(); + } + } + } +} diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs new file mode 100644 index 000000000..f1fe4ed9c --- /dev/null +++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs @@ -0,0 +1,43 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Linq; + +namespace SixLabors.ImageSharp.Memory +{ + internal abstract partial class MemoryGroup + { + // Analogous to the "consumed" variant of MemorySource + private sealed class Consumed : MemoryGroup + { + private readonly Memory[] source; + + public Consumed(Memory[] source, int bufferLength, long totalLength) + : base(bufferLength, totalLength) + { + this.source = source; + this.View = new MemoryGroupView(this); + } + + public override int Count => this.source.Length; + + public override Memory this[int index] => this.source[index]; + + public override IEnumerator> GetEnumerator() + { + for (int i = 0; i < this.source.Length; i++) + { + yield return this.source[i]; + } + } + + public override void Dispose() + { + this.View.Invalidate(); + } + } + } +} diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs new file mode 100644 index 000000000..b42b90d28 --- /dev/null +++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs @@ -0,0 +1,104 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Linq; + +namespace SixLabors.ImageSharp.Memory +{ + // Analogous to the "owned" variant of MemorySource + internal abstract partial class MemoryGroup + { + private sealed class Owned : MemoryGroup + { + private IMemoryOwner[] memoryOwners; + + public Owned(IMemoryOwner[] memoryOwners, int bufferLength, long totalLength, bool swappable) + : base(bufferLength, totalLength) + { + this.memoryOwners = memoryOwners; + this.Swappable = swappable; + this.View = new MemoryGroupView(this); + } + + public bool Swappable { get; } + + private bool IsDisposed => this.memoryOwners == null; + + public override int Count + { + get + { + this.EnsureNotDisposed(); + return this.memoryOwners.Length; + } + } + + public override Memory this[int index] + { + get + { + this.EnsureNotDisposed(); + return this.memoryOwners[index].Memory; + } + } + + public override IEnumerator> GetEnumerator() + { + this.EnsureNotDisposed(); + return this.memoryOwners.Select(mo => mo.Memory).GetEnumerator(); + } + + public override void Dispose() + { + if (this.IsDisposed) + { + return; + } + + this.View.Invalidate(); + + foreach (IMemoryOwner memoryOwner in this.memoryOwners) + { + memoryOwner.Dispose(); + } + + this.memoryOwners = null; + this.IsValid = false; + } + + private void EnsureNotDisposed() + { + if (this.memoryOwners == null) + { + throw new ObjectDisposedException(nameof(MemoryGroup)); + } + } + + internal static void SwapContents(Owned a, Owned b) + { + a.EnsureNotDisposed(); + b.EnsureNotDisposed(); + + IMemoryOwner[] tempOwners = a.memoryOwners; + long tempTotalLength = a.TotalLength; + int tempBufferLength = a.BufferLength; + + a.memoryOwners = b.memoryOwners; + a.TotalLength = b.TotalLength; + a.BufferLength = b.BufferLength; + + b.memoryOwners = tempOwners; + b.TotalLength = tempTotalLength; + b.BufferLength = tempBufferLength; + + a.View.Invalidate(); + b.View.Invalidate(); + a.View = new MemoryGroupView(a); + b.View = new MemoryGroupView(b); + } + } + } +} diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs new file mode 100644 index 000000000..38de57b4a --- /dev/null +++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs @@ -0,0 +1,190 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory.Internals; + +namespace SixLabors.ImageSharp.Memory +{ + /// + /// Represents discontinuous group of multiple uniformly-sized memory segments. + /// The underlying buffers may change with time, therefore it's not safe to expose them directly on + /// and . + /// + /// The element type. + internal abstract partial class MemoryGroup : IMemoryGroup, IDisposable + where T : struct + { + private static readonly int ElementSize = Unsafe.SizeOf(); + + private MemoryGroup(int bufferLength, long totalLength) + { + this.BufferLength = bufferLength; + this.TotalLength = totalLength; + } + + /// + public abstract int Count { get; } + + /// + public int BufferLength { get; private set; } + + /// + public long TotalLength { get; private set; } + + /// + public bool IsValid { get; private set; } = true; + + public MemoryGroupView View { get; private set; } + + /// + public abstract Memory this[int index] { get; } + + /// + public abstract void Dispose(); + + /// + public abstract IEnumerator> GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => this.GetEnumerator(); + + /// + /// Creates a new memory group, allocating it's buffers with the provided allocator. + /// + /// The to use. + /// The total length of the buffer. + /// The expected alignment (eg. to make sure image rows fit into single buffers). + /// The . + /// A new . + /// Thrown when 'blockAlignment' converted to bytes is greater than the buffer capacity of the allocator. + public static MemoryGroup Allocate( + MemoryAllocator allocator, + long totalLength, + int bufferAlignment, + AllocationOptions options = AllocationOptions.None) + { + Guard.NotNull(allocator, nameof(allocator)); + Guard.MustBeGreaterThanOrEqualTo(totalLength, 0, nameof(totalLength)); + Guard.MustBeGreaterThanOrEqualTo(bufferAlignment, 0, nameof(bufferAlignment)); + + int blockCapacityInElements = allocator.GetBufferCapacityInBytes() / ElementSize; + + if (bufferAlignment > blockCapacityInElements) + { + throw new InvalidMemoryOperationException( + $"The buffer capacity of the provided MemoryAllocator is insufficient for the requested buffer alignment: {bufferAlignment}."); + } + + if (totalLength == 0) + { + var buffers0 = new IMemoryOwner[1] { allocator.Allocate(0, options) }; + return new Owned(buffers0, 0, 0, true); + } + + int numberOfAlignedSegments = blockCapacityInElements / bufferAlignment; + int bufferLength = numberOfAlignedSegments * bufferAlignment; + if (totalLength > 0 && totalLength < bufferLength) + { + bufferLength = (int)totalLength; + } + + int sizeOfLastBuffer = (int)(totalLength % bufferLength); + long bufferCount = totalLength / bufferLength; + + if (sizeOfLastBuffer == 0) + { + sizeOfLastBuffer = bufferLength; + } + else + { + bufferCount++; + } + + var buffers = new IMemoryOwner[bufferCount]; + for (int i = 0; i < buffers.Length - 1; i++) + { + buffers[i] = allocator.Allocate(bufferLength, options); + } + + if (bufferCount > 0) + { + buffers[buffers.Length - 1] = allocator.Allocate(sizeOfLastBuffer, options); + } + + return new Owned(buffers, bufferLength, totalLength, true); + } + + public static MemoryGroup Wrap(params Memory[] source) + { + int bufferLength = source.Length > 0 ? source[0].Length : 0; + for (int i = 1; i < source.Length - 1; i++) + { + if (source[i].Length != bufferLength) + { + throw new InvalidMemoryOperationException("Wrap: buffers should be uniformly sized!"); + } + } + + if (source.Length > 0 && source[source.Length - 1].Length > bufferLength) + { + throw new InvalidMemoryOperationException("Wrap: the last buffer is too large!"); + } + + long totalLength = bufferLength > 0 ? ((long)bufferLength * (source.Length - 1)) + source[source.Length - 1].Length : 0; + + return new Consumed(source, bufferLength, totalLength); + } + + public static MemoryGroup Wrap(params IMemoryOwner[] source) + { + int bufferLength = source.Length > 0 ? source[0].Memory.Length : 0; + for (int i = 1; i < source.Length - 1; i++) + { + if (source[i].Memory.Length != bufferLength) + { + throw new InvalidMemoryOperationException("Wrap: buffers should be uniformly sized!"); + } + } + + if (source.Length > 0 && source[source.Length - 1].Memory.Length > bufferLength) + { + throw new InvalidMemoryOperationException("Wrap: the last buffer is too large!"); + } + + long totalLength = bufferLength > 0 ? ((long)bufferLength * (source.Length - 1)) + source[source.Length - 1].Memory.Length : 0; + + return new Owned(source, bufferLength, totalLength, false); + } + + /// + /// 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. + /// + public static bool SwapOrCopyContent(MemoryGroup target, MemoryGroup source) + { + if (source is Owned ownedSrc && ownedSrc.Swappable && + target is Owned ownedTarget && ownedTarget.Swappable) + { + Owned.SwapContents(ownedTarget, ownedSrc); + return true; + } + else + { + if (target.TotalLength != source.TotalLength) + { + throw new InvalidMemoryOperationException( + "Trying to copy/swap incompatible buffers. This is most likely caused by applying an unsupported processor to wrapped-memory images."); + } + + source.CopyTo(target); + return false; + } + } + } +} diff --git a/src/ImageSharp/Memory/InvalidMemoryOperationException.cs b/src/ImageSharp/Memory/InvalidMemoryOperationException.cs new file mode 100644 index 000000000..c1d5c5d41 --- /dev/null +++ b/src/ImageSharp/Memory/InvalidMemoryOperationException.cs @@ -0,0 +1,30 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Memory +{ + /// + /// Exception thrown when the library detects an invalid memory allocation request, + /// or an attempt has been made to use an invalidated . + /// + public class InvalidMemoryOperationException : InvalidOperationException + { + /// + /// Initializes a new instance of the class. + /// + /// The exception message text. + public InvalidMemoryOperationException(string message) + : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + public InvalidMemoryOperationException() + { + } + } +} diff --git a/src/ImageSharp/Memory/MemoryAllocatorExtensions.cs b/src/ImageSharp/Memory/MemoryAllocatorExtensions.cs index 6e317bb8f..22d1bddd2 100644 --- a/src/ImageSharp/Memory/MemoryAllocatorExtensions.cs +++ b/src/ImageSharp/Memory/MemoryAllocatorExtensions.cs @@ -17,7 +17,7 @@ namespace SixLabors.ImageSharp.Memory /// The type of buffer items to allocate. /// The memory allocator. /// The buffer width. - /// The buffer heght. + /// The buffer height. /// The allocation options. /// The . public static Buffer2D Allocate2D( @@ -27,10 +27,9 @@ namespace SixLabors.ImageSharp.Memory AllocationOptions options = AllocationOptions.None) where T : struct { - IMemoryOwner buffer = memoryAllocator.Allocate(width * height, options); - var memorySource = new MemorySource(buffer, true); - - return new Buffer2D(memorySource, width, height); + long groupLength = (long)width * height; + MemoryGroup memoryGroup = memoryAllocator.AllocateGroup(groupLength, width, options); + return new Buffer2D(memoryGroup, width, height); } /// @@ -49,14 +48,30 @@ namespace SixLabors.ImageSharp.Memory where T : struct => Allocate2D(memoryAllocator, size.Width, size.Height, options); + internal static Buffer2D Allocate2DOveraligned( + this MemoryAllocator memoryAllocator, + int width, + int height, + int alignmentMultiplier, + AllocationOptions options = AllocationOptions.None) + where T : struct + { + long groupLength = (long)width * height; + MemoryGroup memoryGroup = memoryAllocator.AllocateGroup( + groupLength, + width * alignmentMultiplier, + options); + return new Buffer2D(memoryGroup, width, height); + } + /// - /// Allocates padded buffers for BMP encoder/decoder. (Replacing old PixelRow/PixelArea) + /// Allocates padded buffers for BMP encoder/decoder. (Replacing old PixelRow/PixelArea). /// - /// The + /// The . /// Pixel count in the row - /// The pixel size in bytes, eg. 3 for RGB - /// The padding - /// A + /// The pixel size in bytes, eg. 3 for RGB. + /// The padding. + /// A . internal static IManagedByteBuffer AllocatePaddedPixelRowBuffer( this MemoryAllocator memoryAllocator, int width, @@ -66,5 +81,22 @@ namespace SixLabors.ImageSharp.Memory int length = (width * pixelSizeInBytes) + paddingInBytes; return memoryAllocator.AllocateManagedByteBuffer(length); } + + /// + /// Allocates a . + /// + /// The to use. + /// The total length of the buffer. + /// The expected alignment (eg. to make sure image rows fit into single buffers). + /// The . + /// A new . + /// Thrown when 'blockAlignment' converted to bytes is greater than the buffer capacity of the allocator. + internal static MemoryGroup AllocateGroup( + this MemoryAllocator memoryAllocator, + long totalLength, + int bufferAlignment, + AllocationOptions options = AllocationOptions.None) + where T : struct + => MemoryGroup.Allocate(memoryAllocator, totalLength, bufferAlignment, options); } } diff --git a/src/ImageSharp/Memory/MemorySource.cs b/src/ImageSharp/Memory/MemorySource.cs deleted file mode 100644 index 54f1bb0d1..000000000 --- a/src/ImageSharp/Memory/MemorySource.cs +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.Buffers; - -namespace SixLabors.ImageSharp.Memory -{ - /// - /// Holds a that is either OWNED or CONSUMED. - /// When the memory is being owned, the instance is also known. - /// Implements content transfer logic in that depends on the ownership status. - /// This is needed to transfer the contents of a temporary - /// to a persistent without copying the buffer. - /// - /// - /// For a deeper understanding of the owner/consumer model, check out the following docs:
- /// https://gist.github.com/GrabYourPitchforks/4c3e1935fd4d9fa2831dbfcab35dffc6 - /// https://www.codemag.com/Article/1807051/Introducing-.NET-Core-2.1-Flagship-Types-Span-T-and-Memory-T - ///
- internal struct MemorySource : IDisposable - { - /// - /// Initializes a new instance of the struct - /// by wrapping an existing . - /// - /// The to wrap - /// - /// A value indicating whether is an internal memory source managed by ImageSharp. - /// Eg. allocated by a . - /// - public MemorySource(IMemoryOwner memoryOwner, bool isInternalMemorySource) - { - this.MemoryOwner = memoryOwner; - this.Memory = memoryOwner.Memory; - this.HasSwappableContents = isInternalMemorySource; - } - - public MemorySource(Memory memory) - { - this.Memory = memory; - this.MemoryOwner = null; - this.HasSwappableContents = false; - } - - public IMemoryOwner MemoryOwner { get; private set; } - - public Memory Memory { get; private set; } - - /// - /// Gets a value indicating whether we are allowed to swap the contents of this buffer - /// with an other instance. - /// The value is true only and only if is present, - /// and it's coming from an internal source managed by ImageSharp (). - /// - public bool HasSwappableContents { get; } - - public Span GetSpan() => this.Memory.Span; - - public void Clear() => this.Memory.Span.Clear(); - - /// - /// 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! - /// - public static void SwapOrCopyContent(ref MemorySource destination, ref MemorySource source) - { - if (source.HasSwappableContents && destination.HasSwappableContents) - { - SwapContents(ref destination, ref source); - } - else - { - if (destination.Memory.Length != source.Memory.Length) - { - throw new InvalidOperationException("SwapOrCopyContents(): buffers should both owned or the same size!"); - } - - source.Memory.CopyTo(destination.Memory); - } - } - - /// - public void Dispose() - { - this.MemoryOwner?.Dispose(); - } - - private static void SwapContents(ref MemorySource a, ref MemorySource b) - { - IMemoryOwner tempOwner = a.MemoryOwner; - Memory tempMemory = a.Memory; - - a.MemoryOwner = b.MemoryOwner; - a.Memory = b.Memory; - - b.MemoryOwner = tempOwner; - b.Memory = tempMemory; - } - } -} \ No newline at end of file diff --git a/src/ImageSharp/Memory/TransformItemsDelegate{T}.cs b/src/ImageSharp/Memory/TransformItemsDelegate{T}.cs new file mode 100644 index 000000000..31825b7b4 --- /dev/null +++ b/src/ImageSharp/Memory/TransformItemsDelegate{T}.cs @@ -0,0 +1,9 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Memory +{ + internal delegate void TransformItemsDelegate(ReadOnlySpan source, Span target); +} diff --git a/src/ImageSharp/Memory/TransformItemsInplaceDelegate.cs b/src/ImageSharp/Memory/TransformItemsInplaceDelegate.cs new file mode 100644 index 000000000..023606f52 --- /dev/null +++ b/src/ImageSharp/Memory/TransformItemsInplaceDelegate.cs @@ -0,0 +1,9 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; + +namespace SixLabors.ImageSharp.Memory +{ + internal delegate void TransformItemsInplaceDelegate(Span data); +} diff --git a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor{TPixel}.cs index 0d9055f34..574d3cb18 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/AffineTransformProcessor{TPixel}.cs @@ -45,7 +45,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms if (this.transformMatrix.Equals(default) || this.transformMatrix.Equals(Matrix3x2.Identity)) { // The clone will be blank here copy all the pixel data over - source.GetPixelSpan().CopyTo(destination.GetPixelSpan()); + source.GetPixelMemoryGroup().CopyTo(destination.GetPixelMemoryGroup()); return; } diff --git a/src/ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs index 6ad7aa2a2..e8eeea3cb 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs @@ -41,7 +41,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms && this.SourceRectangle == this.cropRectangle) { // the cloned will be blank here copy all the pixel data over - source.GetPixelSpan().CopyTo(destination.GetPixelSpan()); + source.GetPixelMemoryGroup().CopyTo(destination.GetPixelMemoryGroup()); return; } diff --git a/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor{TPixel}.cs index da071e3f2..175615ebd 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/ProjectiveTransformProcessor{TPixel}.cs @@ -45,7 +45,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms if (this.transformMatrix.Equals(default) || this.transformMatrix.Equals(Matrix4x4.Identity)) { // The clone will be blank here copy all the pixel data over - source.GetPixelSpan().CopyTo(destination.GetPixelSpan()); + source.GetPixelMemoryGroup().CopyTo(destination.GetPixelMemoryGroup()); return; } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs index 8e8eaceb0..cc9516956 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs @@ -55,7 +55,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.DestinationLength = destinationLength; this.MaxDiameter = (radius * 2) + 1; this.data = memoryAllocator.Allocate2D(this.MaxDiameter, bufferHeight, AllocationOptions.Clean); - this.pinHandle = this.data.GetMemory().Pin(); + this.pinHandle = this.data.GetSingleMemory().Pin(); this.kernels = new ResizeKernel[destinationLength]; this.tempValues = new double[this.MaxDiameter]; } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs index 53810a5cc..92c1b71fe 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs @@ -81,7 +81,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms && sourceRectangle == this.targetRectangle) { // The cloned will be blank here copy all the pixel data over - source.GetPixelSpan().CopyTo(destination.GetPixelSpan()); + source.GetPixelMemoryGroup().CopyTo(destination.GetPixelMemoryGroup()); return; } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs index 7cfd8e592..de339823e 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs @@ -74,10 +74,15 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms this.windowBandHeight = verticalKernelMap.MaxDiameter; + // We need to make sure the working buffer is contiguous: + int workingBufferLimitHintInBytes = Math.Min( + configuration.WorkingBufferSizeHintInBytes, + configuration.MemoryAllocator.GetBufferCapacityInBytes()); + int numberOfWindowBands = ResizeHelper.CalculateResizeWorkerHeightInWindowBands( this.windowBandHeight, destWidth, - configuration.WorkingBufferSizeHintInBytes); + workingBufferLimitHintInBytes); this.workerHeight = Math.Min(this.sourceRectangle.Height, numberOfWindowBands * this.windowBandHeight); @@ -113,7 +118,9 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms public void FillDestinationPixels(RowInterval rowInterval, Buffer2D destination) { Span tempColSpan = this.tempColumnBuffer.GetSpan(); - Span transposedFirstPassBufferSpan = this.transposedFirstPassBuffer.GetSpan(); + + // When creating transposedFirstPassBuffer, we made sure it's contiguous: + Span transposedFirstPassBufferSpan = this.transposedFirstPassBuffer.GetSingleSpan(); for (int y = rowInterval.Min; y < rowInterval.Max; y++) { @@ -165,7 +172,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms private void CalculateFirstPassValues(RowInterval calculationInterval) { Span tempRowSpan = this.tempRowBuffer.GetSpan(); - Span transposedFirstPassBufferSpan = this.transposedFirstPassBuffer.GetSpan(); + Span transposedFirstPassBufferSpan = this.transposedFirstPassBuffer.GetSingleSpan(); for (int y = calculationInterval.Min; y < calculationInterval.Max; y++) { diff --git a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor{TPixel}.cs index 086314a26..198e142d0 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/RotateProcessor{TPixel}.cs @@ -98,7 +98,7 @@ namespace SixLabors.ImageSharp.Processing.Processors.Transforms if (MathF.Abs(degrees) < Constants.Epsilon) { // The destination will be blank here so copy all the pixel data over - source.GetPixelSpan().CopyTo(destination.GetPixelSpan()); + source.GetPixelMemoryGroup().CopyTo(destination.GetPixelMemoryGroup()); return true; } diff --git a/tests/ImageSharp.Tests/Advanced/AdvancedImageExtensionsTests.cs b/tests/ImageSharp.Tests/Advanced/AdvancedImageExtensionsTests.cs index f6b51e8c5..f9a562b9d 100644 --- a/tests/ImageSharp.Tests/Advanced/AdvancedImageExtensionsTests.cs +++ b/tests/ImageSharp.Tests/Advanced/AdvancedImageExtensionsTests.cs @@ -2,9 +2,13 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Linq; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers; using Xunit; // ReSharper disable InconsistentNaming @@ -12,137 +16,145 @@ namespace SixLabors.ImageSharp.Tests.Advanced { public class AdvancedImageExtensionsTests { - public class GetPixelMemory + public class GetPixelMemoryGroup { [Theory] - [WithSolidFilledImages(1, 1, "Red", PixelTypes.Rgba32)] - [WithTestPatternImages(131, 127, PixelTypes.Rgba32 | PixelTypes.Bgr24)] - public void WhenMemoryIsOwned(TestImageProvider provider) + [WithBasicTestPatternImages(1, 1, PixelTypes.Rgba32)] + [WithBasicTestPatternImages(131, 127, PixelTypes.Rgba32)] + [WithBasicTestPatternImages(333, 555, PixelTypes.Bgr24)] + public void OwnedMemory_PixelDataIsCorrect(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image0 = provider.GetImage()) - { - var targetBuffer = new TPixel[image0.Width * image0.Height]; + provider.LimitAllocatorBufferCapacity().InPixelsSqrt(200); - // Act: - Memory memory = image0.GetPixelMemory(); + using Image image = provider.GetImage(); - // Assert: - Assert.Equal(image0.Width * image0.Height, memory.Length); - memory.Span.CopyTo(targetBuffer); + // Act: + IMemoryGroup memoryGroup = image.GetPixelMemoryGroup(); - using (Image image1 = provider.GetImage()) - { - // We are using a copy of the original image for assertion - image1.ComparePixelBufferTo(targetBuffer); - } - } + // Assert: + VerifyMemoryGroupDataMatchesTestPattern(provider, memoryGroup, image.Size()); } [Theory] - [WithSolidFilledImages(1, 1, "Red", PixelTypes.Rgba32 | PixelTypes.Bgr24)] - [WithTestPatternImages(131, 127, PixelTypes.Rgba32 | PixelTypes.Bgr24)] - public void WhenMemoryIsConsumed(TestImageProvider provider) + [WithBlankImages(16, 16, PixelTypes.Rgba32)] + public void OwnedMemory_DestructiveMutate_ShouldInvalidateMemoryGroup(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image0 = provider.GetImage()) - { - var targetBuffer = new TPixel[image0.Width * image0.Height]; - image0.GetPixelSpan().CopyTo(targetBuffer); + using Image image = provider.GetImage(); - var managerOfExternalMemory = new TestMemoryManager(targetBuffer); + IMemoryGroup memoryGroup = image.GetPixelMemoryGroup(); + Memory memory = memoryGroup.Single(); - Memory externalMemory = managerOfExternalMemory.Memory; + image.Mutate(c => c.Resize(8, 8)); + + Assert.False(memoryGroup.IsValid); + Assert.ThrowsAny(() => _ = memoryGroup.First()); + Assert.ThrowsAny(() => _ = memory.Span); + } + + [Theory] + [WithBasicTestPatternImages(1, 1, PixelTypes.Rgba32)] + [WithBasicTestPatternImages(131, 127, PixelTypes.Bgr24)] + public void ConsumedMemory_PixelDataIsCorrect(TestImageProvider provider) + where TPixel : struct, IPixel + { + using Image image0 = provider.GetImage(); + var targetBuffer = new TPixel[image0.Width * image0.Height]; + image0.GetPixelSpan().CopyTo(targetBuffer); - using (var image1 = Image.WrapMemory(externalMemory, image0.Width, image0.Height)) - { - Memory internalMemory = image1.GetPixelMemory(); - Assert.Equal(targetBuffer.Length, internalMemory.Length); - Assert.True(Unsafe.AreSame(ref targetBuffer[0], ref internalMemory.Span[0])); + var managerOfExternalMemory = new TestMemoryManager(targetBuffer); - image0.ComparePixelBufferTo(internalMemory.Span); - } + Memory externalMemory = managerOfExternalMemory.Memory; - // Make sure externalMemory works after destruction: - image0.ComparePixelBufferTo(externalMemory.Span); + using (var image1 = Image.WrapMemory(externalMemory, image0.Width, image0.Height)) + { + VerifyMemoryGroupDataMatchesTestPattern(provider, image1.GetPixelMemoryGroup(), image1.Size()); } + + // Make sure externalMemory works after destruction: + VerifyMemoryGroupDataMatchesTestPattern(provider, image0.GetPixelMemoryGroup(), image0.Size()); } - } - [Theory] - [WithSolidFilledImages(1, 1, "Red", PixelTypes.Rgba32)] - [WithTestPatternImages(131, 127, PixelTypes.Rgba32 | PixelTypes.Bgr24)] - public void GetPixelRowMemory(TestImageProvider provider) - where TPixel : struct, IPixel - { - using (Image image = provider.GetImage()) + private static void VerifyMemoryGroupDataMatchesTestPattern( + TestImageProvider provider, + IMemoryGroup memoryGroup, + Size size) + where TPixel : struct, IPixel { - var targetBuffer = new TPixel[image.Width * image.Height]; + Assert.True(memoryGroup.IsValid); + Assert.Equal(size.Width * size.Height, memoryGroup.TotalLength); + Assert.True(memoryGroup.BufferLength % size.Width == 0); - // Act: - for (int y = 0; y < image.Height; y++) + int cnt = 0; + for (MemoryGroupIndex i = memoryGroup.MaxIndex(); i < memoryGroup.MaxIndex(); i += 1, cnt++) { - Memory rowMemory = image.GetPixelRowMemory(y); - rowMemory.Span.CopyTo(targetBuffer.AsSpan(image.Width * y)); - } + int y = cnt / size.Width; + int x = cnt % size.Width; - // Assert: - using (Image image1 = provider.GetImage()) - { - // We are using a copy of the original image for assertion - image1.ComparePixelBufferTo(targetBuffer); + TPixel expected = provider.GetExpectedBasicTestPatternPixelAt(x, y); + TPixel actual = memoryGroup.GetElementAt(i); + Assert.Equal(expected, actual); } } } [Theory] - [WithSolidFilledImages(1, 1, "Red", PixelTypes.Rgba32)] - [WithTestPatternImages(131, 127, PixelTypes.Rgba32 | PixelTypes.Bgr24)] - public void GetPixelRowSpan(TestImageProvider provider) + [WithBasicTestPatternImages(1, 1, PixelTypes.Rgba32)] + [WithBasicTestPatternImages(131, 127, PixelTypes.Rgba32)] + [WithBasicTestPatternImages(333, 555, PixelTypes.Bgr24)] + public void GetPixelRowMemory_PixelDataIsCorrect(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage()) - { - var targetBuffer = new TPixel[image.Width * image.Height]; + provider.LimitAllocatorBufferCapacity().InPixelsSqrt(200); + using Image image = provider.GetImage(); + + for (int y = 0; y < image.Height; y++) + { // Act: - for (int y = 0; y < image.Height; y++) - { - Span rowMemory = image.GetPixelRowSpan(y); - rowMemory.CopyTo(targetBuffer.AsSpan(image.Width * y)); - } + Memory rowMemory = image.GetPixelRowMemory(y); + Span span = rowMemory.Span; // Assert: - using (Image image1 = provider.GetImage()) + for (int x = 0; x < image.Width; x++) { - // We are using a copy of the original image for assertion - image1.ComparePixelBufferTo(targetBuffer); + Assert.Equal(provider.GetExpectedBasicTestPatternPixelAt(x, y), span[x]); } } } - #pragma warning disable 0618 + [Theory] + [WithBasicTestPatternImages(16, 16, PixelTypes.Rgba32)] + public void GetPixelRowMemory_DestructiveMutate_ShouldInvalidateMemory(TestImageProvider provider) + where TPixel : struct, IPixel + { + using Image image = provider.GetImage(); + + Memory memory3 = image.GetPixelRowMemory(3); + Memory memory10 = image.GetPixelRowMemory(10); + + image.Mutate(c => c.Resize(8, 8)); + + Assert.ThrowsAny(() => _ = memory3.Span); + Assert.ThrowsAny(() => _ = memory10.Span); + } [Theory] - [WithTestPatternImages(131, 127, PixelTypes.Rgba32 | PixelTypes.Bgr24)] - public unsafe void DangerousGetPinnableReference_CopyToBuffer(TestImageProvider provider) + [WithBlankImages(1, 1, PixelTypes.Rgba32)] + [WithBlankImages(100, 111, PixelTypes.Rgba32)] + [WithBlankImages(400, 600, PixelTypes.Rgba32)] + public void GetPixelRowSpan_ShouldReferenceSpanOfMemory(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage()) - { - var targetBuffer = new TPixel[image.Width * image.Height]; + provider.LimitAllocatorBufferCapacity().InPixelsSqrt(200); - ref byte source = ref Unsafe.As(ref targetBuffer[0]); - ref byte dest = ref Unsafe.As(ref image.DangerousGetPinnableReferenceToPixelBuffer()); - fixed (byte* targetPtr = &source) - fixed (byte* pixelBasePtr = &dest) - { - uint dataSizeInBytes = (uint)(image.Width * image.Height * Unsafe.SizeOf()); - Unsafe.CopyBlock(targetPtr, pixelBasePtr, dataSizeInBytes); - } + using Image image = provider.GetImage(); - image.ComparePixelBufferTo(targetBuffer); - } + Memory memory = image.GetPixelRowMemory(image.Height - 1); + Span span = image.GetPixelRowSpan(image.Height - 1); + + Assert.True(span == memory.Span); } } } diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs index fb3348be7..4705fa3f2 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpDecoderTests.cs @@ -3,8 +3,12 @@ using System; using System.IO; +using Microsoft.DotNet.RemoteExecutor; + using SixLabors.ImageSharp.Formats.Bmp; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; @@ -24,6 +28,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public static readonly string[] BitfieldsBmpFiles = BitFields; + private static BmpDecoder BmpDecoder => new BmpDecoder(); + public static readonly TheoryData RatioFiles = new TheoryData { @@ -33,18 +39,46 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp }; [Theory] - [WithFileCollection(nameof(MiscBmpFiles), PixelTypes.Rgba32)] - public void BmpDecoder_CanDecode_MiscellaneousBitmaps(TestImageProvider provider) + [WithFileCollection(nameof(MiscBmpFiles), PixelTypes.Rgba32, false)] + [WithFileCollection(nameof(MiscBmpFiles), PixelTypes.Rgba32, true)] + public void BmpDecoder_CanDecode_MiscellaneousBitmaps(TestImageProvider provider, bool enforceDiscontiguousBuffers) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new BmpDecoder())) + static void RunTest(string providerDump, string nonContiguousBuffersStr) { - image.DebugSave(provider); + TestImageProvider provider = BasicSerializer.Deserialize>(providerDump); + + if (!string.IsNullOrEmpty(nonContiguousBuffersStr)) + { + provider.LimitAllocatorBufferCapacity().InPixelsSqrt(100); + } + + using Image image = provider.GetImage(BmpDecoder); + image.DebugSave(provider, testOutputDetails: nonContiguousBuffersStr); + if (TestEnvironment.IsWindows) { image.CompareToOriginal(provider); } } + + string providerDump = BasicSerializer.Serialize(provider); + RemoteExecutor.Invoke( + RunTest, + providerDump, + enforceDiscontiguousBuffers ? "Disco" : string.Empty) + .Dispose(); + } + + [Theory] + [WithFile(Bit32Rgb, PixelTypes.Rgba32)] + [WithFile(Bit16, PixelTypes.Rgba32)] + public void BmpDecoder_DegenerateMemoryRequest_ShouldTranslateTo_ImageFormatException(TestImageProvider provider) + where TPixel : struct, IPixel + { + provider.LimitAllocatorBufferCapacity().InPixelsSqrt(10); + ImageFormatException ex = Assert.Throws(() => provider.GetImage(BmpDecoder)); + Assert.IsType(ex.InnerException); } [Theory] @@ -52,7 +86,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public void BmpDecoder_CanDecodeBitfields(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new BmpDecoder())) + using (Image image = provider.GetImage(BmpDecoder)) { image.DebugSave(provider); image.CompareToOriginal(provider); @@ -65,7 +99,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public void BmpDecoder_CanDecode_Inverted(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new BmpDecoder())) + using (Image image = provider.GetImage(BmpDecoder)) { image.DebugSave(provider); image.CompareToOriginal(provider); @@ -78,7 +112,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public void BmpDecoder_CanDecode_1Bit(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new BmpDecoder())) + using (Image image = provider.GetImage(BmpDecoder)) { image.DebugSave(provider); image.CompareToOriginal(provider, new SystemDrawingReferenceDecoder()); @@ -90,7 +124,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public void BmpDecoder_CanDecode_4Bit(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new BmpDecoder())) + using (Image image = provider.GetImage(BmpDecoder)) { image.DebugSave(provider); @@ -107,7 +141,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public void BmpDecoder_CanDecode_8Bit(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new BmpDecoder())) + using (Image image = provider.GetImage(BmpDecoder)) { image.DebugSave(provider); image.CompareToOriginal(provider); @@ -119,7 +153,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public void BmpDecoder_CanDecode_16Bit(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new BmpDecoder())) + using (Image image = provider.GetImage(BmpDecoder)) { image.DebugSave(provider); image.CompareToOriginal(provider); @@ -131,7 +165,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public void BmpDecoder_CanDecode_32Bit(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new BmpDecoder())) + using (Image image = provider.GetImage(BmpDecoder)) { image.DebugSave(provider); image.CompareToOriginal(provider); @@ -143,7 +177,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public void BmpDecoder_CanDecode_32BitV4Header_Fast(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new BmpDecoder())) + using (Image image = provider.GetImage(BmpDecoder)) { image.DebugSave(provider); image.CompareToOriginal(provider); @@ -218,11 +252,18 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp } [Theory] - [WithFile(RLE8, PixelTypes.Rgba32)] - [WithFile(RLE8Inverted, PixelTypes.Rgba32)] - public void BmpDecoder_CanDecode_RunLengthEncoded_8Bit(TestImageProvider provider) + [WithFile(RLE8, PixelTypes.Rgba32, false)] + [WithFile(RLE8Inverted, PixelTypes.Rgba32, false)] + [WithFile(RLE8, PixelTypes.Rgba32, true)] + [WithFile(RLE8Inverted, PixelTypes.Rgba32, true)] + public void BmpDecoder_CanDecode_RunLengthEncoded_8Bit(TestImageProvider provider, bool enforceDiscontiguousBuffers) where TPixel : struct, IPixel { + if (enforceDiscontiguousBuffers) + { + provider.LimitAllocatorBufferCapacity().InBytesSqrt(400); + } + using (Image image = provider.GetImage(new BmpDecoder { RleSkippedPixelHandling = RleSkippedPixelHandling.FirstColorOfPalette })) { image.DebugSave(provider); @@ -231,12 +272,20 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp } [Theory] - [WithFile(RLE24, PixelTypes.Rgba32)] - [WithFile(RLE24Cut, PixelTypes.Rgba32)] - [WithFile(RLE24Delta, PixelTypes.Rgba32)] - public void BmpDecoder_CanDecode_RunLengthEncoded_24Bit(TestImageProvider provider) + [WithFile(RLE24, PixelTypes.Rgba32, false)] + [WithFile(RLE24Cut, PixelTypes.Rgba32, false)] + [WithFile(RLE24Delta, PixelTypes.Rgba32, false)] + [WithFile(RLE24, PixelTypes.Rgba32, true)] + [WithFile(RLE24Cut, PixelTypes.Rgba32, true)] + [WithFile(RLE24Delta, PixelTypes.Rgba32, true)] + public void BmpDecoder_CanDecode_RunLengthEncoded_24Bit(TestImageProvider provider, bool enforceNonContiguous) where TPixel : struct, IPixel { + if (enforceNonContiguous) + { + provider.LimitAllocatorBufferCapacity().InBytesSqrt(400); + } + using (Image image = provider.GetImage(new BmpDecoder { RleSkippedPixelHandling = RleSkippedPixelHandling.Black })) { image.DebugSave(provider); @@ -251,7 +300,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public void BmpDecoder_CanDecodeAlphaBitfields(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new BmpDecoder())) + using (Image image = provider.GetImage(BmpDecoder)) { image.DebugSave(provider); @@ -265,7 +314,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public void BmpDecoder_CanDecodeBitmap_WithAlphaChannel(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new BmpDecoder())) + using (Image image = provider.GetImage(BmpDecoder)) { image.DebugSave(provider); image.CompareToOriginal(provider, new MagickReferenceDecoder()); @@ -277,7 +326,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public void BmpDecoder_CanDecodeBitfields_WithUnusualBitmasks(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new BmpDecoder())) + using (Image image = provider.GetImage(BmpDecoder)) { image.DebugSave(provider); @@ -296,7 +345,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public void BmpDecoder_CanDecodeBmpv2(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new BmpDecoder())) + using (Image image = provider.GetImage(BmpDecoder)) { image.DebugSave(provider); image.CompareToOriginal(provider); @@ -308,7 +357,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public void BmpDecoder_CanDecodeBmpv3(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new BmpDecoder())) + using (Image image = provider.GetImage(BmpDecoder)) { image.DebugSave(provider); image.CompareToOriginal(provider); @@ -320,7 +369,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public void BmpDecoder_CanDecodeLessThanFullPalette(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new BmpDecoder())) + using (Image image = provider.GetImage(BmpDecoder)) { image.DebugSave(provider); image.CompareToOriginal(provider, new MagickReferenceDecoder()); @@ -333,7 +382,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public void BmpDecoder_CanDecodeOversizedPalette(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new BmpDecoder())) + using (Image image = provider.GetImage(BmpDecoder)) { image.DebugSave(provider); if (TestEnvironment.IsWindows) @@ -350,7 +399,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp { Assert.Throws(() => { - using (provider.GetImage(new BmpDecoder())) + using (provider.GetImage(BmpDecoder)) { } }); @@ -364,7 +413,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp { Assert.Throws(() => { - using (provider.GetImage(new BmpDecoder())) + using (provider.GetImage(BmpDecoder)) { } }); @@ -375,7 +424,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public void BmpDecoder_CanDecodeAdobeBmpv3(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new BmpDecoder())) + using (Image image = provider.GetImage(BmpDecoder)) { image.DebugSave(provider); image.CompareToOriginal(provider, new MagickReferenceDecoder()); @@ -387,7 +436,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public void BmpDecoder_CanDecodeAdobeBmpv3_WithAlpha(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new BmpDecoder())) + using (Image image = provider.GetImage(BmpDecoder)) { image.DebugSave(provider); image.CompareToOriginal(provider, new MagickReferenceDecoder()); @@ -399,7 +448,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public void BmpDecoder_CanDecodeBmpv4(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new BmpDecoder())) + using (Image image = provider.GetImage(BmpDecoder)) { image.DebugSave(provider); image.CompareToOriginal(provider); @@ -412,7 +461,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public void BmpDecoder_CanDecodeBmpv5(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new BmpDecoder())) + using (Image image = provider.GetImage(BmpDecoder)) { image.DebugSave(provider); image.CompareToOriginal(provider); @@ -424,7 +473,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public void BmpDecoder_RespectsFileHeaderOffset(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new BmpDecoder())) + using (Image image = provider.GetImage(BmpDecoder)) { image.DebugSave(provider); image.CompareToOriginal(provider); @@ -436,7 +485,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public void BmpDecoder_IsNotBoundToSinglePixelType(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new BmpDecoder())) + using (Image image = provider.GetImage(BmpDecoder)) { image.DebugSave(provider); image.CompareToOriginal(provider); @@ -448,7 +497,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public void BmpDecoder_CanDecode4BytePerEntryPalette(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new BmpDecoder())) + using (Image image = provider.GetImage(BmpDecoder)) { image.DebugSave(provider); image.CompareToOriginal(provider); @@ -523,7 +572,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public void BmpDecoder_CanDecode_Os2v2XShortHeader(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new BmpDecoder())) + using (Image image = provider.GetImage(BmpDecoder)) { image.DebugSave(provider); @@ -537,7 +586,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public void BmpDecoder_CanDecode_Os2v2Header(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new BmpDecoder())) + using (Image image = provider.GetImage(BmpDecoder)) { image.DebugSave(provider); @@ -561,7 +610,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public void BmpDecoder_CanDecode_Os2BitmapArray(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new BmpDecoder())) + using (Image image = provider.GetImage(BmpDecoder)) { image.DebugSave(provider); diff --git a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs index 10be33a97..85d5ca1b8 100644 --- a/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Bmp/BmpEncoderTests.cs @@ -240,6 +240,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Bmp public void Encode_PreservesAlpha(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) where TPixel : struct, IPixel => TestBmpEncoderCore(provider, bitsPerPixel, supportTransparency: true); + [Theory] + [WithFile(Car, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel32)] + [WithFile(V5Header, PixelTypes.Rgba32, BmpBitsPerPixel.Pixel32)] + public void Encode_WorksWithDiscontiguousBuffers(TestImageProvider provider, BmpBitsPerPixel bitsPerPixel) + where TPixel : struct, IPixel + { + provider.LimitAllocatorBufferCapacity().InBytesSqrt(100); + TestBmpEncoderCore(provider, bitsPerPixel); + } + private static void TestBmpEncoderCore( TestImageProvider provider, BmpBitsPerPixel bitsPerPixel, diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs index 99dc2d06d..45c768892 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs @@ -4,11 +4,16 @@ using System; using System.Collections.Generic; using System.IO; +using Microsoft.DotNet.RemoteExecutor; + using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats.Gif; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; + using Xunit; // ReSharper disable InconsistentNaming @@ -18,6 +23,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif { private const PixelTypes TestPixelTypes = PixelTypes.Rgba32 | PixelTypes.RgbaVector | PixelTypes.Argb32; + private static GifDecoder GifDecoder => new GifDecoder(); + public static readonly string[] MultiFrameTestFiles = { TestImages.Gif.Giphy, TestImages.Gif.Kumin @@ -72,9 +79,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif { using (var stream = new UnmanagedMemoryStream(data, length)) { - var decoder = new GifDecoder(); - - using (Image image = decoder.Decode(Configuration.Default, stream)) + using (Image image = GifDecoder.Decode(Configuration.Default, stream)) { Assert.Equal((200, 200), (image.Width, image.Height)); } @@ -163,5 +168,41 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif } } } + + [Theory] + [WithFile(TestImages.Gif.Giphy, PixelTypes.Rgba32)] + [WithFile(TestImages.Gif.Kumin, PixelTypes.Rgba32)] + public void GifDecoder_DegenerateMemoryRequest_ShouldTranslateTo_ImageFormatException(TestImageProvider provider) + where TPixel : struct, IPixel + { + provider.LimitAllocatorBufferCapacity().InPixelsSqrt(10); + ImageFormatException ex = Assert.Throws(() => provider.GetImage(GifDecoder)); + Assert.IsType(ex.InnerException); + } + + [Theory] + [WithFile(TestImages.Gif.Giphy, PixelTypes.Rgba32)] + [WithFile(TestImages.Gif.Kumin, PixelTypes.Rgba32)] + public void GifDecoder_CanDecode_WithLimitedAllocatorBufferCapacity(TestImageProvider provider) + where TPixel : struct, IPixel + { + static void RunTest(string providerDump, string nonContiguousBuffersStr) + { + TestImageProvider provider = BasicSerializer.Deserialize>(providerDump); + + provider.LimitAllocatorBufferCapacity().InPixelsSqrt(100); + + using Image image = provider.GetImage(GifDecoder); + image.DebugSave(provider); + image.CompareToOriginal(provider); + } + + string providerDump = BasicSerializer.Serialize(provider); + RemoteExecutor.Invoke( + RunTest, + providerDump, + "Disco") + .Dispose(); + } } } diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index ea1eb700a..1519bc801 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -26,10 +26,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Gif }; [Theory] - [WithTestPatternImages(100, 100, TestPixelTypes)] - public void EncodeGeneratedPatterns(TestImageProvider provider) + [WithTestPatternImages(100, 100, TestPixelTypes, false)] + [WithTestPatternImages(100, 100, TestPixelTypes, false)] + public void EncodeGeneratedPatterns(TestImageProvider provider, bool limitAllocationBuffer) where TPixel : struct, IPixel { + if (limitAllocationBuffer) + { + provider.LimitAllocatorBufferCapacity().InPixelsSqrt(100); + } + using (Image image = provider.GetImage()) { var encoder = new GifEncoder diff --git a/tests/ImageSharp.Tests/Formats/Jpg/GenericBlock8x8Tests.cs b/tests/ImageSharp.Tests/Formats/Jpg/GenericBlock8x8Tests.cs index 7c42af596..38b33e842 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/GenericBlock8x8Tests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/GenericBlock8x8Tests.cs @@ -41,7 +41,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg using (Image s = provider.GetImage()) { var d = default(GenericBlock8x8); - d.LoadAndStretchEdges(s.Frames.RootFrame, 0, 0); + var rowOctet = new RowOctet(s.GetRootFramePixelBuffer(), 0); + d.LoadAndStretchEdges(s.Frames.RootFrame.PixelBuffer, 0, 0, rowOctet); TPixel a = s.Frames.RootFrame[0, 0]; TPixel b = d[0, 0]; @@ -65,7 +66,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg using (Image s = provider.GetImage()) { var d = default(GenericBlock8x8); - d.LoadAndStretchEdges(s.Frames.RootFrame, 6, 7); + var rowOctet = new RowOctet(s.GetRootFramePixelBuffer(), 7); + d.LoadAndStretchEdges(s.Frames.RootFrame.PixelBuffer, 6, 7, rowOctet); Assert.Equal(s[6, 7], d[0, 0]); Assert.Equal(s[6, 8], d[0, 1]); diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs index 146b07d05..877571425 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegColorConverterTests.cs @@ -292,7 +292,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg // no need to dispose when buffer is not array owner var memory = new Memory(values); - var source = new MemorySource(memory); + var source = MemoryGroup.Wrap(memory); buffers[i] = new Buffer2D(source, values.Length, 1); } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Baseline.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Baseline.cs index 628f59a9a..6ea61892c 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Baseline.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Baseline.cs @@ -2,8 +2,10 @@ // Licensed under the Apache License, Version 2.0. using Microsoft.DotNet.RemoteExecutor; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.TestUtilities; +using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using Xunit; // ReSharper disable InconsistentNaming @@ -12,17 +14,24 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg public partial class JpegDecoderTests { [Theory] - [WithFileCollection(nameof(BaselineTestJpegs), PixelTypes.Rgba32)] - public void DecodeBaselineJpeg(TestImageProvider provider) + [WithFileCollection(nameof(BaselineTestJpegs), PixelTypes.Rgba32, false)] + [WithFile(TestImages.Jpeg.Baseline.Calliphora, PixelTypes.Rgba32, true)] + [WithFile(TestImages.Jpeg.Baseline.Turtle420, PixelTypes.Rgba32, true)] + public void DecodeBaselineJpeg(TestImageProvider provider, bool enforceDiscontiguousBuffers) where TPixel : struct, IPixel { - static void RunTest(string providerDump) + static void RunTest(string providerDump, string nonContiguousBuffersStr) { TestImageProvider provider = BasicSerializer.Deserialize>(providerDump); + if (!string.IsNullOrEmpty(nonContiguousBuffersStr)) + { + provider.LimitAllocatorBufferCapacity().InPixels(1000 * 8); + } + using Image image = provider.GetImage(JpegDecoder); - image.DebugSave(provider); + image.DebugSave(provider, testOutputDetails: nonContiguousBuffersStr); provider.Utility.TestName = DecodeBaselineJpegOutputName; image.CompareToReferenceOutput( @@ -32,12 +41,18 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg } string providerDump = BasicSerializer.Serialize(provider); - RemoteExecutor.Invoke(RunTest, providerDump).Dispose(); + RunTest(providerDump, enforceDiscontiguousBuffers ? "Disco" : string.Empty); + + // RemoteExecutor.Invoke( + // RunTest, + // providerDump, + // enforceDiscontiguousBuffers ? "Disco" : string.Empty) + // .Dispose(); } [Theory] [WithFileCollection(nameof(UnrecoverableTestJpegs), PixelTypes.Rgba32)] - public void UnrecoverableImagesShouldThrowCorrectError(TestImageProvider provider) + public void UnrecoverableImage_Throws_ImageFormatException(TestImageProvider provider) where TPixel : struct, IPixel => Assert.Throws(provider.GetImage); } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs index b7d7e6b83..a01f4d46c 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Images.cs @@ -13,10 +13,11 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg TestImages.Jpeg.Baseline.Cmyk, TestImages.Jpeg.Baseline.Ycck, TestImages.Jpeg.Baseline.Jpeg400, + TestImages.Jpeg.Baseline.Turtle420, TestImages.Jpeg.Baseline.Testorig420, // BUG: The following image has a high difference compared to the expected output: 1.0096% - // TestImages.Jpeg.Baseline.Jpeg420Small, + TestImages.Jpeg.Baseline.Jpeg420Small, TestImages.Jpeg.Issues.Fuzz.AccessViolationException922, TestImages.Jpeg.Baseline.Jpeg444, TestImages.Jpeg.Baseline.Bad.BadEOF, @@ -95,9 +96,12 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg // Baseline: [TestImages.Jpeg.Baseline.Calliphora] = 0.00002f / 100, [TestImages.Jpeg.Baseline.Bad.BadEOF] = 0.38f / 100, - [TestImages.Jpeg.Baseline.Testorig420] = 0.38f / 100, [TestImages.Jpeg.Baseline.Bad.BadRST] = 0.0589f / 100, + [TestImages.Jpeg.Baseline.Testorig420] = 0.38f / 100, + [TestImages.Jpeg.Baseline.Jpeg420Small] = 1.1f / 100, + [TestImages.Jpeg.Baseline.Turtle420] = 1.0f / 100, + // Progressive: [TestImages.Jpeg.Issues.MissingFF00ProgressiveGirl159] = 0.34f / 100, [TestImages.Jpeg.Issues.BadCoeffsProgressive178] = 0.38f / 100, diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Progressive.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Progressive.cs index 886cef7ac..b8a791278 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Progressive.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.Progressive.cs @@ -14,17 +14,23 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg public const string DecodeProgressiveJpegOutputName = "DecodeProgressiveJpeg"; [Theory] - [WithFileCollection(nameof(ProgressiveTestJpegs), PixelTypes.Rgba32)] - public void DecodeProgressiveJpeg(TestImageProvider provider) + [WithFileCollection(nameof(ProgressiveTestJpegs), PixelTypes.Rgba32, false)] + [WithFile(TestImages.Jpeg.Progressive.Progress, PixelTypes.Rgba32, true)] + public void DecodeProgressiveJpeg(TestImageProvider provider, bool enforceDiscontiguousBuffers) where TPixel : struct, IPixel { - static void RunTest(string providerDump) + static void RunTest(string providerDump, string nonContiguousBuffersStr) { TestImageProvider provider = BasicSerializer.Deserialize>(providerDump); + if (!string.IsNullOrEmpty(nonContiguousBuffersStr)) + { + provider.LimitAllocatorBufferCapacity().InBytesSqrt(200); + } + using Image image = provider.GetImage(JpegDecoder); - image.DebugSave(provider); + image.DebugSave(provider, nonContiguousBuffersStr); provider.Utility.TestName = DecodeProgressiveJpegOutputName; image.CompareToReferenceOutput( @@ -33,8 +39,13 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg appendPixelTypeToFileName: false); } - string dump = BasicSerializer.Serialize(provider); - RemoteExecutor.Invoke(RunTest, dump).Dispose(); + string providerDump = BasicSerializer.Serialize(provider); + + RemoteExecutor.Invoke( + RunTest, + providerDump, + enforceDiscontiguousBuffers ? "Disco" : string.Empty) + .Dispose(); } } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs index 09e98b5c4..32060df9a 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegDecoderTests.cs @@ -95,19 +95,26 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg public void JpegDecoder_IsNotBoundToSinglePixelType(TestImageProvider provider) where TPixel : struct, IPixel { - static void RunTest(string providerDump) - { - TestImageProvider provider = - BasicSerializer.Deserialize>(providerDump); - using Image image = provider.GetImage(JpegDecoder); - image.DebugSave(provider); - - provider.Utility.TestName = DecodeBaselineJpegOutputName; - image.CompareToReferenceOutput(ImageComparer.Tolerant(BaselineTolerance), provider, appendPixelTypeToFileName: false); - } + using Image image = provider.GetImage(JpegDecoder); + image.DebugSave(provider); + + provider.Utility.TestName = DecodeBaselineJpegOutputName; + image.CompareToReferenceOutput( + ImageComparer.Tolerant(BaselineTolerance), + provider, + appendPixelTypeToFileName: false); + } - string dump = BasicSerializer.Serialize(provider); - RemoteExecutor.Invoke(RunTest, dump).Dispose(); + [Theory] + [WithFile(TestImages.Jpeg.Baseline.Floorplan, PixelTypes.Rgba32)] + [WithFile(TestImages.Jpeg.Progressive.Festzug, PixelTypes.Rgba32)] + public void DegenerateMemoryRequest_ShouldTranslateTo_ImageFormatException(TestImageProvider provider) + where TPixel : struct, IPixel + { + provider.LimitAllocatorBufferCapacity().InBytesSqrt(10); + ImageFormatException ex = Assert.Throws(() => provider.GetImage(JpegDecoder)); + this.Output.WriteLine(ex.Message); + Assert.IsType(ex.InnerException); } // DEBUG ONLY! diff --git a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs index f7acb9fca..b62a555b8 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/JpegEncoderTests.cs @@ -3,6 +3,7 @@ using System.IO; using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; @@ -15,30 +16,30 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg public class JpegEncoderTests { public static readonly TheoryData QualityFiles = - new TheoryData - { - { TestImages.Jpeg.Baseline.Calliphora, 80 }, - { TestImages.Jpeg.Progressive.Fb, 75 } - }; + new TheoryData + { + { TestImages.Jpeg.Baseline.Calliphora, 80 }, + { TestImages.Jpeg.Progressive.Fb, 75 } + }; public static readonly TheoryData BitsPerPixel_Quality = - new TheoryData - { - { JpegSubsample.Ratio420, 40 }, - { JpegSubsample.Ratio420, 60 }, - { JpegSubsample.Ratio420, 100 }, - { JpegSubsample.Ratio444, 40 }, - { JpegSubsample.Ratio444, 60 }, - { JpegSubsample.Ratio444, 100 }, - }; + new TheoryData + { + { JpegSubsample.Ratio420, 40 }, + { JpegSubsample.Ratio420, 60 }, + { JpegSubsample.Ratio420, 100 }, + { JpegSubsample.Ratio444, 40 }, + { JpegSubsample.Ratio444, 60 }, + { JpegSubsample.Ratio444, 100 }, + }; public static readonly TheoryData RatioFiles = - new TheoryData - { - { TestImages.Jpeg.Baseline.Ratio1x1, 1, 1, PixelResolutionUnit.AspectRatio }, - { TestImages.Jpeg.Baseline.Snake, 300, 300, PixelResolutionUnit.PixelsPerInch }, - { TestImages.Jpeg.Baseline.GammaDalaiLamaGray, 72, 72, PixelResolutionUnit.PixelsPerInch } - }; + new TheoryData + { + { TestImages.Jpeg.Baseline.Ratio1x1, 1, 1, PixelResolutionUnit.AspectRatio }, + { TestImages.Jpeg.Baseline.Snake, 300, 300, PixelResolutionUnit.PixelsPerInch }, + { TestImages.Jpeg.Baseline.GammaDalaiLamaGray, 72, 72, PixelResolutionUnit.PixelsPerInch } + }; [Theory] [MemberData(nameof(QualityFiles))] @@ -71,6 +72,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg [WithTestPatternImages(nameof(BitsPerPixel_Quality), 51, 7, PixelTypes.Rgba32)] [WithSolidFilledImages(nameof(BitsPerPixel_Quality), 1, 1, 255, 100, 50, 255, PixelTypes.Rgba32)] [WithTestPatternImages(nameof(BitsPerPixel_Quality), 7, 5, PixelTypes.Rgba32)] + [WithTestPatternImages(nameof(BitsPerPixel_Quality), 600, 400, PixelTypes.Rgba32)] public void EncodeBaseline_WorksWithDifferentSizes(TestImageProvider provider, JpegSubsample subsample, int quality) where TPixel : struct, IPixel => TestJpegEncoderCore(provider, subsample, quality); @@ -79,6 +81,22 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg public void EncodeBaseline_IsNotBoundToSinglePixelType(TestImageProvider provider, JpegSubsample subsample, int quality) where TPixel : struct, IPixel => TestJpegEncoderCore(provider, subsample, quality); + [Theory] + [WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Rgba32, JpegSubsample.Ratio444)] + [WithTestPatternImages(587, 821, PixelTypes.Rgba32, JpegSubsample.Ratio444)] + [WithTestPatternImages(677, 683, PixelTypes.Bgra32, JpegSubsample.Ratio420)] + [WithSolidFilledImages(400, 400, "Red", PixelTypes.Bgr24, JpegSubsample.Ratio420)] + public void EncodeBaseline_WorksWithDiscontiguousBuffers(TestImageProvider provider, JpegSubsample subsample) + where TPixel : struct, IPixel + { + ImageComparer comparer = subsample == JpegSubsample.Ratio444 + ? ImageComparer.TolerantPercentage(0.1f) + : ImageComparer.TolerantPercentage(5f); + + provider.LimitAllocatorBufferCapacity().InBytesSqrt(200); + TestJpegEncoderCore(provider, subsample, 100, comparer); + } + /// /// Anton's SUPER-SCIENTIFIC tolerance threshold calculation /// @@ -105,25 +123,26 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg private static void TestJpegEncoderCore( TestImageProvider provider, JpegSubsample subsample, - int quality = 100) + int quality = 100, + ImageComparer comparer = null) where TPixel : struct, IPixel { - using (Image image = provider.GetImage()) + using Image image = provider.GetImage(); + + // There is no alpha in Jpeg! + image.Mutate(c => c.MakeOpaque()); + + var encoder = new JpegEncoder { - // There is no alpha in Jpeg! - image.Mutate(c => c.MakeOpaque()); + Subsample = subsample, + Quality = quality + }; + string info = $"{subsample}-Q{quality}"; - var encoder = new JpegEncoder - { - Subsample = subsample, - Quality = quality - }; - string info = $"{subsample}-Q{quality}"; - ImageComparer comparer = GetComparer(quality, subsample); - - // Does DebugSave & load reference CompareToReferenceInput(): - image.VerifyEncoder(provider, "jpeg", info, encoder, comparer, referenceImageExtension: "png"); - } + comparer ??= GetComparer(quality, subsample); + + // Does DebugSave & load reference CompareToReferenceInput(): + image.VerifyEncoder(provider, "jpeg", info, encoder, comparer, referenceImageExtension: "png"); } [Fact] diff --git a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs index 8d7dda2fe..c69740ede 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.Formats.Jpg.Utils; @@ -113,7 +114,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Jpg this.Output.WriteLine($"Component{i}: {diff}"); averageDifference += diff.average; totalDifference += diff.total; - tolerance += libJpegComponent.SpectralBlocks.MemorySource.GetSpan().Length; + tolerance += libJpegComponent.SpectralBlocks.GetSingleSpan().Length; } averageDifference /= componentCount; diff --git a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs index a88962e5f..bf767e811 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngDecoderTests.cs @@ -2,11 +2,16 @@ // Licensed under the Apache License, Version 2.0. using System.IO; +using Microsoft.DotNet.RemoteExecutor; + using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.TestUtilities; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs; + using Xunit; // ReSharper disable InconsistentNaming @@ -16,6 +21,8 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png { private const PixelTypes PixelTypes = Tests.PixelTypes.Rgba32 | Tests.PixelTypes.RgbaVector | Tests.PixelTypes.Argb32; + private static PngDecoder PngDecoder => new PngDecoder(); + public static readonly string[] CommonTestImages = { TestImages.Png.Splash, @@ -87,7 +94,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png public void Decode(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new PngDecoder())) + using (Image image = provider.GetImage(PngDecoder)) { image.DebugSave(provider); @@ -111,7 +118,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png public void Decode_Interlaced_ImageIsCorrect(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new PngDecoder())) + using (Image image = provider.GetImage(PngDecoder)) { image.DebugSave(provider); image.CompareToOriginal(provider, ImageComparer.Exact); @@ -123,7 +130,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png public void Decode_48Bpp(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new PngDecoder())) + using (Image image = provider.GetImage(PngDecoder)) { image.DebugSave(provider); image.CompareToOriginal(provider, ImageComparer.Exact); @@ -135,7 +142,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png public void Decode_64Bpp(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new PngDecoder())) + using (Image image = provider.GetImage(PngDecoder)) { image.DebugSave(provider); image.CompareToOriginal(provider, ImageComparer.Exact); @@ -147,7 +154,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png public void Decoder_L8bitInterlaced(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new PngDecoder())) + using (Image image = provider.GetImage(PngDecoder)) { image.DebugSave(provider); image.CompareToOriginal(provider, ImageComparer.Exact); @@ -159,7 +166,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png public void Decode_L16Bit(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new PngDecoder())) + using (Image image = provider.GetImage(PngDecoder)) { image.DebugSave(provider); image.CompareToOriginal(provider, ImageComparer.Exact); @@ -171,7 +178,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png public void Decode_GrayAlpha16Bit(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new PngDecoder())) + using (Image image = provider.GetImage(PngDecoder)) { image.DebugSave(provider); image.CompareToOriginal(provider, ImageComparer.Exact); @@ -183,7 +190,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png public void Decoder_CanDecodeGrey8bitWithAlpha(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new PngDecoder())) + using (Image image = provider.GetImage(PngDecoder)) { image.DebugSave(provider); image.CompareToOriginal(provider, ImageComparer.Exact); @@ -195,7 +202,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png public void Decoder_IsNotBoundToSinglePixelType(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new PngDecoder())) + using (Image image = provider.GetImage(PngDecoder)) { image.DebugSave(provider); image.CompareToOriginal(provider, ImageComparer.Exact); @@ -227,7 +234,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png System.Exception ex = Record.Exception( () => { - using (Image image = provider.GetImage(new PngDecoder())) + using (Image image = provider.GetImage(PngDecoder)) { image.DebugSave(provider); image.CompareToOriginal(provider, ImageComparer.Exact); @@ -235,5 +242,41 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png }); Assert.Null(ex); } + + [Theory] + [WithFile(TestImages.Png.Splash, PixelTypes.Rgba32)] + [WithFile(TestImages.Png.Bike, PixelTypes.Rgba32)] + public void PngDecoder_DegenerateMemoryRequest_ShouldTranslateTo_ImageFormatException(TestImageProvider provider) + where TPixel : struct, IPixel + { + provider.LimitAllocatorBufferCapacity().InPixelsSqrt(10); + ImageFormatException ex = Assert.Throws(() => provider.GetImage(PngDecoder)); + Assert.IsType(ex.InnerException); + } + + [Theory] + [WithFile(TestImages.Png.Splash, PixelTypes.Rgba32)] + [WithFile(TestImages.Png.Bike, PixelTypes.Rgba32)] + public void PngDecoder_CanDecode_WithLimitedAllocatorBufferCapacity(TestImageProvider provider) + where TPixel : struct, IPixel + { + static void RunTest(string providerDump, string nonContiguousBuffersStr) + { + TestImageProvider provider = BasicSerializer.Deserialize>(providerDump); + + provider.LimitAllocatorBufferCapacity().InPixelsSqrt(100); + + using Image image = provider.GetImage(PngDecoder); + image.DebugSave(provider, testOutputDetails: nonContiguousBuffersStr); + image.CompareToOriginal(provider); + } + + string providerDump = BasicSerializer.Serialize(provider); + RemoteExecutor.Invoke( + RunTest, + providerDump, + "Disco") + .Dispose(); + } } } diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index 2fa1657e6..390613256 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -404,6 +404,26 @@ namespace SixLabors.ImageSharp.Tests.Formats.Png } } + [Theory] + [WithTestPatternImages(587, 821, PixelTypes.Rgba32)] + [WithTestPatternImages(677, 683, PixelTypes.Rgba32)] + public void Encode_WorksWithDiscontiguousBuffers(TestImageProvider provider) + where TPixel : struct, IPixel + { + provider.LimitAllocatorBufferCapacity().InPixelsSqrt(200); + foreach (PngInterlaceMode interlaceMode in InterlaceMode) + { + TestPngEncoderCore( + provider, + PngColorType.Rgb, + PngFilterMethod.Adaptive, + PngBitDepth.Bit8, + interlaceMode, + appendPixelType: true, + appendPngColorType: true); + } + } + private static void TestPngEncoderCore( TestImageProvider provider, PngColorType pngColorType, diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs index 1f8cbd6a9..985ccb596 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaDecoderTests.cs @@ -1,9 +1,12 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using Microsoft.DotNet.RemoteExecutor; + using SixLabors.ImageSharp.Formats.Tga; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; - +using SixLabors.ImageSharp.Tests.TestUtilities; using Xunit; // ReSharper disable InconsistentNaming @@ -13,12 +16,14 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga public class TgaDecoderTests { + private static TgaDecoder TgaDecoder => new TgaDecoder(); + [Theory] [WithFile(Grey, PixelTypes.Rgba32)] public void TgaDecoder_CanDecode_Uncompressed_MonoChrome(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new TgaDecoder())) + using (Image image = provider.GetImage(TgaDecoder)) { image.DebugSave(provider); TgaTestUtils.CompareWithReferenceDecoder(provider, image); @@ -30,7 +35,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga public void TgaDecoder_CanDecode_Uncompressed_15Bit(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new TgaDecoder())) + using (Image image = provider.GetImage(TgaDecoder)) { image.DebugSave(provider); TgaTestUtils.CompareWithReferenceDecoder(provider, image); @@ -42,7 +47,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga public void TgaDecoder_CanDecode_RunLengthEncoded_15Bit(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new TgaDecoder())) + using (Image image = provider.GetImage(TgaDecoder)) { image.DebugSave(provider); TgaTestUtils.CompareWithReferenceDecoder(provider, image); @@ -54,7 +59,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga public void TgaDecoder_CanDecode_Uncompressed_16Bit(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new TgaDecoder())) + using (Image image = provider.GetImage(TgaDecoder)) { image.DebugSave(provider); TgaTestUtils.CompareWithReferenceDecoder(provider, image); @@ -66,7 +71,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga public void TgaDecoder_CanDecode_RunLengthEncoded_WithPalette_16Bit(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new TgaDecoder())) + using (Image image = provider.GetImage(TgaDecoder)) { image.DebugSave(provider); TgaTestUtils.CompareWithReferenceDecoder(provider, image); @@ -78,7 +83,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga public void TgaDecoder_CanDecode_Uncompressed_24Bit(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new TgaDecoder())) + using (Image image = provider.GetImage(TgaDecoder)) { image.DebugSave(provider); TgaTestUtils.CompareWithReferenceDecoder(provider, image); @@ -90,7 +95,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga public void TgaDecoder_CanDecode_RunLengthEncoded_WithTopLeftOrigin_24Bit(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new TgaDecoder())) + using (Image image = provider.GetImage(TgaDecoder)) { image.DebugSave(provider); TgaTestUtils.CompareWithReferenceDecoder(provider, image); @@ -102,7 +107,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga public void TgaDecoder_CanDecode_Palette_WithTopLeftOrigin_24Bit(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new TgaDecoder())) + using (Image image = provider.GetImage(TgaDecoder)) { image.DebugSave(provider); TgaTestUtils.CompareWithReferenceDecoder(provider, image); @@ -114,7 +119,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga public void TgaDecoder_CanDecode_Uncompressed_32Bit(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new TgaDecoder())) + using (Image image = provider.GetImage(TgaDecoder)) { image.DebugSave(provider); TgaTestUtils.CompareWithReferenceDecoder(provider, image); @@ -126,7 +131,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga public void TgaDecoder_CanDecode_RunLengthEncoded_MonoChrome(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new TgaDecoder())) + using (Image image = provider.GetImage(TgaDecoder)) { image.DebugSave(provider); TgaTestUtils.CompareWithReferenceDecoder(provider, image); @@ -138,7 +143,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga public void TgaDecoder_CanDecode_RunLengthEncoded_16Bit(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new TgaDecoder())) + using (Image image = provider.GetImage(TgaDecoder)) { image.DebugSave(provider); TgaTestUtils.CompareWithReferenceDecoder(provider, image); @@ -150,7 +155,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga public void TgaDecoder_CanDecode_RunLengthEncoded_24Bit(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new TgaDecoder())) + using (Image image = provider.GetImage(TgaDecoder)) { image.DebugSave(provider); TgaTestUtils.CompareWithReferenceDecoder(provider, image); @@ -162,7 +167,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga public void TgaDecoder_CanDecode_RunLengthEncoded_32Bit(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new TgaDecoder())) + using (Image image = provider.GetImage(TgaDecoder)) { image.DebugSave(provider); TgaTestUtils.CompareWithReferenceDecoder(provider, image); @@ -174,7 +179,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga public void TgaDecoder_CanDecode_WithPalette_16Bit(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new TgaDecoder())) + using (Image image = provider.GetImage(TgaDecoder)) { image.DebugSave(provider); TgaTestUtils.CompareWithReferenceDecoder(provider, image); @@ -186,11 +191,52 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga public void TgaDecoder_CanDecode_WithPalette_24Bit(TestImageProvider provider) where TPixel : struct, IPixel { - using (Image image = provider.GetImage(new TgaDecoder())) + using (Image image = provider.GetImage(TgaDecoder)) { image.DebugSave(provider); TgaTestUtils.CompareWithReferenceDecoder(provider, image); } } + + [Theory] + [WithFile(Bit16, PixelTypes.Rgba32)] + [WithFile(Bit24, PixelTypes.Rgba32)] + [WithFile(Bit32, PixelTypes.Rgba32)] + public void TgaDecoder_DegenerateMemoryRequest_ShouldTranslateTo_ImageFormatException(TestImageProvider provider) + where TPixel : struct, IPixel + { + provider.LimitAllocatorBufferCapacity().InPixelsSqrt(10); + ImageFormatException ex = Assert.Throws(() => provider.GetImage(TgaDecoder)); + Assert.IsType(ex.InnerException); + } + + [Theory] + [WithFile(Bit24, PixelTypes.Rgba32)] + [WithFile(Bit32, PixelTypes.Rgba32)] + public void TgaDecoder_CanDecode_WithLimitedAllocatorBufferCapacity(TestImageProvider provider) + where TPixel : struct, IPixel + { + static void RunTest(string providerDump, string nonContiguousBuffersStr) + { + TestImageProvider provider = BasicSerializer.Deserialize>(providerDump); + + provider.LimitAllocatorBufferCapacity().InPixelsSqrt(100); + + using Image image = provider.GetImage(TgaDecoder); + image.DebugSave(provider, testOutputDetails: nonContiguousBuffersStr); + + if (TestEnvironment.IsWindows) + { + image.CompareToOriginal(provider); + } + } + + string providerDump = BasicSerializer.Serialize(provider); + RemoteExecutor.Invoke( + RunTest, + providerDump, + "Disco") + .Dispose(); + } } } diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs index 26fe7cbda..339945f8b 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaEncoderTests.cs @@ -122,6 +122,16 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tga public void Encode_Bit32_WithRunLengthEncoding_Works(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel = TgaBitsPerPixel.Pixel32) where TPixel : struct, IPixel => TestTgaEncoderCore(provider, bitsPerPixel, TgaCompression.RunLength); + [Theory] + [WithFile(Bit32, PixelTypes.Rgba32, TgaBitsPerPixel.Pixel32)] + [WithFile(Bit24, PixelTypes.Rgba32, TgaBitsPerPixel.Pixel24)] + public void Encode_WorksWithDiscontiguousBuffers(TestImageProvider provider, TgaBitsPerPixel bitsPerPixel) + where TPixel : struct, IPixel + { + provider.LimitAllocatorBufferCapacity().InPixelsSqrt(100); + TestTgaEncoderCore(provider, bitsPerPixel, TgaCompression.RunLength); + } + private static void TestTgaEncoderCore( TestImageProvider provider, TgaBitsPerPixel bitsPerPixel, diff --git a/tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs b/tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs index 3f5e9040d..332a141e9 100644 --- a/tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs +++ b/tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs @@ -361,7 +361,7 @@ namespace SixLabors.ImageSharp.Tests.Helpers in operation); // Assert: - TestImageExtensions.CompareBuffers(expected.GetSpan(), actual.GetSpan()); + TestImageExtensions.CompareBuffers(expected.GetSingleSpan(), actual.GetSingleSpan()); } } diff --git a/tests/ImageSharp.Tests/Helpers/RowIntervalTests.cs b/tests/ImageSharp.Tests/Helpers/RowIntervalTests.cs index 0bb3f49d6..fd1eb546b 100644 --- a/tests/ImageSharp.Tests/Helpers/RowIntervalTests.cs +++ b/tests/ImageSharp.Tests/Helpers/RowIntervalTests.cs @@ -10,31 +10,6 @@ namespace SixLabors.ImageSharp.Tests.Helpers { public class RowIntervalTests { - [Theory] - [InlineData(10, 20, 5, 10)] - [InlineData(1, 10, 0, 10)] - [InlineData(1, 10, 5, 8)] - [InlineData(1, 1, 0, 1)] - [InlineData(10, 20, 9, 10)] - [InlineData(10, 20, 0, 1)] - public void GetMultiRowSpan(int width, int height, int min, int max) - { - using (Buffer2D buffer = Configuration.Default.MemoryAllocator.Allocate2D(width, height)) - { - var rows = new RowInterval(min, max); - - Span span = buffer.GetMultiRowSpan(rows); - - ref int expected0 = ref buffer.GetSpan()[min * width]; - int expectedLength = (max - min) * width; - - ref int actual0 = ref span[0]; - - Assert.Equal(span.Length, expectedLength); - Assert.True(Unsafe.AreSame(ref expected0, ref actual0)); - } - } - [Fact] public void Slice1() { diff --git a/tests/ImageSharp.Tests/Image/ImageFrameTests.cs b/tests/ImageSharp.Tests/Image/ImageFrameTests.cs new file mode 100644 index 000000000..58d7d7981 --- /dev/null +++ b/tests/ImageSharp.Tests/Image/ImageFrameTests.cs @@ -0,0 +1,96 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; + +namespace SixLabors.ImageSharp.Tests +{ + public class ImageFrameTests + { + public class Indexer + { + private readonly Configuration configuration = Configuration.CreateDefaultInstance(); + + private void LimitBufferCapacity(int bufferCapacityInBytes) + { + var allocator = (ArrayPoolMemoryAllocator)this.configuration.MemoryAllocator; + allocator.BufferCapacityInBytes = bufferCapacityInBytes; + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void GetSet(bool enforceDisco) + { + if (enforceDisco) + { + this.LimitBufferCapacity(100); + } + + using var image = new Image(this.configuration, 10, 10); + ImageFrame frame = image.Frames.RootFrame; + Rgba32 val = frame[3, 4]; + Assert.Equal(default(Rgba32), val); + frame[3, 4] = Color.Red; + val = frame[3, 4]; + Assert.Equal(Color.Red.ToRgba32(), val); + } + + public static TheoryData OutOfRangeData = new TheoryData() + { + { false, -1 }, + { false, 10 }, + { true, -1 }, + { true, 10 }, + }; + + [Theory] + [MemberData(nameof(OutOfRangeData))] + public void Get_OutOfRangeX(bool enforceDisco, int x) + { + if (enforceDisco) + { + this.LimitBufferCapacity(100); + } + + using var image = new Image(this.configuration, 10, 10); + ImageFrame frame = image.Frames.RootFrame; + ArgumentOutOfRangeException ex = Assert.Throws(() => _ = frame[x, 3]); + Assert.Equal("x", ex.ParamName); + } + + [Theory] + [MemberData(nameof(OutOfRangeData))] + public void Set_OutOfRangeX(bool enforceDisco, int x) + { + if (enforceDisco) + { + this.LimitBufferCapacity(100); + } + + using var image = new Image(this.configuration, 10, 10); + ImageFrame frame = image.Frames.RootFrame; + ArgumentOutOfRangeException ex = Assert.Throws(() => frame[x, 3] = default); + Assert.Equal("x", ex.ParamName); + } + + [Theory] + [MemberData(nameof(OutOfRangeData))] + public void Set_OutOfRangeY(bool enforceDisco, int y) + { + if (enforceDisco) + { + this.LimitBufferCapacity(100); + } + + using var image = new Image(this.configuration, 10, 10); + ImageFrame frame = image.Frames.RootFrame; + ArgumentOutOfRangeException ex = Assert.Throws(() => frame[3, y] = default); + Assert.Equal("y", ex.ParamName); + } + } + } +} diff --git a/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs b/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs index 0cf3071a0..423309dfb 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs @@ -9,6 +9,7 @@ using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; using Xunit; @@ -116,7 +117,7 @@ namespace SixLabors.ImageSharp.Tests using (var image = Image.WrapMemory(memory, bmp.Width, bmp.Height)) { - Assert.Equal(memory, image.GetPixelMemory()); + Assert.Equal(memory, image.GetRootFramePixelBuffer().GetSingleMemory()); image.GetPixelSpan().Fill(bg); for (var i = 10; i < 20; i++) { @@ -151,7 +152,7 @@ namespace SixLabors.ImageSharp.Tests using (var image = Image.WrapMemory(memoryManager, bmp.Width, bmp.Height)) { - Assert.Equal(memoryManager.Memory, image.GetPixelMemory()); + Assert.Equal(memoryManager.Memory, image.GetRootFramePixelBuffer().GetSingleMemory()); image.GetPixelSpan().Fill(bg); for (var i = 10; i < 20; i++) diff --git a/tests/ImageSharp.Tests/Image/ImageTests.cs b/tests/ImageSharp.Tests/Image/ImageTests.cs index 0fa917972..c99b75733 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.cs @@ -1,7 +1,9 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. +using System; using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Tests.Memory; @@ -85,5 +87,84 @@ namespace SixLabors.ImageSharp.Tests } } } + + public class Indexer + { + private readonly Configuration configuration = Configuration.CreateDefaultInstance(); + + private void LimitBufferCapacity(int bufferCapacityInBytes) + { + var allocator = (ArrayPoolMemoryAllocator)this.configuration.MemoryAllocator; + allocator.BufferCapacityInBytes = bufferCapacityInBytes; + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void GetSet(bool enforceDisco) + { + if (enforceDisco) + { + this.LimitBufferCapacity(100); + } + + using var image = new Image(this.configuration, 10, 10); + Rgba32 val = image[3, 4]; + Assert.Equal(default(Rgba32), val); + image[3, 4] = Color.Red; + val = image[3, 4]; + Assert.Equal(Color.Red.ToRgba32(), val); + } + + public static TheoryData OutOfRangeData = new TheoryData() + { + { false, -1 }, + { false, 10 }, + { true, -1 }, + { true, 10 }, + }; + + [Theory] + [MemberData(nameof(OutOfRangeData))] + public void Get_OutOfRangeX(bool enforceDisco, int x) + { + if (enforceDisco) + { + this.LimitBufferCapacity(100); + } + + using var image = new Image(this.configuration, 10, 10); + ArgumentOutOfRangeException ex = Assert.Throws(() => _ = image[x, 3]); + Assert.Equal("x", ex.ParamName); + } + + [Theory] + [MemberData(nameof(OutOfRangeData))] + public void Set_OutOfRangeX(bool enforceDisco, int x) + { + if (enforceDisco) + { + this.LimitBufferCapacity(100); + } + + using var image = new Image(this.configuration, 10, 10); + ArgumentOutOfRangeException ex = Assert.Throws(() => image[x, 3] = default); + Assert.Equal("x", ex.ParamName); + } + + [Theory] + [MemberData(nameof(OutOfRangeData))] + public void Set_OutOfRangeY(bool enforceDisco, int y) + { + if (enforceDisco) + { + this.LimitBufferCapacity(100); + } + + using var image = new Image(this.configuration, 10, 10); + ArgumentOutOfRangeException ex = Assert.Throws(() => image[3, y] = default); + Assert.Equal("y", ex.ParamName); + } + } } } diff --git a/tests/ImageSharp.Tests/Image/LargeImageIntegrationTests.cs b/tests/ImageSharp.Tests/Image/LargeImageIntegrationTests.cs new file mode 100644 index 000000000..c8a8baf1d --- /dev/null +++ b/tests/ImageSharp.Tests/Image/LargeImageIntegrationTests.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using Xunit; + +namespace SixLabors.ImageSharp.Tests +{ + public class LargeImageIntegrationTests + { + [Theory(Skip = "For local testing only.")] + [WithBasicTestPatternImages(width: 30000, height: 30000, PixelTypes.Rgba32)] + public void CreateAndResize(TestImageProvider provider) + { + using Image image = provider.GetImage(); + image.Mutate(c => c.Resize(1000, 1000)); + image.DebugSave(provider); + } + } +} diff --git a/tests/ImageSharp.Tests/Memory/Alocators/ArrayPoolMemoryAllocatorTests.cs b/tests/ImageSharp.Tests/Memory/Allocators/ArrayPoolMemoryAllocatorTests.cs similarity index 84% rename from tests/ImageSharp.Tests/Memory/Alocators/ArrayPoolMemoryAllocatorTests.cs rename to tests/ImageSharp.Tests/Memory/Allocators/ArrayPoolMemoryAllocatorTests.cs index dd497e57e..1e079fcf5 100644 --- a/tests/ImageSharp.Tests/Memory/Alocators/ArrayPoolMemoryAllocatorTests.cs +++ b/tests/ImageSharp.Tests/Memory/Allocators/ArrayPoolMemoryAllocatorTests.cs @@ -1,18 +1,15 @@ // Copyright (c) Six Labors and contributors. // Licensed under the Apache License, Version 2.0. -// ReSharper disable InconsistentNaming using System; using System.Buffers; -using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using Microsoft.DotNet.RemoteExecutor; -using Microsoft.Win32; -using SixLabors.ImageSharp.Tests; +using SixLabors.ImageSharp.Memory; using Xunit; -namespace SixLabors.ImageSharp.Memory.Tests +namespace SixLabors.ImageSharp.Tests.Memory.Allocators { public class ArrayPoolMemoryAllocatorTests { @@ -116,13 +113,13 @@ namespace SixLabors.ImageSharp.Memory.Tests MemoryAllocator memoryAllocator = this.LocalFixture.MemoryAllocator; using (IMemoryOwner firstAlloc = memoryAllocator.Allocate(42)) { - firstAlloc.GetSpan().Fill(666); + BufferExtensions.GetSpan(firstAlloc).Fill(666); } using (IMemoryOwner secondAlloc = memoryAllocator.Allocate(42, options)) { int expected = options == AllocationOptions.Clean ? 0 : 666; - Assert.Equal(expected, secondAlloc.GetSpan()[0]); + Assert.Equal(expected, BufferExtensions.GetSpan(secondAlloc)[0]); } } @@ -133,7 +130,7 @@ namespace SixLabors.ImageSharp.Memory.Tests { MemoryAllocator memoryAllocator = this.LocalFixture.MemoryAllocator; IMemoryOwner buffer = memoryAllocator.Allocate(32); - ref int ptrToPrev0 = ref MemoryMarshal.GetReference(buffer.GetSpan()); + ref int ptrToPrev0 = ref MemoryMarshal.GetReference(BufferExtensions.GetSpan(buffer)); if (!keepBufferAlive) { @@ -144,7 +141,7 @@ namespace SixLabors.ImageSharp.Memory.Tests buffer = memoryAllocator.Allocate(32); - Assert.False(Unsafe.AreSame(ref ptrToPrev0, ref buffer.GetReference())); + Assert.False(Unsafe.AreSame(ref ptrToPrev0, ref BufferExtensions.GetReference(buffer))); } [Fact] @@ -164,12 +161,12 @@ namespace SixLabors.ImageSharp.Memory.Tests const int ArrayLengthThreshold = PoolSelectorThresholdInBytes / sizeof(int); IMemoryOwner small = StaticFixture.MemoryAllocator.Allocate(ArrayLengthThreshold - 1); - ref int ptr2Small = ref small.GetReference(); + ref int ptr2Small = ref BufferExtensions.GetReference(small); small.Dispose(); IMemoryOwner large = StaticFixture.MemoryAllocator.Allocate(ArrayLengthThreshold + 1); - Assert.False(Unsafe.AreSame(ref ptr2Small, ref large.GetReference())); + Assert.False(Unsafe.AreSame(ref ptr2Small, ref BufferExtensions.GetReference(large))); } RemoteExecutor.Invoke(RunTest).Dispose(); @@ -216,14 +213,34 @@ namespace SixLabors.ImageSharp.Memory.Tests [Theory] [InlineData(-1)] - [InlineData((int.MaxValue / SizeOfLargeStruct) + 1)] - public void AllocateIncorrectAmount_ThrowsCorrect_ArgumentOutOfRangeException(int length) + [InlineData(-111)] + public void Allocate_Negative_Throws_ArgumentOutOfRangeException(int length) { ArgumentOutOfRangeException ex = Assert.Throws(() => this.LocalFixture.MemoryAllocator.Allocate(length)); Assert.Equal("length", ex.ParamName); } + [Fact] + public void AllocateZero() + { + using IMemoryOwner buffer = this.LocalFixture.MemoryAllocator.Allocate(0); + Assert.Equal(0, buffer.Memory.Length); + } + + [Theory] + [InlineData(101)] + [InlineData((int.MaxValue / SizeOfLargeStruct) - 1)] + [InlineData(int.MaxValue / SizeOfLargeStruct)] + [InlineData((int.MaxValue / SizeOfLargeStruct) + 1)] + [InlineData((int.MaxValue / SizeOfLargeStruct) + 137)] + public void Allocate_OverCapacity_Throws_InvalidMemoryOperationException(int length) + { + this.LocalFixture.MemoryAllocator.BufferCapacityInBytes = 100 * SizeOfLargeStruct; + Assert.Throws(() => + this.LocalFixture.MemoryAllocator.Allocate(length)); + } + [Theory] [InlineData(-1)] public void AllocateManagedByteBuffer_IncorrectAmount_ThrowsCorrect_ArgumentOutOfRangeException(int length) @@ -235,7 +252,7 @@ namespace SixLabors.ImageSharp.Memory.Tests private class MemoryAllocatorFixture { - public MemoryAllocator MemoryAllocator { get; set; } = + public ArrayPoolMemoryAllocator MemoryAllocator { get; set; } = new ArrayPoolMemoryAllocator(MaxPooledBufferSizeInBytes, PoolSelectorThresholdInBytes); /// @@ -245,11 +262,11 @@ namespace SixLabors.ImageSharp.Memory.Tests where T : struct { IMemoryOwner buffer = this.MemoryAllocator.Allocate(length); - ref T ptrToPrevPosition0 = ref buffer.GetReference(); + ref T ptrToPrevPosition0 = ref BufferExtensions.GetReference(buffer); buffer.Dispose(); buffer = this.MemoryAllocator.Allocate(length); - bool sameBuffers = Unsafe.AreSame(ref ptrToPrevPosition0, ref buffer.GetReference()); + bool sameBuffers = Unsafe.AreSame(ref ptrToPrevPosition0, ref BufferExtensions.GetReference(buffer)); buffer.Dispose(); return sameBuffers; diff --git a/tests/ImageSharp.Tests/Memory/Alocators/BufferExtensions.cs b/tests/ImageSharp.Tests/Memory/Allocators/BufferExtensions.cs similarity index 93% rename from tests/ImageSharp.Tests/Memory/Alocators/BufferExtensions.cs rename to tests/ImageSharp.Tests/Memory/Allocators/BufferExtensions.cs index 8073d069d..9f8543fff 100644 --- a/tests/ImageSharp.Tests/Memory/Alocators/BufferExtensions.cs +++ b/tests/ImageSharp.Tests/Memory/Allocators/BufferExtensions.cs @@ -6,7 +6,7 @@ using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -namespace SixLabors.ImageSharp.Memory.Tests +namespace SixLabors.ImageSharp.Tests.Memory.Allocators { internal static class BufferExtensions { @@ -22,4 +22,4 @@ namespace SixLabors.ImageSharp.Memory.Tests where T : struct => ref MemoryMarshal.GetReference(buffer.GetSpan()); } -} \ No newline at end of file +} diff --git a/tests/ImageSharp.Tests/Memory/Alocators/BufferTestSuite.cs b/tests/ImageSharp.Tests/Memory/Allocators/BufferTestSuite.cs similarity index 99% rename from tests/ImageSharp.Tests/Memory/Alocators/BufferTestSuite.cs rename to tests/ImageSharp.Tests/Memory/Allocators/BufferTestSuite.cs index 4590bbe97..6465e0b81 100644 --- a/tests/ImageSharp.Tests/Memory/Alocators/BufferTestSuite.cs +++ b/tests/ImageSharp.Tests/Memory/Allocators/BufferTestSuite.cs @@ -5,10 +5,11 @@ using System; using System.Buffers; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; using Xunit; // ReSharper disable InconsistentNaming -namespace SixLabors.ImageSharp.Memory.Tests +namespace SixLabors.ImageSharp.Tests.Memory.Allocators { /// /// Inherit this class to test an implementation (provided by ). diff --git a/tests/ImageSharp.Tests/Memory/Alocators/SimpleGcMemoryAllocatorTests.cs b/tests/ImageSharp.Tests/Memory/Allocators/SimpleGcMemoryAllocatorTests.cs similarity index 93% rename from tests/ImageSharp.Tests/Memory/Alocators/SimpleGcMemoryAllocatorTests.cs rename to tests/ImageSharp.Tests/Memory/Allocators/SimpleGcMemoryAllocatorTests.cs index 8e3b82be5..9e14bd1db 100644 --- a/tests/ImageSharp.Tests/Memory/Alocators/SimpleGcMemoryAllocatorTests.cs +++ b/tests/ImageSharp.Tests/Memory/Allocators/SimpleGcMemoryAllocatorTests.cs @@ -3,9 +3,10 @@ using System; using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; using Xunit; -namespace SixLabors.ImageSharp.Memory.Tests +namespace SixLabors.ImageSharp.Tests.Memory.Allocators { public class SimpleGcMemoryAllocatorTests { diff --git a/tests/ImageSharp.Tests/Memory/Buffer2DTests.cs b/tests/ImageSharp.Tests/Memory/Buffer2DTests.cs index 02b59825b..ab04b3700 100644 --- a/tests/ImageSharp.Tests/Memory/Buffer2DTests.cs +++ b/tests/ImageSharp.Tests/Memory/Buffer2DTests.cs @@ -3,6 +3,8 @@ using System; using System.Buffers; +using System.Diagnostics; +using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -18,37 +20,74 @@ namespace SixLabors.ImageSharp.Tests.Memory // ReSharper disable once ClassNeverInstantiated.Local private class Assert : Xunit.Assert { - public static void SpanPointsTo(Span span, IMemoryOwner buffer, int bufferOffset = 0) + public static void SpanPointsTo(Span span, Memory buffer, int bufferOffset = 0) where T : struct { ref T actual = ref MemoryMarshal.GetReference(span); - ref T expected = ref Unsafe.Add(ref buffer.GetReference(), bufferOffset); + ref T expected = ref buffer.Span[bufferOffset]; True(Unsafe.AreSame(ref expected, ref actual), "span does not point to the expected position"); } } - private MemoryAllocator MemoryAllocator { get; } = new TestMemoryAllocator(); + private TestMemoryAllocator MemoryAllocator { get; } = new TestMemoryAllocator(); + + private const int Big = 99999; + + [Theory] + [InlineData(Big, 7, 42)] + [InlineData(Big, 1025, 17)] + [InlineData(300, 42, 777)] + public unsafe void Construct(int bufferCapacity, int width, int height) + { + this.MemoryAllocator.BufferCapacityInBytes = sizeof(TestStructs.Foo) * bufferCapacity; + + using (Buffer2D buffer = this.MemoryAllocator.Allocate2D(width, height)) + { + Assert.Equal(width, buffer.Width); + Assert.Equal(height, buffer.Height); + Assert.Equal(width * height, buffer.FastMemoryGroup.TotalLength); + Assert.True(buffer.FastMemoryGroup.BufferLength % width == 0); + } + } [Theory] - [InlineData(7, 42)] - [InlineData(1025, 17)] - public void Construct(int width, int height) + [InlineData(Big, 0, 42)] + [InlineData(Big, 1, 0)] + [InlineData(60, 42, 0)] + [InlineData(3, 0, 0)] + public unsafe void Construct_Empty(int bufferCapacity, int width, int height) { + this.MemoryAllocator.BufferCapacityInBytes = sizeof(TestStructs.Foo) * bufferCapacity; + using (Buffer2D buffer = this.MemoryAllocator.Allocate2D(width, height)) { Assert.Equal(width, buffer.Width); Assert.Equal(height, buffer.Height); - Assert.Equal(width * height, buffer.GetMemory().Length); + Assert.Equal(0, buffer.FastMemoryGroup.TotalLength); + Assert.Equal(0, buffer.GetSingleSpan().Length); } } + [Theory] + [InlineData(50, 10, 20, 4)] + public void Allocate2DOveraligned(int bufferCapacity, int width, int height, int alignmentMultiplier) + { + this.MemoryAllocator.BufferCapacityInBytes = sizeof(int) * bufferCapacity; + + using Buffer2D buffer = this.MemoryAllocator.Allocate2DOveraligned(width, height, alignmentMultiplier); + MemoryGroup memoryGroup = buffer.FastMemoryGroup; + int expectedAlignment = width * alignmentMultiplier; + + Assert.Equal(expectedAlignment, memoryGroup.BufferLength); + } + [Fact] public void CreateClean() { using (Buffer2D buffer = this.MemoryAllocator.Allocate2D(42, 42, AllocationOptions.Clean)) { - Span span = buffer.GetSpan(); + Span span = buffer.GetSingleSpan(); for (int j = 0; j < span.Length; j++) { Assert.Equal(0, span[j]); @@ -57,58 +96,147 @@ namespace SixLabors.ImageSharp.Tests.Memory } [Theory] - [InlineData(7, 42, 0)] - [InlineData(7, 42, 10)] - [InlineData(17, 42, 41)] - public void GetRowSpanY(int width, int height, int y) + [InlineData(Big, 7, 42, 0, 0)] + [InlineData(Big, 7, 42, 10, 0)] + [InlineData(Big, 17, 42, 41, 0)] + [InlineData(500, 17, 42, 41, 1)] + [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) { + this.MemoryAllocator.BufferCapacityInBytes = sizeof(TestStructs.Foo) * bufferCapacity; + using (Buffer2D buffer = this.MemoryAllocator.Allocate2D(width, height)) { Span span = buffer.GetRowSpan(y); - // Assert.Equal(width * y, span.Start); Assert.Equal(width, span.Length); - Assert.SpanPointsTo(span, buffer.MemorySource.MemoryOwner, width * y); + + int expectedSubBufferOffset = (width * y) - (expectedBufferIndex * buffer.FastMemoryGroup.BufferLength); + Assert.SpanPointsTo(span, buffer.FastMemoryGroup[expectedBufferIndex], expectedSubBufferOffset); } } + public static TheoryData GetRowSpanY_OutOfRange_Data = new TheoryData() + { + { Big, 10, 8, -1 }, + { Big, 10, 8, 8 }, + { 20, 10, 8, -1 }, + { 20, 10, 8, 10 }, + }; + + [Theory] + [MemberData(nameof(GetRowSpanY_OutOfRange_Data))] + public void GetRowSpan_OutOfRange(int bufferCapacity, int width, int height, int y) + { + this.MemoryAllocator.BufferCapacityInBytes = bufferCapacity; + using Buffer2D buffer = this.MemoryAllocator.Allocate2D(width, height); + + Exception ex = Assert.ThrowsAny(() => buffer.GetRowSpan(y)); + Assert.True(ex is ArgumentOutOfRangeException || ex is IndexOutOfRangeException); + } + + public static TheoryData Indexer_OutOfRange_Data = new TheoryData() + { + { Big, 10, 8, 1, -1 }, + { Big, 10, 8, 1, 8 }, + { Big, 10, 8, -1, 1 }, + { Big, 10, 8, 10, 1 }, + { 20, 10, 8, 1, -1 }, + { 20, 10, 8, 1, 10 }, + { 20, 10, 8, -1, 1 }, + { 20, 10, 8, 10, 1 }, + }; + + [Theory] + [MemberData(nameof(Indexer_OutOfRange_Data))] + public void Indexer_OutOfRange(int bufferCapacity, int width, int height, int x, int y) + { + this.MemoryAllocator.BufferCapacityInBytes = bufferCapacity; + using Buffer2D buffer = this.MemoryAllocator.Allocate2D(width, height); + + Exception ex = Assert.ThrowsAny(() => buffer[x, y]++); + Assert.True(ex is ArgumentOutOfRangeException || ex is IndexOutOfRangeException); + } + [Theory] - [InlineData(42, 8, 0, 0)] - [InlineData(400, 1000, 20, 10)] - [InlineData(99, 88, 98, 87)] - public void Indexer(int width, int height, int x, int y) + [InlineData(Big, 42, 8, 0, 0)] + [InlineData(Big, 400, 1000, 20, 10)] + [InlineData(Big, 99, 88, 98, 87)] + [InlineData(500, 200, 30, 42, 13)] + [InlineData(500, 200, 30, 199, 29)] + public unsafe void Indexer(int bufferCapacity, int width, int height, int x, int y) { + this.MemoryAllocator.BufferCapacityInBytes = sizeof(TestStructs.Foo) * bufferCapacity; + using (Buffer2D buffer = this.MemoryAllocator.Allocate2D(width, height)) { - Span span = buffer.MemorySource.GetSpan(); + int bufferIndex = (width * y) / buffer.FastMemoryGroup.BufferLength; + int subBufferStart = (width * y) - (bufferIndex * buffer.FastMemoryGroup.BufferLength); + + Span span = buffer.FastMemoryGroup[bufferIndex].Span.Slice(subBufferStart); ref TestStructs.Foo actual = ref buffer[x, y]; - ref TestStructs.Foo expected = ref span[(y * width) + x]; + ref TestStructs.Foo expected = ref span[x]; Assert.True(Unsafe.AreSame(ref expected, ref actual)); } } [Fact] - public void SwapOrCopyContent() + public void SwapOrCopyContent_WhenBothAllocated() { - using (Buffer2D a = this.MemoryAllocator.Allocate2D(10, 5)) - using (Buffer2D b = this.MemoryAllocator.Allocate2D(3, 7)) + using (Buffer2D a = this.MemoryAllocator.Allocate2D(10, 5, AllocationOptions.Clean)) + using (Buffer2D b = this.MemoryAllocator.Allocate2D(3, 7, AllocationOptions.Clean)) { - IMemoryOwner aa = a.MemorySource.MemoryOwner; - IMemoryOwner bb = b.MemorySource.MemoryOwner; + a[1, 3] = 666; + b[1, 3] = 444; + + Memory aa = a.FastMemoryGroup.Single(); + Memory bb = b.FastMemoryGroup.Single(); Buffer2D.SwapOrCopyContent(a, b); - Assert.Equal(bb, a.MemorySource.MemoryOwner); - Assert.Equal(aa, b.MemorySource.MemoryOwner); + 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.Wrap(new int[100]); + using var dest = new Buffer2D(destData, 10, 10); + + using (Buffer2D source = this.MemoryAllocator.Allocate2D(10, 10, AllocationOptions.Clean)) + { + source[0, 0] = 1; + dest[0, 0] = 2; + + Buffer2D.SwapOrCopyContent(dest, source); + } + + int actual1 = dest.GetRowSpan(0)[0]; + int actual2 = dest.GetRowSpan(0)[0]; + int actual3 = dest.GetSafeRowMemory(0).Span[0]; + int actual4 = dest.GetFastRowMemory(0).Span[0]; + int actual5 = dest[0, 0]; + + Assert.Equal(1, actual1); + Assert.Equal(1, actual2); + Assert.Equal(1, actual3); + Assert.Equal(1, actual4); + Assert.Equal(1, actual5); + } + [Theory] [InlineData(100, 20, 0, 90, 10)] [InlineData(100, 3, 0, 50, 50)] @@ -121,7 +249,7 @@ namespace SixLabors.ImageSharp.Tests.Memory var rnd = new Random(123); using (Buffer2D b = this.MemoryAllocator.Allocate2D(width, height)) { - rnd.RandomFill(b.GetSpan(), 0, 1); + rnd.RandomFill(b.GetSingleSpan(), 0, 1); b.CopyColumns(startIndex, destIndex, columnCount); @@ -143,7 +271,7 @@ namespace SixLabors.ImageSharp.Tests.Memory var rnd = new Random(123); using (Buffer2D b = this.MemoryAllocator.Allocate2D(100, 100)) { - rnd.RandomFill(b.GetSpan(), 0, 1); + rnd.RandomFill(b.GetSingleSpan(), 0, 1); b.CopyColumns(0, 50, 22); b.CopyColumns(0, 50, 22); @@ -159,5 +287,18 @@ namespace SixLabors.ImageSharp.Tests.Memory } } } + + [Fact] + public void PublicMemoryGroup_IsMemoryGroupView() + { + using Buffer2D buffer1 = this.MemoryAllocator.Allocate2D(10, 10); + using Buffer2D buffer2 = this.MemoryAllocator.Allocate2D(10, 10); + IMemoryGroup mgBefore = buffer1.MemoryGroup; + + Buffer2D.SwapOrCopyContent(buffer1, buffer2); + + Assert.False(mgBefore.IsValid); + Assert.NotSame(mgBefore, buffer1.MemoryGroup); + } } } diff --git a/tests/ImageSharp.Tests/Memory/BufferAreaTests.cs b/tests/ImageSharp.Tests/Memory/BufferAreaTests.cs index 9f523156f..77e899a4c 100644 --- a/tests/ImageSharp.Tests/Memory/BufferAreaTests.cs +++ b/tests/ImageSharp.Tests/Memory/BufferAreaTests.cs @@ -9,22 +9,22 @@ namespace SixLabors.ImageSharp.Tests.Memory { public class BufferAreaTests { + private readonly TestMemoryAllocator memoryAllocator = new TestMemoryAllocator(); + [Fact] public void Construct() { - using (var buffer = Configuration.Default.MemoryAllocator.Allocate2D(10, 20)) - { - var rectangle = new Rectangle(3, 2, 5, 6); - var area = new BufferArea(buffer, rectangle); + using Buffer2D buffer = this.memoryAllocator.Allocate2D(10, 20); + var rectangle = new Rectangle(3, 2, 5, 6); + var area = new BufferArea(buffer, rectangle); - Assert.Equal(buffer, area.DestinationBuffer); - Assert.Equal(rectangle, area.Rectangle); - } + Assert.Equal(buffer, area.DestinationBuffer); + Assert.Equal(rectangle, area.Rectangle); } - private static Buffer2D CreateTestBuffer(int w, int h) + private Buffer2D CreateTestBuffer(int w, int h) { - var buffer = Configuration.Default.MemoryAllocator.Allocate2D(w, h); + Buffer2D buffer = this.memoryAllocator.Allocate2D(w, h); for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) @@ -37,110 +37,122 @@ namespace SixLabors.ImageSharp.Tests.Memory } [Theory] - [InlineData(2, 3, 2, 2)] - [InlineData(5, 4, 3, 2)] - public void Indexer(int rx, int ry, int x, int y) + [InlineData(1000, 2, 3, 2, 2)] + [InlineData(1000, 5, 4, 3, 2)] + [InlineData(200, 2, 3, 2, 2)] + [InlineData(200, 5, 4, 3, 2)] + public void Indexer(int bufferCapacity, int rx, int ry, int x, int y) { - using (Buffer2D buffer = CreateTestBuffer(20, 30)) - { - var r = new Rectangle(rx, ry, 5, 6); + this.memoryAllocator.BufferCapacityInBytes = sizeof(int) * bufferCapacity; + using Buffer2D buffer = this.CreateTestBuffer(20, 30); + var r = new Rectangle(rx, ry, 5, 6); - BufferArea area = buffer.GetArea(r); + BufferArea area = buffer.GetArea(r); - int value = area[x, y]; - int expected = ((ry + y) * 100) + rx + x; - Assert.Equal(expected, value); - } + int value = area[x, y]; + int expected = ((ry + y) * 100) + rx + x; + Assert.Equal(expected, value); } [Theory] - [InlineData(2, 3, 2, 5, 6)] - [InlineData(5, 4, 3, 6, 5)] - public void GetRowSpan(int rx, int ry, int y, int w, int h) + [InlineData(1000, 2, 3, 2, 5, 6)] + [InlineData(1000, 5, 4, 3, 6, 5)] + [InlineData(200, 2, 3, 2, 5, 6)] + [InlineData(200, 5, 4, 3, 6, 5)] + public void GetRowSpan(int bufferCapacity, int rx, int ry, int y, int w, int h) { - using (Buffer2D buffer = CreateTestBuffer(20, 30)) - { - var r = new Rectangle(rx, ry, w, h); + this.memoryAllocator.BufferCapacityInBytes = sizeof(int) * bufferCapacity; - BufferArea area = buffer.GetArea(r); + using Buffer2D buffer = this.CreateTestBuffer(20, 30); + var r = new Rectangle(rx, ry, w, h); - Span span = area.GetRowSpan(y); + BufferArea area = buffer.GetArea(r); - Assert.Equal(w, span.Length); + Span span = area.GetRowSpan(y); - for (int i = 0; i < w; i++) - { - int expected = ((ry + y) * 100) + rx + i; - int value = span[i]; + Assert.Equal(w, span.Length); - Assert.Equal(expected, value); - } + for (int i = 0; i < w; i++) + { + int expected = ((ry + y) * 100) + rx + i; + int value = span[i]; + + Assert.Equal(expected, value); } } [Fact] public void GetSubArea() { - using (Buffer2D buffer = CreateTestBuffer(20, 30)) - { - BufferArea area0 = buffer.GetArea(6, 8, 10, 10); + using Buffer2D buffer = this.CreateTestBuffer(20, 30); + BufferArea area0 = buffer.GetArea(6, 8, 10, 10); - BufferArea area1 = area0.GetSubArea(4, 4, 5, 5); + BufferArea area1 = area0.GetSubArea(4, 4, 5, 5); - var expectedRect = new Rectangle(10, 12, 5, 5); + var expectedRect = new Rectangle(10, 12, 5, 5); - Assert.Equal(buffer, area1.DestinationBuffer); - Assert.Equal(expectedRect, area1.Rectangle); + Assert.Equal(buffer, area1.DestinationBuffer); + Assert.Equal(expectedRect, area1.Rectangle); - int value00 = (12 * 100) + 10; - Assert.Equal(value00, area1[0, 0]); - } + int value00 = (12 * 100) + 10; + Assert.Equal(value00, area1[0, 0]); } - [Fact] - public void DangerousGetPinnableReference() + [Theory] + [InlineData(1000)] + [InlineData(40)] + public void GetReferenceToOrigin(int bufferCapacity) { - using (Buffer2D buffer = CreateTestBuffer(20, 30)) - { - BufferArea area0 = buffer.GetArea(6, 8, 10, 10); + this.memoryAllocator.BufferCapacityInBytes = sizeof(int) * bufferCapacity; - ref int r = ref area0.GetReferenceToOrigin(); + using Buffer2D buffer = this.CreateTestBuffer(20, 30); + BufferArea area0 = buffer.GetArea(6, 8, 10, 10); - int expected = buffer[6, 8]; - Assert.Equal(expected, r); - } + ref int r = ref area0.GetReferenceToOrigin(); + + int expected = buffer[6, 8]; + Assert.Equal(expected, r); } - [Fact] - public void Clear_FullArea() + [Theory] + [InlineData(1000)] + [InlineData(70)] + public void Clear_FullArea(int bufferCapacity) { - using (Buffer2D buffer = CreateTestBuffer(22, 13)) + this.memoryAllocator.BufferCapacityInBytes = sizeof(int) * bufferCapacity; + + using Buffer2D buffer = this.CreateTestBuffer(22, 13); + var emptyRow = new int[22]; + buffer.GetArea().Clear(); + + for (int y = 0; y < 13; y++) { - buffer.GetArea().Clear(); - Span fullSpan = buffer.GetSpan(); - Assert.True(fullSpan.SequenceEqual(new int[fullSpan.Length])); + Span row = buffer.GetRowSpan(y); + Assert.True(row.SequenceEqual(emptyRow)); } } - [Fact] - public void Clear_SubArea() + [Theory] + [InlineData(1000)] + [InlineData(40)] + public void Clear_SubArea(int bufferCapacity) { - using (Buffer2D buffer = CreateTestBuffer(20, 30)) - { - BufferArea area = buffer.GetArea(5, 5, 10, 10); - area.Clear(); + this.memoryAllocator.BufferCapacityInBytes = sizeof(int) * bufferCapacity; - Assert.NotEqual(0, buffer[4, 4]); - Assert.NotEqual(0, buffer[15, 15]); + using Buffer2D buffer = this.CreateTestBuffer(20, 30); + BufferArea area = buffer.GetArea(5, 5, 10, 10); + area.Clear(); - Assert.Equal(0, buffer[5, 5]); - Assert.Equal(0, buffer[14, 14]); + Assert.NotEqual(0, buffer[4, 4]); + Assert.NotEqual(0, buffer[15, 15]); - for (int y = area.Rectangle.Y; y < area.Rectangle.Bottom; y++) - { - Span span = buffer.GetRowSpan(y).Slice(area.Rectangle.X, area.Width); - Assert.True(span.SequenceEqual(new int[area.Width])); - } + Assert.Equal(0, buffer[5, 5]); + Assert.Equal(0, buffer[14, 14]); + + for (int y = area.Rectangle.Y; y < area.Rectangle.Bottom; y++) + { + Span span = buffer.GetRowSpan(y).Slice(area.Rectangle.X, area.Width); + Assert.True(span.SequenceEqual(new int[area.Width])); } } } diff --git a/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupIndex.cs b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupIndex.cs new file mode 100644 index 000000000..555d641c7 --- /dev/null +++ b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupIndex.cs @@ -0,0 +1,120 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers +{ + public struct MemoryGroupIndex : IEquatable + { + public override bool Equals(object obj) => obj is MemoryGroupIndex other && this.Equals(other); + + public override int GetHashCode() => HashCode.Combine(this.BufferLength, this.BufferIndex, this.ElementIndex); + + public int BufferLength { get; } + + public int BufferIndex { get; } + + public int ElementIndex { get; } + + public MemoryGroupIndex(int bufferLength, int bufferIndex, int elementIndex) + { + this.BufferLength = bufferLength; + this.BufferIndex = bufferIndex; + this.ElementIndex = elementIndex; + } + + public static MemoryGroupIndex operator +(MemoryGroupIndex idx, int val) + { + int nextElementIndex = idx.ElementIndex + val; + return new MemoryGroupIndex( + idx.BufferLength, + idx.BufferIndex + (nextElementIndex / idx.BufferLength), + nextElementIndex % idx.BufferLength); + } + + public bool Equals(MemoryGroupIndex other) + { + if (this.BufferLength != other.BufferLength) + { + throw new InvalidOperationException(); + } + + return this.BufferIndex == other.BufferIndex && this.ElementIndex == other.ElementIndex; + } + + public static bool operator ==(MemoryGroupIndex a, MemoryGroupIndex b) => a.Equals(b); + + public static bool operator !=(MemoryGroupIndex a, MemoryGroupIndex b) => !a.Equals(b); + + public static bool operator <(MemoryGroupIndex a, MemoryGroupIndex b) + { + if (a.BufferLength != b.BufferLength) + { + throw new InvalidOperationException(); + } + + if (a.BufferIndex < b.BufferIndex) + { + return true; + } + + if (a.BufferIndex == b.BufferIndex) + { + return a.ElementIndex < b.ElementIndex; + } + + return false; + } + + public static bool operator >(MemoryGroupIndex a, MemoryGroupIndex b) + { + if (a.BufferLength != b.BufferLength) + { + throw new InvalidOperationException(); + } + + if (a.BufferIndex > b.BufferIndex) + { + return true; + } + + if (a.BufferIndex == b.BufferIndex) + { + return a.ElementIndex > b.ElementIndex; + } + + return false; + } + } + + internal static class MemoryGroupIndexExtensions + { + public static T GetElementAt(this IMemoryGroup group, MemoryGroupIndex idx) + where T : struct + { + return group[idx.BufferIndex].Span[idx.ElementIndex]; + } + + public static void SetElementAt(this IMemoryGroup group, MemoryGroupIndex idx, T value) + where T : struct + { + group[idx.BufferIndex].Span[idx.ElementIndex] = value; + } + + public static MemoryGroupIndex MinIndex(this IMemoryGroup group) + where T : struct + { + return new MemoryGroupIndex(group.BufferLength, 0, 0); + } + + public static MemoryGroupIndex MaxIndex(this IMemoryGroup group) + where T : struct + { + return group.Count == 0 + ? new MemoryGroupIndex(group.BufferLength, 0, 0) + : new MemoryGroupIndex(group.BufferLength, group.Count - 1, group[group.Count - 1].Length); + } + } +} diff --git a/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupIndexTests.cs b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupIndexTests.cs new file mode 100644 index 000000000..f0cc18f29 --- /dev/null +++ b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupIndexTests.cs @@ -0,0 +1,67 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers +{ + public class MemoryGroupIndexTests + { + [Fact] + public void Equal() + { + var a = new MemoryGroupIndex(10, 1, 3); + var b = new MemoryGroupIndex(10, 1, 3); + + Assert.True(a.Equals(b)); + Assert.True(a == b); + Assert.False(a != b); + Assert.False(a < b); + Assert.False(a > b); + } + + [Fact] + public void SmallerBufferIndex() + { + var a = new MemoryGroupIndex(10, 3, 3); + var b = new MemoryGroupIndex(10, 5, 3); + + Assert.False(a == b); + Assert.True(a != b); + Assert.True(a < b); + Assert.False(a > b); + } + + [Fact] + public void SmallerElementIndex() + { + var a = new MemoryGroupIndex(10, 3, 3); + var b = new MemoryGroupIndex(10, 3, 9); + + Assert.False(a == b); + Assert.True(a != b); + Assert.True(a < b); + Assert.False(a > b); + } + + [Fact] + public void Increment() + { + var a = new MemoryGroupIndex(10, 3, 3); + a += 1; + Assert.Equal(new MemoryGroupIndex(10, 3, 4), a); + } + + [Fact] + public void Increment_OverflowBuffer() + { + var a = new MemoryGroupIndex(10, 5, 3); + var b = new MemoryGroupIndex(10, 5, 9); + a += 8; + b += 1; + + Assert.Equal(new MemoryGroupIndex(10, 6, 1), a); + Assert.Equal(new MemoryGroupIndex(10, 6, 0), b); + } + } +} diff --git a/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.Allocate.cs b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.Allocate.cs new file mode 100644 index 000000000..298b5a93f --- /dev/null +++ b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.Allocate.cs @@ -0,0 +1,128 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Generic; +using System.Linq; +using SixLabors.ImageSharp.Memory; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers +{ + public partial class MemoryGroupTests + { + public class Allocate : MemoryGroupTestsBase + { +#pragma warning disable SA1509 + public static TheoryData AllocateData = + new TheoryData() + { + { default(S5), 22, 4, 4, 1, 4, 4 }, + { default(S5), 22, 4, 7, 2, 4, 3 }, + { default(S5), 22, 4, 8, 2, 4, 4 }, + { default(S5), 22, 4, 21, 6, 4, 1 }, + + // empty: + { default(S5), 22, 0, 0, 1, -1, 0 }, + { default(S5), 22, 4, 0, 1, -1, 0 }, + + { default(S4), 50, 12, 12, 1, 12, 12 }, + { default(S4), 50, 7, 12, 2, 7, 5 }, + { default(S4), 50, 6, 12, 1, 12, 12 }, + { default(S4), 50, 5, 12, 2, 10, 2 }, + { default(S4), 50, 4, 12, 1, 12, 12 }, + { default(S4), 50, 3, 12, 1, 12, 12 }, + { default(S4), 50, 2, 12, 1, 12, 12 }, + { default(S4), 50, 1, 12, 1, 12, 12 }, + + { default(S4), 50, 12, 13, 2, 12, 1 }, + { default(S4), 50, 7, 21, 3, 7, 7 }, + { default(S4), 50, 7, 23, 4, 7, 2 }, + { default(S4), 50, 6, 13, 2, 12, 1 }, + + { default(short), 200, 50, 49, 1, 49, 49 }, + { default(short), 200, 50, 1, 1, 1, 1 }, + { default(byte), 1000, 512, 2047, 4, 512, 511 } + }; + + [Theory] + [MemberData(nameof(AllocateData))] + public void BufferSizesAreCorrect( + T dummy, + int bufferCapacity, + int bufferAlignment, + long totalLength, + int expectedNumberOfBuffers, + int expectedBufferSize, + int expectedSizeOfLastBuffer) + where T : struct + { + this.MemoryAllocator.BufferCapacityInBytes = bufferCapacity; + + // Act: + using var g = MemoryGroup.Allocate(this.MemoryAllocator, totalLength, bufferAlignment); + + // Assert: + Assert.Equal(expectedNumberOfBuffers, g.Count); + + if (expectedBufferSize >= 0) + { + Assert.Equal(expectedBufferSize, g.BufferLength); + } + + if (g.Count == 0) + { + return; + } + + for (int i = 0; i < g.Count - 1; i++) + { + Assert.Equal(g[i].Length, expectedBufferSize); + } + + Assert.Equal(g.Last().Length, expectedSizeOfLastBuffer); + } + + [Fact] + public void WhenBlockAlignmentIsOverCapacity_Throws_InvalidMemoryOperationException() + { + this.MemoryAllocator.BufferCapacityInBytes = 84; // 42 * Int16 + + Assert.Throws(() => + { + MemoryGroup.Allocate(this.MemoryAllocator, 50, 43); + }); + } + + [Theory] + [InlineData(AllocationOptions.None)] + [InlineData(AllocationOptions.Clean)] + public void MemoryAllocatorIsUtilizedCorrectly(AllocationOptions allocationOptions) + { + this.MemoryAllocator.BufferCapacityInBytes = 200; + + HashSet bufferHashes; + + int expectedBlockCount = 5; + using (var g = MemoryGroup.Allocate(this.MemoryAllocator, 500, 100, allocationOptions)) + { + IReadOnlyList allocationLog = this.MemoryAllocator.AllocationLog; + Assert.Equal(expectedBlockCount, allocationLog.Count); + bufferHashes = allocationLog.Select(l => l.HashCodeOfBuffer).ToHashSet(); + Assert.Equal(expectedBlockCount, bufferHashes.Count); + Assert.Equal(0, this.MemoryAllocator.ReturnLog.Count); + + for (int i = 0; i < expectedBlockCount; i++) + { + Assert.Equal(allocationOptions, allocationLog[i].AllocationOptions); + Assert.Equal(100, allocationLog[i].Length); + Assert.Equal(200, allocationLog[i].LengthInBytes); + } + } + + Assert.Equal(expectedBlockCount, this.MemoryAllocator.ReturnLog.Count); + Assert.True(bufferHashes.SetEquals(this.MemoryAllocator.ReturnLog.Select(l => l.HashCodeOfBuffer))); + } + } + } +} diff --git a/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.CopyTo.cs b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.CopyTo.cs new file mode 100644 index 000000000..ab69a3077 --- /dev/null +++ b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.CopyTo.cs @@ -0,0 +1,111 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Memory; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers +{ + public partial class MemoryGroupTests + { + public class CopyTo : MemoryGroupTestsBase + { + public static readonly TheoryData WhenSourceBufferIsShorterOrEqual_Data = + CopyAndTransformData; + + [Theory] + [MemberData(nameof(WhenSourceBufferIsShorterOrEqual_Data))] + public void WhenSourceBufferIsShorterOrEqual(int srcTotal, int srcBufLen, int trgTotal, int trgBufLen) + { + using MemoryGroup src = this.CreateTestGroup(srcTotal, srcBufLen, true); + using MemoryGroup trg = this.CreateTestGroup(trgTotal, trgBufLen, false); + + src.CopyTo(trg); + + int pos = 0; + MemoryGroupIndex i = src.MinIndex(); + MemoryGroupIndex j = trg.MinIndex(); + for (; i < src.MaxIndex(); i += 1, j += 1, pos++) + { + int a = src.GetElementAt(i); + int b = trg.GetElementAt(j); + + Assert.True(a == b, $"Mismatch @ {pos} Expected: {a} Actual: {b}"); + } + } + + [Fact] + public void WhenTargetBufferTooShort_Throws() + { + using MemoryGroup src = this.CreateTestGroup(10, 20, true); + using MemoryGroup trg = this.CreateTestGroup(5, 20, false); + + Assert.Throws(() => src.CopyTo(trg)); + } + + [Theory] + [InlineData(30, 10, 40)] + [InlineData(42, 23, 42)] + [InlineData(1, 3, 10)] + [InlineData(0, 4, 0)] + public void GroupToSpan_Success(long totalLength, int bufferLength, int spanLength) + { + using MemoryGroup src = this.CreateTestGroup(totalLength, bufferLength, true); + var trg = new int[spanLength]; + src.CopyTo(trg); + + int expected = 1; + foreach (int val in trg.AsSpan().Slice(0, (int)totalLength)) + { + Assert.Equal(expected, val); + expected++; + } + } + + [Theory] + [InlineData(20, 7, 19)] + [InlineData(2, 1, 1)] + public void GroupToSpan_OutOfRange(long totalLength, int bufferLength, int spanLength) + { + using MemoryGroup src = this.CreateTestGroup(totalLength, bufferLength, true); + var trg = new int[spanLength]; + Assert.ThrowsAny(() => src.CopyTo(trg)); + } + + [Theory] + [InlineData(30, 35, 10)] + [InlineData(42, 23, 42)] + [InlineData(10, 3, 1)] + [InlineData(0, 3, 0)] + public void SpanToGroup_Success(long totalLength, int bufferLength, int spanLength) + { + var src = new int[spanLength]; + for (int i = 0; i < src.Length; i++) + { + src[i] = i + 1; + } + + using MemoryGroup trg = this.CreateTestGroup(totalLength, bufferLength); + src.AsSpan().CopyTo(trg); + + int position = 0; + for (MemoryGroupIndex i = trg.MinIndex(); position < spanLength; i += 1, position++) + { + int expected = position + 1; + Assert.Equal(expected, trg.GetElementAt(i)); + } + } + + [Theory] + [InlineData(10, 3, 11)] + [InlineData(0, 3, 1)] + public void SpanToGroup_OutOfRange(long totalLength, int bufferLength, int spanLength) + { + var src = new int[spanLength]; + using MemoryGroup trg = this.CreateTestGroup(totalLength, bufferLength, true); + Assert.ThrowsAny(() => src.AsSpan().CopyTo(trg)); + } + } + } +} diff --git a/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.SwapOrCopyContent.cs b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.SwapOrCopyContent.cs new file mode 100644 index 000000000..c10fdc15d --- /dev/null +++ b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.SwapOrCopyContent.cs @@ -0,0 +1,107 @@ +// Copyright (c) Six Labors and contributors. +// 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 a = this.MemoryAllocator.AllocateGroup(100, 50); + using MemoryGroup b = this.MemoryAllocator.AllocateGroup(120, 50); + + Memory a0 = a[0]; + Memory a1 = a[1]; + Memory b0 = b[0]; + Memory b1 = b[1]; + + bool swap = MemoryGroup.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 a = this.MemoryAllocator.AllocateGroup(100, 100); + using MemoryGroup b = this.MemoryAllocator.AllocateGroup(120, 100); + + a[0].Span[42] = 1; + b[0].Span[33] = 2; + MemoryGroupView aView0 = a.View; + MemoryGroupView bView0 = b.View; + + MemoryGroup.SwapOrCopyContent(a, b); + Assert.False(aView0.IsValid); + Assert.False(bView0.IsValid); + Assert.ThrowsAny(() => _ = aView0[0].Span); + Assert.ThrowsAny(() => _ = 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(data); + using var dest = MemoryGroup.Wrap(destOwner.Memory); + + using MemoryGroup source = this.MemoryAllocator.AllocateGroup(21, 30); + + source[0].Span[10] = color; + + // Act: + bool swap = MemoryGroup.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(data); + var dest = MemoryGroup.Wrap(destOwner.Memory); + + using MemoryGroup source = this.MemoryAllocator.AllocateGroup(22, 30); + + source[0].Span[10] = color; + + // Act: + Assert.ThrowsAny(() => MemoryGroup.SwapOrCopyContent(dest, source)); + + Assert.Equal(color, source[0].Span[10]); + Assert.NotEqual(color, dest[0].Span[10]); + } + } + } +} diff --git a/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.View.cs b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.View.cs new file mode 100644 index 000000000..8884037a5 --- /dev/null +++ b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.View.cs @@ -0,0 +1,84 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Memory; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers +{ + public partial class MemoryGroupTests + { + public class View : MemoryGroupTestsBase + { + [Fact] + public void RefersToOwnerGroupContent() + { + using MemoryGroup group = this.CreateTestGroup(240, 80, true); + + MemoryGroupView view = group.View; + Assert.True(view.IsValid); + Assert.Equal(group.Count, view.Count); + Assert.Equal(group.BufferLength, view.BufferLength); + Assert.Equal(group.TotalLength, view.TotalLength); + int cnt = 1; + foreach (Memory memory in view) + { + Span span = memory.Span; + foreach (int t in span) + { + Assert.Equal(cnt, t); + cnt++; + } + } + } + + [Fact] + public void IsInvalidatedOnOwnerGroupDispose() + { + MemoryGroupView view; + using (MemoryGroup group = this.CreateTestGroup(240, 80, true)) + { + view = group.View; + } + + Assert.False(view.IsValid); + + Assert.ThrowsAny(() => + { + _ = view.Count; + }); + + Assert.ThrowsAny(() => + { + _ = view.BufferLength; + }); + + Assert.ThrowsAny(() => + { + _ = view.TotalLength; + }); + + Assert.ThrowsAny(() => + { + _ = view[0]; + }); + } + + [Fact] + public void WhenInvalid_CanNotUseMemberMemory() + { + Memory memory; + using (MemoryGroup group = this.CreateTestGroup(240, 80, true)) + { + memory = group.View[0]; + } + + Assert.ThrowsAny(() => + { + _ = memory.Span; + }); + } + } + } +} diff --git a/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.cs b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.cs new file mode 100644 index 000000000..694c4d32f --- /dev/null +++ b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.cs @@ -0,0 +1,214 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Linq; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers +{ + public partial class MemoryGroupTests : MemoryGroupTestsBase + { + [Fact] + public void IsValid_TrueAfterCreation() + { + using var g = MemoryGroup.Allocate(this.MemoryAllocator, 10, 100); + + Assert.True(g.IsValid); + } + + [Fact] + public void IsValid_FalseAfterDisposal() + { + using var g = MemoryGroup.Allocate(this.MemoryAllocator, 10, 100); + + g.Dispose(); + Assert.False(g.IsValid); + } + +#pragma warning disable SA1509 + private static readonly TheoryData CopyAndTransformData = + new TheoryData() + { + { 20, 10, 20, 10 }, + { 20, 5, 20, 4 }, + { 20, 4, 20, 5 }, + { 18, 6, 20, 5 }, + { 19, 10, 20, 10 }, + { 21, 10, 22, 2 }, + { 1, 5, 5, 4 }, + + { 30, 12, 40, 5 }, + { 30, 5, 40, 12 }, + }; + + public class TransformTo : MemoryGroupTestsBase + { + public static readonly TheoryData WhenSourceBufferIsShorterOrEqual_Data = + CopyAndTransformData; + + [Theory] + [MemberData(nameof(WhenSourceBufferIsShorterOrEqual_Data))] + public void WhenSourceBufferIsShorterOrEqual(int srcTotal, int srcBufLen, int trgTotal, int trgBufLen) + { + using MemoryGroup src = this.CreateTestGroup(srcTotal, srcBufLen, true); + using MemoryGroup trg = this.CreateTestGroup(trgTotal, trgBufLen, false); + + src.TransformTo(trg, MultiplyAllBy2); + + int pos = 0; + MemoryGroupIndex i = src.MinIndex(); + MemoryGroupIndex j = trg.MinIndex(); + for (; i < src.MaxIndex(); i += 1, j += 1, pos++) + { + int a = src.GetElementAt(i); + int b = trg.GetElementAt(j); + + Assert.True(b == 2 * a, $"Mismatch @ {pos} Expected: {a} Actual: {b}"); + } + } + + [Fact] + public void WhenTargetBufferTooShort_Throws() + { + using MemoryGroup src = this.CreateTestGroup(10, 20, true); + using MemoryGroup trg = this.CreateTestGroup(5, 20, false); + + Assert.Throws(() => src.TransformTo(trg, MultiplyAllBy2)); + } + } + + [Theory] + [InlineData(100, 5)] + [InlineData(100, 101)] + public void TransformInplace(int totalLength, int bufferLength) + { + using MemoryGroup src = this.CreateTestGroup(10, 20, true); + + src.TransformInplace(s => MultiplyAllBy2(s, s)); + + int cnt = 1; + for (MemoryGroupIndex i = src.MinIndex(); i < src.MaxIndex(); i += 1) + { + int val = src.GetElementAt(i); + Assert.Equal(expected: cnt * 2, val); + cnt++; + } + } + + [Fact] + public void Wrap() + { + int[] data0 = { 1, 2, 3, 4 }; + int[] data1 = { 5, 6, 7, 8 }; + int[] data2 = { 9, 10 }; + using var mgr0 = new TestMemoryManager(data0); + using var mgr1 = new TestMemoryManager(data1); + + using var group = MemoryGroup.Wrap(mgr0.Memory, mgr1.Memory, data2); + + Assert.Equal(3, group.Count); + Assert.Equal(4, group.BufferLength); + Assert.Equal(10, group.TotalLength); + + Assert.True(group[0].Span.SequenceEqual(data0)); + Assert.True(group[1].Span.SequenceEqual(data1)); + Assert.True(group[2].Span.SequenceEqual(data2)); + } + + public static TheoryData GetBoundedSlice_SuccessData = new TheoryData() + { + { 300, 100, 110, 80 }, + { 300, 100, 100, 100 }, + { 280, 100, 201, 79 }, + { 42, 7, 0, 0 }, + { 42, 7, 0, 1 }, + { 42, 7, 0, 7 }, + { 42, 9, 9, 9 }, + }; + + [Theory] + [MemberData(nameof(GetBoundedSlice_SuccessData))] + public void GetBoundedSlice_WhenArgsAreCorrect(long totalLength, int bufferLength, long start, int length) + { + using MemoryGroup group = this.CreateTestGroup(totalLength, bufferLength, true); + + Memory slice = group.GetBoundedSlice(start, length); + + Assert.Equal(length, slice.Length); + + int expected = (int)start + 1; + foreach (int val in slice.Span) + { + Assert.Equal(expected, val); + expected++; + } + } + + public static TheoryData GetBoundedSlice_ErrorData = new TheoryData() + { + { 300, 100, 110, 91 }, + { 42, 7, 0, 8 }, + { 42, 7, 1, 7 }, + { 42, 7, 1, 30 }, + }; + + [Theory] + [MemberData(nameof(GetBoundedSlice_ErrorData))] + public void GetBoundedSlice_WhenOverlapsBuffers_Throws(long totalLength, int bufferLength, long start, int length) + { + using MemoryGroup group = this.CreateTestGroup(totalLength, bufferLength, true); + Assert.ThrowsAny(() => group.GetBoundedSlice(start, length)); + } + + [Fact] + public void Fill() + { + using MemoryGroup group = this.CreateTestGroup(100, 10, true); + group.Fill(42); + + int[] expectedRow = Enumerable.Repeat(42, 10).ToArray(); + foreach (Memory memory in group) + { + Assert.True(memory.Span.SequenceEqual(expectedRow)); + } + } + + [Fact] + public void Clear() + { + using MemoryGroup group = this.CreateTestGroup(100, 10, true); + group.Clear(); + + var expectedRow = new int[10]; + foreach (Memory memory in group) + { + Assert.True(memory.Span.SequenceEqual(expectedRow)); + } + } + + private static void MultiplyAllBy2(ReadOnlySpan source, Span target) + { + Assert.Equal(source.Length, target.Length); + for (int k = 0; k < source.Length; k++) + { + target[k] = source[k] * 2; + } + } + + [StructLayout(LayoutKind.Sequential, Size = 5)] + private struct S5 + { + public override string ToString() => "S5"; + } + + [StructLayout(LayoutKind.Sequential, Size = 4)] + private struct S4 + { + public override string ToString() => "S4"; + } + } +} diff --git a/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTestsBase.cs b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTestsBase.cs new file mode 100644 index 000000000..8dd28653c --- /dev/null +++ b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTestsBase.cs @@ -0,0 +1,35 @@ +// Copyright (c) Six Labors and contributors. +// Licensed under the Apache License, Version 2.0. + +using SixLabors.ImageSharp.Memory; + +namespace SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers +{ + public abstract class MemoryGroupTestsBase + { + internal readonly TestMemoryAllocator MemoryAllocator = new TestMemoryAllocator(); + + /// + /// Create a group, either uninitialized or filled with incrementing numbers starting with 1. + /// + internal MemoryGroup CreateTestGroup(long totalLength, int bufferLength, bool fillSequence = false) + { + this.MemoryAllocator.BufferCapacityInBytes = bufferLength * sizeof(int); + var g = MemoryGroup.Allocate(this.MemoryAllocator, totalLength, bufferLength); + + if (!fillSequence) + { + return g; + } + + int j = 1; + for (MemoryGroupIndex i = g.MinIndex(); i < g.MaxIndex(); i += 1) + { + g.SetElementAt(i, j); + j++; + } + + return g; + } + } +} diff --git a/tests/ImageSharp.Tests/Memory/MemorySourceTests.cs b/tests/ImageSharp.Tests/Memory/MemorySourceTests.cs deleted file mode 100644 index d0f8c6f91..000000000 --- a/tests/ImageSharp.Tests/Memory/MemorySourceTests.cs +++ /dev/null @@ -1,159 +0,0 @@ -// Copyright (c) Six Labors and contributors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.Buffers; -using SixLabors.ImageSharp.Memory; -using SixLabors.ImageSharp.PixelFormats; -using Xunit; - -// ReSharper disable InconsistentNaming -namespace SixLabors.ImageSharp.Tests.Memory -{ - public class MemorySourceTests - { - public class Construction - { - [Theory] - [InlineData(false)] - [InlineData(true)] - public void InitializeAsOwner(bool isInternalMemorySource) - { - var data = new Rgba32[21]; - var mmg = new TestMemoryManager(data); - - var a = new MemorySource(mmg, isInternalMemorySource); - - Assert.Equal(mmg, a.MemoryOwner); - Assert.Equal(mmg.Memory, a.Memory); - Assert.Equal(isInternalMemorySource, a.HasSwappableContents); - } - - [Fact] - public void InitializeAsObserver_MemoryOwner_IsNull() - { - var data = new Rgba32[21]; - var mmg = new TestMemoryManager(data); - - var a = new MemorySource(mmg.Memory); - - Assert.Null(a.MemoryOwner); - Assert.Equal(mmg.Memory, a.Memory); - Assert.False(a.HasSwappableContents); - } - } - - public class Dispose - { - [Theory] - [InlineData(false)] - [InlineData(true)] - public void WhenOwnershipIsTransferred_ShouldDisposeMemoryOwner(bool isInternalMemorySource) - { - var mmg = new TestMemoryManager(new int[10]); - var bmg = new MemorySource(mmg, isInternalMemorySource); - - bmg.Dispose(); - Assert.True(mmg.IsDisposed); - } - - [Fact] - public void WhenMemoryObserver_ShouldNotDisposeAnything() - { - var mmg = new TestMemoryManager(new int[10]); - var bmg = new MemorySource(mmg.Memory); - - bmg.Dispose(); - Assert.False(mmg.IsDisposed); - } - } - - public class SwapOrCopyContent - { - private MemoryAllocator MemoryAllocator { get; } = new TestMemoryAllocator(); - - private MemorySource AllocateMemorySource(int length, AllocationOptions options = AllocationOptions.None) - where T : struct - { - IMemoryOwner owner = this.MemoryAllocator.Allocate(length, options); - return new MemorySource(owner, true); - } - - [Fact] - public void WhenBothAreMemoryOwners_ShouldSwap() - { - MemorySource a = this.AllocateMemorySource(13); - MemorySource b = this.AllocateMemorySource(17); - - IMemoryOwner aa = a.MemoryOwner; - IMemoryOwner bb = b.MemoryOwner; - - Memory aaa = a.Memory; - Memory bbb = b.Memory; - - MemorySource.SwapOrCopyContent(ref a, ref b); - - Assert.Equal(bb, a.MemoryOwner); - Assert.Equal(aa, b.MemoryOwner); - - Assert.Equal(bbb, a.Memory); - Assert.Equal(aaa, b.Memory); - Assert.NotEqual(a.Memory, b.Memory); - } - - [Theory] - [InlineData(false, false)] - [InlineData(true, true)] - [InlineData(true, false)] - public void WhenDestIsNotMemoryOwner_SameSize_ShouldCopy(bool sourceIsOwner, bool isInternalMemorySource) - { - var data = new Rgba32[21]; - var color = new Rgba32(1, 2, 3, 4); - - var destOwner = new TestMemoryManager(data); - var dest = new MemorySource(destOwner.Memory); - - IMemoryOwner sourceOwner = this.MemoryAllocator.Allocate(21); - - MemorySource source = sourceIsOwner - ? new MemorySource(sourceOwner, isInternalMemorySource) - : new MemorySource(sourceOwner.Memory); - - sourceOwner.Memory.Span[10] = color; - - // Act: - MemorySource.SwapOrCopyContent(ref dest, ref source); - - // Assert: - Assert.Equal(color, dest.Memory.Span[10]); - Assert.NotEqual(sourceOwner, dest.MemoryOwner); - Assert.NotEqual(destOwner, source.MemoryOwner); - } - - [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); - - var destOwner = new TestMemoryManager(data); - var dest = new MemorySource(destOwner.Memory); - - IMemoryOwner sourceOwner = this.MemoryAllocator.Allocate(22); - - MemorySource source = sourceIsOwner - ? new MemorySource(sourceOwner, true) - : new MemorySource(sourceOwner.Memory); - sourceOwner.Memory.Span[10] = color; - - // Act: - Assert.ThrowsAny(() => MemorySource.SwapOrCopyContent(ref dest, ref source)); - - Assert.Equal(color, source.Memory.Span[10]); - Assert.NotEqual(color, dest.Memory.Span[10]); - } - } - } -} diff --git a/tests/ImageSharp.Tests/Processing/Processors/Convolution/BokehBlurTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Convolution/BokehBlurTest.cs index fcd9eb3cd..2d5971124 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Convolution/BokehBlurTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Convolution/BokehBlurTest.cs @@ -196,5 +196,13 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Convolution .Invoke(RunTest, BasicSerializer.Serialize(provider), BasicSerializer.Serialize(value)) .Dispose(); } + + [Theory] + [WithTestPatternImages(100, 300, PixelTypes.Bgr24)] + public void WorksWithDiscoBuffers(TestImageProvider provider) + where TPixel : struct, IPixel + { + provider.RunBufferCapacityLimitProcessorTest(41, c => c.BokehBlur()); + } } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs index a1f34856e..cfa733423 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Convolution/DetectEdgesTest.cs @@ -103,5 +103,16 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Convolution image.CompareToReferenceOutput(ValidatorComparer, provider); } } + + [Theory] + [WithFile(Tests.TestImages.Png.Bike, nameof(DetectEdgesFilters), PixelTypes.Rgba32)] + public void WorksWithDiscoBuffers(TestImageProvider provider, EdgeDetectionOperators detector) + where TPixel : struct, IPixel + { + provider.RunBufferCapacityLimitProcessorTest( + 41, + c => c.DetectEdges(detector), + detector); + } } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs index 86f982118..fa3e3637f 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs @@ -32,7 +32,7 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization }; public static readonly TheoryData OrderedDitherers - = new TheoryData + = new TheoryData { { KnownDitherings.Bayer2x2, nameof(KnownDitherings.Bayer2x2) }, { KnownDitherings.Bayer4x4, nameof(KnownDitherings.Bayer4x4) }, @@ -152,5 +152,26 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Binarization comparer: ValidatorComparer, appendPixelTypeToFileName: false); } + + [Theory] + [WithFile(TestImages.Png.Bike, PixelTypes.Rgba32, nameof(OrderedDither.Ordered3x3))] + [WithFile(TestImages.Png.Bike, PixelTypes.Rgba32, nameof(ErrorDither.FloydSteinberg))] + public void CommonDitherers_WorkWithDiscoBuffers( + TestImageProvider provider, + string name) + where TPixel : struct, IPixel + { + IDither dither = TestUtils.GetDither(name); + if (SkipAllDitherTests) + { + return; + } + + provider.RunBufferCapacityLimitProcessorTest( + 41, + c => c.Dither(dither), + name, + ImageComparer.TolerantPercentage(0.001f)); + } } } diff --git a/tests/ImageSharp.Tests/Processing/Processors/Filters/FilterTest.cs b/tests/ImageSharp.Tests/Processing/Processors/Filters/FilterTest.cs index a6d7ba451..9b5a5bfd2 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Filters/FilterTest.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Filters/FilterTest.cs @@ -37,6 +37,16 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Filters provider.RunRectangleConstrainedValidatingProcessorTest((x, b) => x.Filter(m, b), comparer: ValidatorComparer); } + [Theory] + [WithTestPatternImages(70, 120, PixelTypes.Rgba32)] + public void FilterProcessor_WorksWithDiscoBuffers(TestImageProvider provider) + where TPixel : struct, IPixel + { + ColorMatrix m = CreateCombinedTestFilterMatrix(); + + provider.RunBufferCapacityLimitProcessorTest(37, c => c.Filter(m)); + } + private static ColorMatrix CreateCombinedTestFilterMatrix() { ColorMatrix brightness = KnownFilterMatrices.CreateBrightnessFilter(0.9F); diff --git a/tests/ImageSharp.Tests/Processing/Processors/Overlays/OverlayTestBase.cs b/tests/ImageSharp.Tests/Processing/Processors/Overlays/OverlayTestBase.cs index 0c09b6872..d959fdf67 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Overlays/OverlayTestBase.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Overlays/OverlayTestBase.cs @@ -54,6 +54,14 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Overlays provider.RunRectangleConstrainedValidatingProcessorTest(this.Apply); } + [Theory] + [WithTestPatternImages(70, 120, PixelTypes.Rgba32)] + public void WorksWithDiscoBuffers(TestImageProvider provider) + where TPixel : struct, IPixel + { + provider.RunBufferCapacityLimitProcessorTest(37, c => this.Apply(c, Color.DarkRed)); + } + protected abstract void Apply(IImageProcessingContext ctx, Color color); protected abstract void Apply(IImageProcessingContext ctx, float radiusX, float radiusY); diff --git a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/AffineTransformTests.cs similarity index 94% rename from tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs rename to tests/ImageSharp.Tests/Processing/Processors/Transforms/AffineTransformTests.cs index 399f1665f..9a4e7c618 100644 --- a/tests/ImageSharp.Tests/Processing/Transforms/AffineTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/AffineTransformTests.cs @@ -213,6 +213,19 @@ namespace SixLabors.ImageSharp.Tests.Processing.Transforms } } + [Theory] + [WithTestPatternImages(100, 100, PixelTypes.Rgba32, 21)] + public void WorksWithDiscoBuffers(TestImageProvider provider, int bufferCapacityInPixelRows) + where TPixel : struct, IPixel + { + AffineTransformBuilder builder = new AffineTransformBuilder() + .AppendRotationDegrees(50) + .AppendScale(new SizeF(.6F, .6F)); + provider.RunBufferCapacityLimitProcessorTest( + bufferCapacityInPixelRows, + c => c.Transform(builder)); + } + private static IResampler GetResampler(string name) { PropertyInfo property = typeof(KnownResamplers).GetTypeInfo().GetProperty(name); diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs index fa2396251..2cbffef47 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs @@ -153,6 +153,39 @@ namespace SixLabors.ImageSharp.Tests.Processing.Processors.Transforms } } + [Theory] + [WithTestPatternImages(100, 100, PixelTypes.Rgba32, 100, 100)] + [WithTestPatternImages(200, 200, PixelTypes.Rgba32, 31, 73)] + [WithTestPatternImages(200, 200, PixelTypes.Rgba32, 73, 31)] + [WithTestPatternImages(200, 193, PixelTypes.Rgba32, 13, 17)] + [WithTestPatternImages(200, 193, PixelTypes.Rgba32, 79, 23)] + [WithTestPatternImages(200, 503, PixelTypes.Rgba32, 61, 33)] + public void WorksWithDiscoBuffers( + TestImageProvider provider, + int workingBufferLimitInRows, + int bufferCapacityInRows) + where TPixel : struct, IPixel + { + using Image expected = provider.GetImage(); + int width = expected.Width; + Size destSize = expected.Size() / 4; + expected.Mutate(c => c.Resize(destSize, KnownResamplers.Bicubic, false)); + + // Replace configuration: + provider.Configuration = Configuration.CreateDefaultInstance(); + + // Note: when AllocatorCapacityInBytes < WorkingBufferSizeHintInBytes, + // ResizeProcessor is expected to use the minimum of the two values, when establishing the working buffer. + provider.LimitAllocatorBufferCapacity().InBytes(width * bufferCapacityInRows * SizeOfVector4); + provider.Configuration.WorkingBufferSizeHintInBytes = width * workingBufferLimitInRows * SizeOfVector4; + + using Image actual = provider.GetImage(); + actual.Mutate(c => c.Resize(destSize, KnownResamplers.Bicubic, false)); + actual.DebugSave(provider, $"{workingBufferLimitInRows}-{bufferCapacityInRows}"); + + ImageComparer.Exact.VerifySimilarity(expected, actual); + } + [Theory] [WithTestPatternImages(100, 100, DefaultPixelType)] public void Resize_Compand(TestImageProvider provider) diff --git a/tests/ImageSharp.Tests/TestImages.cs b/tests/ImageSharp.Tests/TestImages.cs index fb3e974bb..b1cfec218 100644 --- a/tests/ImageSharp.Tests/TestImages.cs +++ b/tests/ImageSharp.Tests/TestImages.cs @@ -143,7 +143,7 @@ namespace SixLabors.ImageSharp.Tests public const string Floorplan = "Jpg/baseline/Floorplan.jpg"; public const string Calliphora = "Jpg/baseline/Calliphora.jpg"; public const string Ycck = "Jpg/baseline/ycck.jpg"; - public const string Turtle = "Jpg/baseline/turtle.jpg"; + public const string Turtle420 = "Jpg/baseline/turtle.jpg"; public const string GammaDalaiLamaGray = "Jpg/baseline/gamma_dalai_lama_gray.jpg"; public const string Hiyamugi = "Jpg/baseline/Hiyamugi.jpg"; public const string Snake = "Jpg/baseline/Snake.jpg"; @@ -162,7 +162,7 @@ namespace SixLabors.ImageSharp.Tests public static readonly string[] All = { Cmyk, Ycck, Exif, Floorplan, - Calliphora, Turtle, GammaDalaiLamaGray, + Calliphora, Turtle420, GammaDalaiLamaGray, Hiyamugi, Jpeg400, Jpeg420Exif, Jpeg444, Ratio1x1, Testorig12bit, YcckSubsample1222 }; diff --git a/tests/ImageSharp.Tests/TestUtilities/Attributes/WithTestPatternImagesAttribute.cs b/tests/ImageSharp.Tests/TestUtilities/Attributes/WithTestPatternImagesAttribute.cs index 7c659c64f..0f00f1d86 100644 --- a/tests/ImageSharp.Tests/TestUtilities/Attributes/WithTestPatternImagesAttribute.cs +++ b/tests/ImageSharp.Tests/TestUtilities/Attributes/WithTestPatternImagesAttribute.cs @@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.Tests public class WithTestPatternImagesAttribute : ImageDataAttributeBase { /// - /// Triggers passing an that produces a test pattern image of size width * height + /// Initializes a new instance of the class. /// /// The required width /// The required height @@ -25,7 +25,7 @@ namespace SixLabors.ImageSharp.Tests } /// - /// Triggers passing an that produces a test pattern image of size width * height + /// Initializes a new instance of the class. /// /// The member data to apply to theories /// The required width @@ -53,4 +53,4 @@ namespace SixLabors.ImageSharp.Tests protected override object[] GetFactoryMethodArgs(MethodInfo testMethod, Type factoryType) => new object[] { this.Width, this.Height }; } -} \ No newline at end of file +} diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs index 9100e26e8..1025ed9a1 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs @@ -11,16 +11,26 @@ namespace SixLabors.ImageSharp.Tests { public abstract partial class TestImageProvider : IXunitSerializable { + public virtual TPixel GetExpectedBasicTestPatternPixelAt(int x, int y) + { + throw new NotSupportedException("GetExpectedBasicTestPatternPixelAt(x,y) only works with BasicTestPattern"); + } + private class BasicTestPatternProvider : BlankProvider { + private static readonly TPixel TopLeftColor = Color.Red.ToPixel(); + private static readonly TPixel TopRightColor = Color.Green.ToPixel(); + private static readonly TPixel BottomLeftColor = Color.Blue.ToPixel(); + + // Transparent purple: + private static readonly TPixel BottomRightColor = GetBottomRightColor(); + public BasicTestPatternProvider(int width, int height) : base(width, height) { } - /// - /// This parameterless constructor is needed for xUnit deserialization - /// + // This parameterless constructor is needed for xUnit deserialization public BasicTestPatternProvider() { } @@ -31,14 +41,6 @@ namespace SixLabors.ImageSharp.Tests { var result = new Image(this.Configuration, this.Width, this.Height); - TPixel topLeftColor = Color.Red.ToPixel(); - TPixel topRightColor = Color.Green.ToPixel(); - TPixel bottomLeftColor = Color.Blue.ToPixel(); - - // Transparent purple: - TPixel bottomRightColor = default; - bottomRightColor.FromVector4(new Vector4(1f, 0f, 1f, 0.5f)); - int midY = this.Height / 2; int midX = this.Width / 2; @@ -46,20 +48,42 @@ namespace SixLabors.ImageSharp.Tests { Span row = result.GetPixelRowSpan(y); - row.Slice(0, midX).Fill(topLeftColor); - row.Slice(midX, this.Width - midX).Fill(topRightColor); + row.Slice(0, midX).Fill(TopLeftColor); + row.Slice(midX, this.Width - midX).Fill(TopRightColor); } for (int y = midY; y < this.Height; y++) { Span row = result.GetPixelRowSpan(y); - row.Slice(0, midX).Fill(bottomLeftColor); - row.Slice(midX, this.Width - midX).Fill(bottomRightColor); + row.Slice(0, midX).Fill(BottomLeftColor); + row.Slice(midX, this.Width - midX).Fill(BottomRightColor); } return result; } + + public override TPixel GetExpectedBasicTestPatternPixelAt(int x, int y) + { + int midY = this.Height / 2; + int midX = this.Width / 2; + + if (y < midY) + { + return x < midX ? TopLeftColor : TopRightColor; + } + else + { + return x < midX ? BottomLeftColor : BottomRightColor; + } + } + + private static TPixel GetBottomRightColor() + { + TPixel bottomRightColor = default; + bottomRightColor.FromVector4(new Vector4(1f, 0f, 1f, 0.5f)); + return bottomRightColor; + } } } -} \ No newline at end of file +} diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs index 4dd6ab655..d94e21609 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs @@ -22,14 +22,18 @@ namespace SixLabors.ImageSharp.Tests // are shared between PixelTypes.Color & PixelTypes.Rgba32 private class Key : IEquatable { - private Tuple commonValues; + private readonly Tuple commonValues; - private Dictionary decoderParameters; + private readonly Dictionary decoderParameters; - public Key(PixelTypes pixelType, string filePath, IImageDecoder customDecoder) + public Key(PixelTypes pixelType, string filePath, int allocatorBufferCapacity, IImageDecoder customDecoder) { Type customType = customDecoder?.GetType(); - this.commonValues = new Tuple(pixelType, filePath, customType); + this.commonValues = new Tuple( + pixelType, + filePath, + customType, + allocatorBufferCapacity); this.decoderParameters = GetDecoderParameters(customDecoder); } @@ -152,7 +156,8 @@ namespace SixLabors.ImageSharp.Tests return this.LoadImage(decoder); } - var key = new Key(this.PixelType, this.FilePath, decoder); + int bufferCapacity = this.Configuration.MemoryAllocator.GetBufferCapacityInBytes(); + var key = new Key(this.PixelType, this.FilePath, bufferCapacity, decoder); Image cachedImage = Cache.GetOrAdd(key, _ => this.LoadImage(decoder)); diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/SolidProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/SolidProvider.cs index 85506a9de..f8aa04827 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/SolidProvider.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/SolidProvider.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using Xunit.Abstractions; @@ -53,7 +54,7 @@ namespace SixLabors.ImageSharp.Tests Image image = base.GetImage(); Color color = new Rgba32(this.r, this.g, this.b, this.a); - image.GetPixelSpan().Fill(color.ToPixel()); + image.GetRootFramePixelBuffer().FastMemoryGroup.Fill(color.ToPixel()); return image; } diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs index 58afd48a7..576ae1d9f 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/MagickReferenceDecoder.cs @@ -3,12 +3,14 @@ using System; using System.IO; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using ImageMagick; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs @@ -17,45 +19,64 @@ namespace SixLabors.ImageSharp.Tests.TestUtilities.ReferenceCodecs { public static MagickReferenceDecoder Instance { get; } = new MagickReferenceDecoder(); + private static void FromRgba32Bytes(Configuration configuration, Span rgbaBytes, IMemoryGroup destinationGroup) + where TPixel : struct, IPixel + { + foreach (Memory m in destinationGroup) + { + Span destBuffer = m.Span; + PixelOperations.Instance.FromRgba32Bytes( + configuration, + rgbaBytes, + destBuffer, + destBuffer.Length); + rgbaBytes = rgbaBytes.Slice(destBuffer.Length * 4); + } + } + + private static void FromRgba64Bytes(Configuration configuration, Span rgbaBytes, IMemoryGroup destinationGroup) + where TPixel : struct, IPixel + { + foreach (Memory m in destinationGroup) + { + Span destBuffer = m.Span; + PixelOperations.Instance.FromRgba64Bytes( + configuration, + rgbaBytes, + destBuffer, + destBuffer.Length); + rgbaBytes = rgbaBytes.Slice(destBuffer.Length * 8); + } + } + public Image Decode(Configuration configuration, Stream stream) where TPixel : struct, IPixel { - using (var magickImage = new MagickImage(stream)) + using var magickImage = new MagickImage(stream); + var result = new Image(configuration, magickImage.Width, magickImage.Height); + MemoryGroup resultPixels = result.GetRootFramePixelBuffer().FastMemoryGroup; + + using (IPixelCollection pixels = magickImage.GetPixelsUnsafe()) { - var result = new Image(configuration, magickImage.Width, magickImage.Height); - Span resultPixels = result.GetPixelSpan(); + if (magickImage.Depth == 8) + { + byte[] data = pixels.ToByteArray(PixelMapping.RGBA); - using (IPixelCollection pixels = magickImage.GetPixelsUnsafe()) + FromRgba32Bytes(configuration, data, resultPixels); + } + else if (magickImage.Depth == 16) { - if (magickImage.Depth == 8) - { - byte[] data = pixels.ToByteArray(PixelMapping.RGBA); - - PixelOperations.Instance.FromRgba32Bytes( - configuration, - data, - resultPixels, - resultPixels.Length); - } - else if (magickImage.Depth == 16) - { - ushort[] data = pixels.ToShortArray(PixelMapping.RGBA); - Span bytes = MemoryMarshal.Cast(data.AsSpan()); - - PixelOperations.Instance.FromRgba64Bytes( - configuration, - bytes, - resultPixels, - resultPixels.Length); - } - else - { - throw new InvalidOperationException(); - } + ushort[] data = pixels.ToShortArray(PixelMapping.RGBA); + Span bytes = MemoryMarshal.Cast(data.AsSpan()); + FromRgba64Bytes(configuration, bytes, resultPixels); + } + else + { + throw new InvalidOperationException(); } - - return result; } + + return result; } public Image Decode(Configuration configuration, Stream stream) => this.Decode(configuration, stream); diff --git a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs index 92e0bf85a..502a5bf46 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs @@ -5,7 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Numerics; - +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Memory; @@ -656,22 +656,30 @@ namespace SixLabors.ImageSharp.Tests testOutputDetails, appendPixelTypeToFileName); - referenceDecoder = referenceDecoder ?? TestEnvironment.GetReferenceDecoder(actualOutputFile); + referenceDecoder ??= TestEnvironment.GetReferenceDecoder(actualOutputFile); - using (var actualImage = Image.Load(actualOutputFile, referenceDecoder)) + using (var encodedImage = Image.Load(actualOutputFile, referenceDecoder)) { ImageComparer comparer = customComparer ?? ImageComparer.Exact; - comparer.VerifySimilarity(actualImage, image); + comparer.VerifySimilarity(encodedImage, image); } } + internal static AllocatorBufferCapacityConfigurator LimitAllocatorBufferCapacity( + this TestImageProvider provider) + where TPixel : struct, IPixel + { + var allocator = (ArrayPoolMemoryAllocator)provider.Configuration.MemoryAllocator; + return new AllocatorBufferCapacityConfigurator(allocator, Unsafe.SizeOf()); + } + internal static Image ToGrayscaleImage(this Buffer2D buffer, float scale) { var image = new Image(buffer.Width, buffer.Height); Span pixels = image.Frames.RootFrame.GetPixelSpan(); - Span bufferSpan = buffer.GetSpan(); + Span bufferSpan = buffer.GetSingleSpan(); for (int i = 0; i < bufferSpan.Length; i++) { @@ -742,4 +750,30 @@ namespace SixLabors.ImageSharp.Tests } } } + + internal class AllocatorBufferCapacityConfigurator + { + private readonly ArrayPoolMemoryAllocator allocator; + private readonly int pixelSizeInBytes; + + public AllocatorBufferCapacityConfigurator(ArrayPoolMemoryAllocator allocator, int pixelSizeInBytes) + { + this.allocator = allocator; + this.pixelSizeInBytes = pixelSizeInBytes; + } + + public void InBytes(int totalBytes) => this.allocator.BufferCapacityInBytes = totalBytes; + + public void InPixels(int totalPixels) => this.InBytes(totalPixels * this.pixelSizeInBytes); + + /// + /// Set the maximum buffer capacity to bytesSqrt^2 bytes. + /// + public void InBytesSqrt(int bytesSqrt) => this.InBytes(bytesSqrt * bytesSqrt); + + /// + /// Set the maximum buffer capacity to pixelsSqrt^2 x sizeof(TPixel) bytes. + /// + public void InPixelsSqrt(int pixelsSqrt) => this.InPixels(pixelsSqrt * pixelsSqrt); + } } diff --git a/tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs b/tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs index 2f7444c3a..dd928cb75 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs @@ -13,7 +13,8 @@ namespace SixLabors.ImageSharp.Tests.Memory { internal class TestMemoryAllocator : MemoryAllocator { - private List allocationLog = new List(); + private readonly List allocationLog = new List(); + private readonly List returnLog = new List(); public TestMemoryAllocator(byte dirtyValue = 42) { @@ -25,25 +26,31 @@ namespace SixLabors.ImageSharp.Tests.Memory /// public byte DirtyValue { get; } - public IList AllocationLog => this.allocationLog; + public int BufferCapacityInBytes { get; set; } = int.MaxValue; + + public IReadOnlyList AllocationLog => this.allocationLog; + + public IReadOnlyList ReturnLog => this.returnLog; + + protected internal override int GetBufferCapacityInBytes() => this.BufferCapacityInBytes; public override IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None) { T[] array = this.AllocateArray(length, options); - return new BasicArrayBuffer(array, length); + return new BasicArrayBuffer(array, length, this); } public override IManagedByteBuffer AllocateManagedByteBuffer(int length, AllocationOptions options = AllocationOptions.None) { byte[] array = this.AllocateArray(length, options); - return new ManagedByteBuffer(array); + return new ManagedByteBuffer(array, this); } private T[] AllocateArray(int length, AllocationOptions options) where T : struct { - this.allocationLog.Add(AllocationRequest.Create(options, length)); var array = new T[length + 42]; + this.allocationLog.Add(AllocationRequest.Create(options, length, array)); if (options == AllocationOptions.None) { @@ -54,25 +61,32 @@ namespace SixLabors.ImageSharp.Tests.Memory return array; } + private void Return(BasicArrayBuffer buffer) + where T : struct + { + this.returnLog.Add(new ReturnRequest(buffer.Array.GetHashCode())); + } + public struct AllocationRequest { - private AllocationRequest(Type elementType, AllocationOptions allocationOptions, int length, int lengthInBytes) + private AllocationRequest(Type elementType, AllocationOptions allocationOptions, int length, int lengthInBytes, int hashCodeOfBuffer) { this.ElementType = elementType; this.AllocationOptions = allocationOptions; this.Length = length; this.LengthInBytes = lengthInBytes; + this.HashCodeOfBuffer = hashCodeOfBuffer; if (elementType == typeof(Vector4)) { } } - public static AllocationRequest Create(AllocationOptions allocationOptions, int length) + public static AllocationRequest Create(AllocationOptions allocationOptions, int length, T[] buffer) { Type type = typeof(T); int elementSize = Marshal.SizeOf(type); - return new AllocationRequest(type, allocationOptions, length, length * elementSize); + return new AllocationRequest(type, allocationOptions, length, length * elementSize, buffer.GetHashCode()); } public Type ElementType { get; } @@ -82,6 +96,18 @@ namespace SixLabors.ImageSharp.Tests.Memory public int Length { get; } public int LengthInBytes { get; } + + public int HashCodeOfBuffer { get; } + } + + public struct ReturnRequest + { + public ReturnRequest(int hashCodeOfBuffer) + { + this.HashCodeOfBuffer = hashCodeOfBuffer; + } + + public int HashCodeOfBuffer { get; } } /// @@ -90,36 +116,29 @@ namespace SixLabors.ImageSharp.Tests.Memory private class BasicArrayBuffer : MemoryManager where T : struct { + private readonly TestMemoryAllocator allocator; private GCHandle pinHandle; - /// - /// Initializes a new instance of the class - /// - /// The array - /// The length of the buffer - public BasicArrayBuffer(T[] array, int length) + public BasicArrayBuffer(T[] array, int length, TestMemoryAllocator allocator) { + this.allocator = allocator; DebugGuard.MustBeLessThanOrEqualTo(length, array.Length, nameof(length)); this.Array = array; this.Length = length; } - /// - /// Initializes a new instance of the class - /// - /// The array - public BasicArrayBuffer(T[] array) - : this(array, array.Length) + public BasicArrayBuffer(T[] array, TestMemoryAllocator allocator) + : this(array, array.Length, allocator) { } /// - /// Gets the array + /// Gets the array. /// public T[] Array { get; } /// - /// Gets the length + /// Gets the length. /// public int Length { get; } @@ -145,13 +164,17 @@ namespace SixLabors.ImageSharp.Tests.Memory /// protected override void Dispose(bool disposing) { + if (disposing) + { + this.allocator.Return(this); + } } } private class ManagedByteBuffer : BasicArrayBuffer, IManagedByteBuffer { - public ManagedByteBuffer(byte[] array) - : base(array) + public ManagedByteBuffer(byte[] array, TestMemoryAllocator allocator) + : base(array, allocator) { } } diff --git a/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs b/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs index 089e5805e..b5bfec17f 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs @@ -6,10 +6,12 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Processing.Processors.Dithering; using SixLabors.ImageSharp.Processing.Processors.Transforms; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; @@ -150,6 +152,28 @@ namespace SixLabors.ImageSharp.Tests where TPixel : struct, IPixel => GetColorByName(colorName).ToPixel(); + internal static void RunBufferCapacityLimitProcessorTest( + this TestImageProvider provider, + int bufferCapacityInPixelRows, + Action process, + object testOutputDetails = null, + ImageComparer comparer = null) + where TPixel : struct, IPixel + { + comparer??= ImageComparer.Exact; + using Image expected = provider.GetImage(); + int width = expected.Width; + expected.Mutate(process); + + var allocator = ArrayPoolMemoryAllocator.CreateDefault(); + provider.Configuration.MemoryAllocator = allocator; + allocator.BufferCapacityInBytes = bufferCapacityInPixelRows * width * Unsafe.SizeOf(); + + using Image actual = provider.GetImage(); + actual.Mutate(process); + comparer.VerifySimilarity(expected, actual); + } + /// /// Utility for testing image processor extension methods: /// 1. Run a processor defined by 'process' @@ -342,6 +366,18 @@ namespace SixLabors.ImageSharp.Tests return (IResampler)property.GetValue(null); } + public static IDither GetDither(string name) + { + PropertyInfo property = typeof(KnownDitherings).GetTypeInfo().GetProperty(name); + + if (property is null) + { + throw new Exception($"No dither named '{name}"); + } + + return (IDither)property.GetValue(null); + } + public static string[] GetAllResamplerNames(bool includeNearestNeighbour = true) { return typeof(KnownResamplers).GetProperties(BindingFlags.Public | BindingFlags.Static) diff --git a/tests/Images/External b/tests/Images/External index 2d1505d70..f9b4bfe42 160000 --- a/tests/Images/External +++ b/tests/Images/External @@ -1 +1 @@ -Subproject commit 2d1505d7087d91cd83d0cda409aee213de7841ab +Subproject commit f9b4bfe42cacb3eefab02ada92ac771a9b93c080 diff --git a/tests/Images/Input/Tga/targa.png b/tests/Images/Input/Tga/targa.png deleted file mode 100644 index c18cf7e23..000000000 Binary files a/tests/Images/Input/Tga/targa.png and /dev/null differ