diff --git a/.gitattributes b/.gitattributes index 70ced69033..355b64dce1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -87,7 +87,6 @@ *.eot binary *.exe binary *.otf binary -*.pbm binary *.pdf binary *.ppt binary *.pptx binary @@ -95,7 +94,6 @@ *.snk binary *.ttc binary *.ttf binary -*.wbmp binary *.woff binary *.woff2 binary *.xls binary @@ -126,3 +124,9 @@ *.dds filter=lfs diff=lfs merge=lfs -text *.ktx filter=lfs diff=lfs merge=lfs -text *.ktx2 filter=lfs diff=lfs merge=lfs -text +*.pam filter=lfs diff=lfs merge=lfs -text +*.pbm filter=lfs diff=lfs merge=lfs -text +*.pgm filter=lfs diff=lfs merge=lfs -text +*.ppm filter=lfs diff=lfs merge=lfs -text +*.pnm filter=lfs diff=lfs merge=lfs -text +*.wbmp filter=lfs diff=lfs merge=lfs -text diff --git a/Directory.Build.props b/Directory.Build.props index 3899ce939f..26b3cc5afc 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -13,6 +13,9 @@ $(MSBuildThisFileDirectory) + + + $(DefineConstants);DEBUG @@ -30,5 +33,4 @@ true - diff --git a/shared-infrastructure b/shared-infrastructure index a042aba176..59ce17f5a4 160000 --- a/shared-infrastructure +++ b/shared-infrastructure @@ -1 +1 @@ -Subproject commit a042aba176cdb840d800c6ed4cfe41a54fb7b1e3 +Subproject commit 59ce17f5a4e1f956811133f41add7638e74c2836 diff --git a/src/ImageSharp/Advanced/AdvancedImageExtensions.cs b/src/ImageSharp/Advanced/AdvancedImageExtensions.cs index 54a773be05..829c6155db 100644 --- a/src/ImageSharp/Advanced/AdvancedImageExtensions.cs +++ b/src/ImageSharp/Advanced/AdvancedImageExtensions.cs @@ -143,7 +143,7 @@ public static IMemoryGroup GetPixelMemoryGroup(this ImageThe source. /// The row. /// The - public static Memory GetPixelRowMemory(this ImageFrame source, int rowIndex) + public static Memory DangerousGetPixelRowMemory(this ImageFrame source, int rowIndex) where TPixel : unmanaged, IPixel { Guard.NotNull(source, nameof(source)); @@ -161,7 +161,7 @@ public static Memory GetPixelRowMemory(this ImageFrame s /// The source. /// The row. /// The - public static Memory GetPixelRowMemory(this Image source, int rowIndex) + public static Memory DangerousGetPixelRowMemory(this Image source, int rowIndex) where TPixel : unmanaged, IPixel { Guard.NotNull(source, nameof(source)); diff --git a/src/ImageSharp/Advanced/AotCompilerTools.cs b/src/ImageSharp/Advanced/AotCompilerTools.cs index 3961cc6c57..b90a6ce3cd 100644 --- a/src/ImageSharp/Advanced/AotCompilerTools.cs +++ b/src/ImageSharp/Advanced/AotCompilerTools.cs @@ -529,7 +529,7 @@ private static void AotCompileDither() private static void AotCompileMemoryManagers() where TPixel : unmanaged, IPixel { - AotCompileMemoryManager(); + AotCompileMemoryManager(); AotCompileMemoryManager(); } diff --git a/src/ImageSharp/Common/Helpers/DebugGuard.cs b/src/ImageSharp/Common/Helpers/DebugGuard.cs index f56cb37a81..f438ca9e24 100644 --- a/src/ImageSharp/Common/Helpers/DebugGuard.cs +++ b/src/ImageSharp/Common/Helpers/DebugGuard.cs @@ -26,6 +26,20 @@ public static void IsTrue(bool target, string message) } } + /// + /// Verifies whether a condition (indicating disposed state) is met, throwing an ObjectDisposedException if it's true. + /// + /// Whether the object is disposed. + /// The name of the object. + [Conditional("DEBUG")] + public static void NotDisposed(bool isDisposed, string objectName) + { + if (isDisposed) + { + throw new ObjectDisposedException(objectName); + } + } + /// /// Verifies, that the target span is of same size than the 'other' span. /// diff --git a/src/ImageSharp/Common/Helpers/Shuffle/IComponentShuffle.cs b/src/ImageSharp/Common/Helpers/Shuffle/IComponentShuffle.cs index 7687a5b95f..929b786921 100644 --- a/src/ImageSharp/Common/Helpers/Shuffle/IComponentShuffle.cs +++ b/src/ImageSharp/Common/Helpers/Shuffle/IComponentShuffle.cs @@ -28,6 +28,10 @@ internal interface IComponentShuffle /// /// The source span of bytes. /// The destination span of bytes. + /// + /// Implementation can assume that source.Length is less or equal than dest.Length. + /// Loops should iterate using source.Length. + /// void RunFallbackShuffle(ReadOnlySpan source, Span dest); } diff --git a/src/ImageSharp/Common/Helpers/SimdUtils.Shuffle.cs b/src/ImageSharp/Common/Helpers/SimdUtils.Shuffle.cs index 07744566a3..abf9e9fed0 100644 --- a/src/ImageSharp/Common/Helpers/SimdUtils.Shuffle.cs +++ b/src/ImageSharp/Common/Helpers/SimdUtils.Shuffle.cs @@ -77,6 +77,7 @@ public static void Shuffle3( TShuffle shuffle) where TShuffle : struct, IShuffle3 { + // Source length should be smaller than dest length, and divisible by 3. VerifyShuffle3SpanInput(source, dest); #if SUPPORTS_RUNTIME_INTRINSICS @@ -182,9 +183,9 @@ private static void VerifyShuffle3SpanInput(ReadOnlySpan source, Span d where T : struct { DebugGuard.IsTrue( - source.Length == dest.Length, + source.Length <= dest.Length, nameof(source), - "Input spans must be of same length!"); + "Source should fit into dest!"); DebugGuard.IsTrue( source.Length % 3 == 0, diff --git a/src/ImageSharp/Configuration.cs b/src/ImageSharp/Configuration.cs index ea9524827f..94584ff203 100644 --- a/src/ImageSharp/Configuration.cs +++ b/src/ImageSharp/Configuration.cs @@ -26,10 +26,11 @@ public sealed class Configuration /// /// A lazily initialized configuration default instance. /// - private static readonly Lazy Lazy = new Lazy(CreateDefaultInstance); + private static readonly Lazy Lazy = new(CreateDefaultInstance); private const int DefaultStreamProcessingBufferSize = 8096; private int streamProcessingBufferSize = DefaultStreamProcessingBufferSize; private int maxDegreeOfParallelism = Environment.ProcessorCount; + private MemoryAllocator memoryAllocator = MemoryAllocator.Default; /// /// Initializes a new instance of the class. @@ -95,6 +96,14 @@ public int StreamProcessingBufferSize } } + /// + /// Gets or sets a value indicating whether to force image buffers to be contiguous whenever possible. + /// + /// + /// Contiguous allocations are not possible, if the image needs a buffer larger than . + /// + public bool PreferContiguousImageBuffers { get; set; } + /// /// Gets a set of properties for the Configuration. /// @@ -117,9 +126,31 @@ public int StreamProcessingBufferSize public ImageFormatManager ImageFormatsManager { get; set; } = new ImageFormatManager(); /// - /// Gets or sets the that is currently in use. + /// Gets or sets the that is currently in use. + /// Defaults to . + /// + /// Allocators are expensive, so it is strongly recommended to use only one busy instance per process. + /// In case you need to customize it, you can ensure this by changing /// - public MemoryAllocator MemoryAllocator { get; set; } = ArrayPoolMemoryAllocator.CreateDefault(); + /// + /// It's possible to reduce allocator footprint by assigning a custom instance created with + /// , but note that since the default pooling + /// allocators are expensive, it is strictly recommended to use a single process-wide allocator. + /// You can ensure this by altering the allocator of , or by implementing custom application logic that + /// manages allocator lifetime. + /// + /// If an allocator has to be dropped for some reason, + /// shall be invoked after disposing all associated instances. + /// + public MemoryAllocator MemoryAllocator + { + get => this.memoryAllocator; + set + { + Guard.NotNull(value, nameof(this.MemoryAllocator)); + this.memoryAllocator = value; + } + } /// /// Gets the maximum header size of all the formats. @@ -165,7 +196,7 @@ public void Configure(IConfigurationModule configuration) MaxDegreeOfParallelism = this.MaxDegreeOfParallelism, StreamProcessingBufferSize = this.StreamProcessingBufferSize, ImageFormatsManager = this.ImageFormatsManager, - MemoryAllocator = this.MemoryAllocator, + memoryAllocator = this.memoryAllocator, ImageOperationsProvider = this.ImageOperationsProvider, ReadOrigin = this.ReadOrigin, FileSystem = this.FileSystem, diff --git a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs index 8919befcb2..41adc1cfff 100644 --- a/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpDecoderCore.cs @@ -306,7 +306,7 @@ private void ReadRle(BmpCompression compression, Buffer2D pixels int newY = Invert(y, height, inverted); int rowStartIdx = y * width; Span bufferRow = bufferSpan.Slice(rowStartIdx, width); - Span pixelRow = pixels.GetRowSpan(newY); + Span pixelRow = pixels.DangerousGetRowSpan(newY); bool rowHasUndefinedPixels = rowsWithUndefinedPixelsSpan[y]; if (rowHasUndefinedPixels) @@ -377,7 +377,7 @@ private void ReadRle24(Buffer2D pixels, int width, int height, b for (int y = 0; y < height; y++) { int newY = Invert(y, height, inverted); - Span pixelRow = pixels.GetRowSpan(newY); + Span pixelRow = pixels.DangerousGetRowSpan(newY); bool rowHasUndefinedPixels = rowsWithUndefinedPixelsSpan[y]; if (rowHasUndefinedPixels) { @@ -826,7 +826,7 @@ private void ReadRgbPalette(Buffer2D pixels, byte[] colors, int int newY = Invert(y, height, inverted); this.stream.Read(rowSpan); int offset = 0; - Span pixelRow = pixels.GetRowSpan(newY); + Span pixelRow = pixels.DangerousGetRowSpan(newY); for (int x = 0; x < arrayWidth; x++) { @@ -878,7 +878,7 @@ private void ReadRgb16(Buffer2D pixels, int width, int height, b { this.stream.Read(bufferSpan); int newY = Invert(y, height, inverted); - Span pixelRow = pixels.GetRowSpan(newY); + Span pixelRow = pixels.DangerousGetRowSpan(newY); int offset = 0; for (int x = 0; x < width; x++) @@ -933,7 +933,7 @@ private void ReadRgb24(Buffer2D pixels, int width, int height, b { this.stream.Read(rowSpan); int newY = Invert(y, height, inverted); - Span pixelSpan = pixels.GetRowSpan(newY); + Span pixelSpan = pixels.DangerousGetRowSpan(newY); PixelOperations.Instance.FromBgr24Bytes( this.Configuration, rowSpan, @@ -961,7 +961,7 @@ private void ReadRgb32Fast(Buffer2D pixels, int width, int heigh { this.stream.Read(rowSpan); int newY = Invert(y, height, inverted); - Span pixelSpan = pixels.GetRowSpan(newY); + Span pixelSpan = pixels.DangerousGetRowSpan(newY); PixelOperations.Instance.FromBgra32Bytes( this.Configuration, rowSpan, @@ -1031,7 +1031,7 @@ private void ReadRgb32Slow(Buffer2D pixels, int width, int heigh this.stream.Read(rowSpan); int newY = Invert(y, height, inverted); - Span pixelSpan = pixels.GetRowSpan(newY); + Span pixelSpan = pixels.DangerousGetRowSpan(newY); PixelOperations.Instance.FromBgra32Bytes( this.Configuration, @@ -1054,7 +1054,7 @@ private void ReadRgb32Slow(Buffer2D pixels, int width, int heigh width); int newY = Invert(y, height, inverted); - Span pixelSpan = pixels.GetRowSpan(newY); + Span pixelSpan = pixels.DangerousGetRowSpan(newY); for (int x = 0; x < width; x++) { @@ -1109,7 +1109,7 @@ private void ReadRgb32BitFields(Buffer2D pixels, int width, int { this.stream.Read(bufferSpan); int newY = Invert(y, height, inverted); - Span pixelRow = pixels.GetRowSpan(newY); + Span pixelRow = pixels.DangerousGetRowSpan(newY); int offset = 0; for (int x = 0; x < width; x++) diff --git a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs index c6ca5b09d2..6384074df3 100644 --- a/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs +++ b/src/ImageSharp/Formats/Bmp/BmpEncoderCore.cs @@ -274,7 +274,7 @@ private void Write32Bit(Stream stream, Buffer2D pixels) for (int y = pixels.Height - 1; y >= 0; y--) { - Span pixelSpan = pixels.GetRowSpan(y); + Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.ToBgra32Bytes( this.configuration, pixelSpan, @@ -300,7 +300,7 @@ private void Write24Bit(Stream stream, Buffer2D pixels) for (int y = pixels.Height - 1; y >= 0; y--) { - Span pixelSpan = pixels.GetRowSpan(y); + Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.ToBgr24Bytes( this.configuration, pixelSpan, @@ -326,7 +326,7 @@ private void Write16Bit(Stream stream, Buffer2D pixels) for (int y = pixels.Height - 1; y >= 0; y--) { - Span pixelSpan = pixels.GetRowSpan(y); + Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.ToBgra5551Bytes( this.configuration, @@ -379,7 +379,7 @@ private void Write8BitColor(Stream stream, ImageFrame image, Spa for (int y = image.Height - 1; y >= 0; y--) { - ReadOnlySpan pixelSpan = quantized.GetPixelRowSpan(y); + ReadOnlySpan pixelSpan = quantized.DangerousGetRowSpan(y); stream.Write(pixelSpan); for (int i = 0; i < this.padding; i++) @@ -413,10 +413,10 @@ private void Write8BitGray(Stream stream, ImageFrame image, Span } stream.Write(colorPalette); - + Buffer2D imageBuffer = image.PixelBuffer; for (int y = image.Height - 1; y >= 0; y--) { - ReadOnlySpan inputPixelRow = image.GetPixelRowSpan(y); + ReadOnlySpan inputPixelRow = imageBuffer.DangerousGetRowSpan(y); ReadOnlySpan outputPixelRow = MemoryMarshal.AsBytes(inputPixelRow); stream.Write(outputPixelRow); @@ -447,11 +447,11 @@ private void Write4BitColor(Stream stream, ImageFrame image) ReadOnlySpan quantizedColorPalette = quantized.Palette.Span; this.WriteColorPalette(stream, quantizedColorPalette, colorPalette); - ReadOnlySpan pixelRowSpan = quantized.GetPixelRowSpan(0); + ReadOnlySpan pixelRowSpan = quantized.DangerousGetRowSpan(0); int rowPadding = pixelRowSpan.Length % 2 != 0 ? this.padding - 1 : this.padding; for (int y = image.Height - 1; y >= 0; y--) { - pixelRowSpan = quantized.GetPixelRowSpan(y); + pixelRowSpan = quantized.DangerousGetRowSpan(y); int endIdx = pixelRowSpan.Length % 2 == 0 ? pixelRowSpan.Length : pixelRowSpan.Length - 1; for (int i = 0; i < endIdx; i += 2) @@ -491,11 +491,11 @@ private void Write1BitColor(Stream stream, ImageFrame image) ReadOnlySpan quantizedColorPalette = quantized.Palette.Span; this.WriteColorPalette(stream, quantizedColorPalette, colorPalette); - ReadOnlySpan quantizedPixelRow = quantized.GetPixelRowSpan(0); + ReadOnlySpan quantizedPixelRow = quantized.DangerousGetRowSpan(0); int rowPadding = quantizedPixelRow.Length % 8 != 0 ? this.padding - 1 : this.padding; for (int y = image.Height - 1; y >= 0; y--) { - quantizedPixelRow = quantized.GetPixelRowSpan(y); + quantizedPixelRow = quantized.DangerousGetRowSpan(y); int endIdx = quantizedPixelRow.Length % 8 == 0 ? quantizedPixelRow.Length : quantizedPixelRow.Length - 8; for (int i = 0; i < endIdx; i += 8) diff --git a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs index 482a761530..3e33a6e379 100644 --- a/src/ImageSharp/Formats/Gif/GifDecoderCore.cs +++ b/src/ImageSharp/Formats/Gif/GifDecoderCore.cs @@ -445,7 +445,7 @@ private void ReadFrameColors(ref Image image, ref ImageFrame(ref Image image, ref ImageFrame pixels) int y = 0; int x = 0; int rowMax = width; - ref byte pixelsRowRef = ref MemoryMarshal.GetReference(pixels.GetRowSpan(y)); + ref byte pixelsRowRef = ref MemoryMarshal.GetReference(pixels.DangerousGetRowSpan(y)); while (xyz < length) { // Reset row reference. if (xyz == rowMax) { x = 0; - pixelsRowRef = ref MemoryMarshal.GetReference(pixels.GetRowSpan(++y)); + pixelsRowRef = ref MemoryMarshal.GetReference(pixels.DangerousGetRowSpan(++y)); rowMax = (y * width) + width; } diff --git a/src/ImageSharp/Formats/Gif/LzwEncoder.cs b/src/ImageSharp/Formats/Gif/LzwEncoder.cs index e9fb7ab00b..c52e34f963 100644 --- a/src/ImageSharp/Formats/Gif/LzwEncoder.cs +++ b/src/ImageSharp/Formats/Gif/LzwEncoder.cs @@ -275,7 +275,7 @@ private void Compress(Buffer2D indexedPixels, int initialBits, Stream stre for (int y = 0; y < indexedPixels.Height; y++) { - ref byte rowSpanRef = ref MemoryMarshal.GetReference(indexedPixels.GetRowSpan(y)); + ref byte rowSpanRef = ref MemoryMarshal.GetReference(indexedPixels.DangerousGetRowSpan(y)); int offsetX = y == 0 ? 1 : 0; for (int x = offsetX; x < indexedPixels.Width; x++) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.cs index dad46861e2..79eedf2f7d 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/ColorConverters/JpegColorConverter.cs @@ -223,12 +223,12 @@ public ComponentValues(IReadOnlyList> componentBuffers, int row) { this.ComponentCount = componentBuffers.Count; - this.Component0 = componentBuffers[0].GetRowSpan(row); + this.Component0 = componentBuffers[0].DangerousGetRowSpan(row); // In case of grayscale, Component1 and Component2 point to Component0 memory area - this.Component1 = this.ComponentCount > 1 ? componentBuffers[1].GetRowSpan(row) : this.Component0; - this.Component2 = this.ComponentCount > 2 ? componentBuffers[2].GetRowSpan(row) : this.Component0; - this.Component3 = this.ComponentCount > 3 ? componentBuffers[3].GetRowSpan(row) : Span.Empty; + this.Component1 = this.ComponentCount > 1 ? componentBuffers[1].DangerousGetRowSpan(row) : this.Component0; + this.Component2 = this.ComponentCount > 2 ? componentBuffers[2].DangerousGetRowSpan(row) : this.Component0; + this.Component3 = this.ComponentCount > 3 ? componentBuffers[3].DangerousGetRowSpan(row) : Span.Empty; } internal ComponentValues( diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs index 6f104351c8..ce5e5110b6 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/HuffmanScanDecoder.cs @@ -203,7 +203,7 @@ private void ParseBaselineDataInterleaved() // by the basic H and V specified for the component for (int y = 0; y < v; y++) { - Span blockSpan = component.SpectralBlocks.GetRowSpan(y); + Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(y); ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); for (int x = 0; x < h; x++) @@ -254,7 +254,7 @@ private void ParseBaselineDataNonInterleaved() for (int j = 0; j < h; j++) { this.cancellationToken.ThrowIfCancellationRequested(); - Span blockSpan = component.SpectralBlocks.GetRowSpan(j); + Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(j); ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); for (int i = 0; i < w; i++) @@ -377,7 +377,7 @@ private void ParseProgressiveDataInterleaved() for (int y = 0; y < v; y++) { int blockRow = (mcuRow * v) + y; - Span blockSpan = component.SpectralBlocks.GetRowSpan(blockRow); + Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(blockRow); ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); for (int x = 0; x < h; x++) @@ -422,7 +422,7 @@ private void ParseProgressiveDataNonInterleaved() { this.cancellationToken.ThrowIfCancellationRequested(); - Span blockSpan = component.SpectralBlocks.GetRowSpan(j); + Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(j); ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); for (int i = 0; i < w; i++) @@ -450,7 +450,7 @@ ref Unsafe.Add(ref blockRef, i), { this.cancellationToken.ThrowIfCancellationRequested(); - Span blockSpan = component.SpectralBlocks.GetRowSpan(j); + Span blockSpan = component.SpectralBlocks.DangerousGetRowSpan(j); ref Block8x8 blockRef = ref MemoryMarshal.GetReference(blockSpan); for (int i = 0; i < w; i++) diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs index 3e04e80b7a..c3bf1cbdd5 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/JpegComponentPostProcessor.cs @@ -84,8 +84,8 @@ public void CopyBlocksToColorBuffer(int spectralStep) { int yBuffer = y * this.blockAreaSize.Height; - Span colorBufferRow = this.ColorBuffer.GetRowSpan(yBuffer); - Span blockRow = spectralBuffer.GetRowSpan(yBlockStart + y); + Span colorBufferRow = this.ColorBuffer.DangerousGetRowSpan(yBuffer); + Span blockRow = spectralBuffer.DangerousGetRowSpan(yBlockStart + y); for (int xBlock = 0; xBlock < spectralBuffer.Width; xBlock++) { @@ -119,11 +119,11 @@ public void ClearSpectralBuffers() Buffer2D spectralBlocks = this.component.SpectralBlocks; for (int i = 0; i < spectralBlocks.Height; i++) { - spectralBlocks.GetRowSpan(i).Clear(); + spectralBlocks.DangerousGetRowSpan(i).Clear(); } } public Span GetColorBufferRowSpan(int row) => - this.ColorBuffer.GetRowSpan(row); + this.ColorBuffer.DangerousGetRowSpan(row); } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs index 0003437e74..5edcf565c2 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/Decoder/SpectralConverter{TPixel}.cs @@ -187,7 +187,7 @@ private void ConvertStride(int spectralStep) { Span proxyRow = this.paddedProxyPixelRow.GetSpan(); PixelOperations.Instance.PackFromRgbPlanes(this.configuration, r, g, b, proxyRow); - proxyRow.Slice(0, width).CopyTo(this.pixelBuffer.GetRowSpan(yy)); + proxyRow.Slice(0, width).CopyTo(this.pixelBuffer.DangerousGetRowSpan(yy)); } } diff --git a/src/ImageSharp/Formats/Jpeg/Components/RowOctet.cs b/src/ImageSharp/Formats/Jpeg/Components/RowOctet.cs index 16d24cf814..d4a4c1cf45 100644 --- a/src/ImageSharp/Formats/Jpeg/Components/RowOctet.cs +++ b/src/ImageSharp/Formats/Jpeg/Components/RowOctet.cs @@ -86,7 +86,7 @@ public void Update(Buffer2D buffer, int startY) int i = 0; while (y < yEnd) { - this[i++] = buffer.GetRowSpan(y++); + this[i++] = buffer.DangerousGetRowSpan(y++); } } diff --git a/src/ImageSharp/Formats/Png/PngDecoderCore.cs b/src/ImageSharp/Formats/Png/PngDecoderCore.cs index cf3cd7eb14..da8481c26c 100644 --- a/src/ImageSharp/Formats/Png/PngDecoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngDecoderCore.cs @@ -565,6 +565,7 @@ private void DecodeInterlacedPixelData(DeflateStream compressedStream, I { int pass = 0; int width = this.header.Width; + Buffer2D imageBuffer = image.PixelBuffer; while (true) { int numColumns = Adam7.ComputeColumns(width, pass); @@ -623,7 +624,7 @@ private void DecodeInterlacedPixelData(DeflateStream compressedStream, I break; } - Span rowSpan = image.GetPixelRowSpan(this.currentRow); + Span rowSpan = imageBuffer.DangerousGetRowSpan(this.currentRow); this.ProcessInterlacedDefilteredScanline(this.scanline.GetSpan(), rowSpan, pngMetadata, Adam7.FirstColumn[pass], Adam7.ColumnIncrement[pass]); this.SwapScanlineBuffers(); @@ -656,7 +657,7 @@ private void DecodeInterlacedPixelData(DeflateStream compressedStream, I private void ProcessDefilteredScanline(ReadOnlySpan defilteredScanline, ImageFrame pixels, PngMetadata pngMetadata) where TPixel : unmanaged, IPixel { - Span rowSpan = pixels.GetPixelRowSpan(this.currentRow); + Span rowSpan = pixels.PixelBuffer.DangerousGetRowSpan(this.currentRow); // Trim the first marker byte from the buffer ReadOnlySpan trimmed = defilteredScanline.Slice(1, defilteredScanline.Length - 1); diff --git a/src/ImageSharp/Formats/Png/PngEncoderCore.cs b/src/ImageSharp/Formats/Png/PngEncoderCore.cs index f10db7a6c0..5e067aba57 100644 --- a/src/ImageSharp/Formats/Png/PngEncoderCore.cs +++ b/src/ImageSharp/Formats/Png/PngEncoderCore.cs @@ -163,23 +163,25 @@ public void Dispose() /// The type of the pixel. /// The cloned image where the transparent pixels will be changed. private static void ClearTransparentPixels(Image image) - where TPixel : unmanaged, IPixel - { - Rgba32 rgba32 = default; - for (int y = 0; y < image.Height; y++) + where TPixel : unmanaged, IPixel => + image.ProcessPixelRows(accessor => { - Span span = image.GetPixelRowSpan(y); - for (int x = 0; x < image.Width; x++) + Rgba32 rgba32 = default; + Rgba32 transparent = Color.Transparent; + for (int y = 0; y < accessor.Height; y++) { - span[x].ToRgba32(ref rgba32); - - if (rgba32.A == 0) + Span span = accessor.GetRowSpan(y); + for (int x = 0; x < accessor.Width; x++) { - span[x].FromRgba32(Color.Transparent); + span[x].ToRgba32(ref rgba32); + + if (rgba32.A == 0) + { + span[x].FromRgba32(transparent); + } } } - } - } + }); /// /// Creates the quantized image and sets calculates and sets the bit depth. @@ -391,11 +393,11 @@ private void CollectPixelBytes(ReadOnlySpan rowSpan, IndexedImag if (this.bitDepth < 8) { - PngEncoderHelpers.ScaleDownFrom8BitArray(quantized.GetPixelRowSpan(row), this.currentScanline.GetSpan(), this.bitDepth); + PngEncoderHelpers.ScaleDownFrom8BitArray(quantized.DangerousGetRowSpan(row), this.currentScanline.GetSpan(), this.bitDepth); } else { - quantized.GetPixelRowSpan(row).CopyTo(this.currentScanline.GetSpan()); + quantized.DangerousGetRowSpan(row).CopyTo(this.currentScanline.GetSpan()); } break; @@ -914,27 +916,31 @@ private void EncodePixels(Image pixels, IndexedImageFrame filterBuffer = this.memoryAllocator.Allocate(filterLength, AllocationOptions.Clean); using IMemoryOwner attemptBuffer = this.memoryAllocator.Allocate(filterLength, AllocationOptions.Clean); - Span filter = filterBuffer.GetSpan(); - Span attempt = attemptBuffer.GetSpan(); - for (int y = 0; y < this.height; y++) + pixels.ProcessPixelRows(accessor => { - this.CollectAndFilterPixelRow(pixels.GetPixelRowSpan(y), ref filter, ref attempt, quantized, y); - deflateStream.Write(filter); - this.SwapScanlineBuffers(); - } + Span filter = filterBuffer.GetSpan(); + Span attempt = attemptBuffer.GetSpan(); + for (int y = 0; y < this.height; y++) + { + this.CollectAndFilterPixelRow(accessor.GetRowSpan(y), ref filter, ref attempt, quantized, y); + deflateStream.Write(filter); + this.SwapScanlineBuffers(); + } + }); } /// /// Interlaced encoding the pixels. /// /// The type of the pixel. - /// The pixels. + /// The image. /// The deflate stream. - private void EncodeAdam7Pixels(Image pixels, ZlibDeflateStream deflateStream) + private void EncodeAdam7Pixels(Image image, ZlibDeflateStream deflateStream) where TPixel : unmanaged, IPixel { - int width = pixels.Width; - int height = pixels.Height; + int width = image.Width; + int height = image.Height; + Buffer2D pixelBuffer = image.Frames.RootFrame.PixelBuffer; for (int pass = 0; pass < 7; pass++) { int startRow = Adam7.FirstRow[pass]; @@ -959,7 +965,7 @@ private void EncodeAdam7Pixels(Image pixels, ZlibDeflateStream d for (int row = startRow; row < height; row += Adam7.RowIncrement[pass]) { // Collect pixel data - Span srcRow = pixels.GetPixelRowSpan(row); + Span srcRow = pixelBuffer.DangerousGetRowSpan(row); for (int col = startCol, i = 0; col < width; col += Adam7.ColumnIncrement[pass]) { block[i++] = srcRow[col]; @@ -1014,7 +1020,7 @@ private void EncodeAdam7IndexedPixels(IndexedImageFrame quantize row += Adam7.RowIncrement[pass]) { // Collect data - ReadOnlySpan srcRow = quantized.GetPixelRowSpan(row); + ReadOnlySpan srcRow = quantized.DangerousGetRowSpan(row); for (int col = startCol, i = 0; col < width; col += Adam7.ColumnIncrement[pass]) diff --git a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs index 8f97861400..d101ccd94a 100644 --- a/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaDecoderCore.cs @@ -234,7 +234,7 @@ private void ReadPaletted(int width, int height, Buffer2D pixels for (int y = 0; y < height; y++) { int newY = InvertY(y, height, origin); - Span pixelRow = pixels.GetRowSpan(newY); + Span pixelRow = pixels.DangerousGetRowSpan(newY); switch (colorMapPixelSizeInBytes) { @@ -318,7 +318,7 @@ private void ReadPalettedRle(int width, int height, Buffer2D pix for (int y = 0; y < height; y++) { int newY = InvertY(y, height, origin); - Span pixelRow = pixels.GetRowSpan(newY); + Span pixelRow = pixels.DangerousGetRowSpan(newY); int rowStartIdx = y * width * bytesPerPixel; for (int x = 0; x < width; x++) { @@ -364,7 +364,7 @@ private void ReadMonoChrome(int width, int height, Buffer2D pixe for (int y = 0; y < height; y++) { int newY = InvertY(y, height, origin); - Span pixelSpan = pixels.GetRowSpan(newY); + Span pixelSpan = pixels.DangerousGetRowSpan(newY); for (int x = width - 1; x >= 0; x--) { this.ReadL8Pixel(color, x, pixelSpan); @@ -412,7 +412,7 @@ private void ReadBgra16(int width, int height, Buffer2D pixels, for (int y = 0; y < height; y++) { int newY = InvertY(y, height, origin); - Span pixelSpan = pixels.GetRowSpan(newY); + Span pixelSpan = pixels.DangerousGetRowSpan(newY); if (invertX) { @@ -479,7 +479,7 @@ private void ReadBgr24(int width, int height, Buffer2D pixels, T for (int y = 0; y < height; y++) { int newY = InvertY(y, height, origin); - Span pixelSpan = pixels.GetRowSpan(newY); + Span pixelSpan = pixels.DangerousGetRowSpan(newY); for (int x = width - 1; x >= 0; x--) { this.ReadBgr24Pixel(color, x, pixelSpan); @@ -548,7 +548,7 @@ private void ReadBgra32(int width, int height, Buffer2D pixels, for (int y = 0; y < height; y++) { int newY = InvertY(y, height, origin); - Span pixelRow = pixels.GetRowSpan(newY); + Span pixelRow = pixels.DangerousGetRowSpan(newY); if (invertX) { for (int x = width - 1; x >= 0; x--) @@ -587,7 +587,7 @@ private void ReadRle(int width, int height, Buffer2D pixels, int for (int y = 0; y < height; y++) { int newY = InvertY(y, height, origin); - Span pixelRow = pixels.GetRowSpan(newY); + Span pixelRow = pixels.DangerousGetRowSpan(newY); int rowStartIdx = y * width * bytesPerPixel; for (int x = 0; x < width; x++) { @@ -654,7 +654,7 @@ private void ReadL8Row(int width, Buffer2D pixels, Span ro where TPixel : unmanaged, IPixel { this.currentStream.Read(row); - Span pixelSpan = pixels.GetRowSpan(y); + Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.FromL8Bytes(this.Configuration, row, pixelSpan, width); } @@ -681,7 +681,7 @@ private void ReadBgr24Row(int width, Buffer2D pixels, Span where TPixel : unmanaged, IPixel { this.currentStream.Read(row); - Span pixelSpan = pixels.GetRowSpan(y); + Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.FromBgr24Bytes(this.Configuration, row, pixelSpan, width); } @@ -700,7 +700,7 @@ private void ReadBgra32Row(int width, Buffer2D pixels, Span { this.currentStream.Read(row); - Span pixelSpan = pixels.GetRowSpan(y); + Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.FromBgra32Bytes(this.Configuration, row, pixelSpan, width); } diff --git a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs index 4bf4ca60a1..1a1260a58e 100644 --- a/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs +++ b/src/ImageSharp/Formats/Tga/TgaEncoderCore.cs @@ -276,7 +276,7 @@ private void Write8Bit(Stream stream, Buffer2D pixels) for (int y = pixels.Height - 1; y >= 0; y--) { - Span pixelSpan = pixels.GetRowSpan(y); + Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.ToL8Bytes( this.configuration, pixelSpan, @@ -300,7 +300,7 @@ private void Write16Bit(Stream stream, Buffer2D pixels) for (int y = pixels.Height - 1; y >= 0; y--) { - Span pixelSpan = pixels.GetRowSpan(y); + Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.ToBgra5551Bytes( this.configuration, pixelSpan, @@ -324,7 +324,7 @@ private void Write24Bit(Stream stream, Buffer2D pixels) for (int y = pixels.Height - 1; y >= 0; y--) { - Span pixelSpan = pixels.GetRowSpan(y); + Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.ToBgr24Bytes( this.configuration, pixelSpan, @@ -348,7 +348,7 @@ private void Write32Bit(Stream stream, Buffer2D pixels) for (int y = pixels.Height - 1; y >= 0; y--) { - Span pixelSpan = pixels.GetRowSpan(y); + Span pixelSpan = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.ToBgra32Bytes( this.configuration, pixelSpan, diff --git a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs index c64fc8ad12..ce7820ccf9 100644 --- a/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs +++ b/src/ImageSharp/Formats/Tiff/Compression/Decompressors/JpegTiffCompression.cs @@ -79,7 +79,7 @@ private static void CopyImageBytesToBuffer(Span buffer, Buffer2D pi int offset = 0; for (int y = 0; y < pixelBuffer.Height; y++) { - Span pixelRowSpan = pixelBuffer.GetRowSpan(y); + Span pixelRowSpan = pixelBuffer.DangerousGetRowSpan(y); Span rgbBytes = MemoryMarshal.AsBytes(pixelRowSpan); rgbBytes.CopyTo(buffer.Slice(offset)); offset += rgbBytes.Length; diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs index 4595068432..5d910d16e7 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero16TiffColor{TPixel}.cs @@ -41,7 +41,7 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in int offset = 0; for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); if (this.isBigEndian) { for (int x = 0; x < pixelRow.Length; x++) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero24TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero24TiffColor{TPixel}.cs index ec07abd5c4..4cda954804 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero24TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero24TiffColor{TPixel}.cs @@ -36,7 +36,7 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in int offset = 0; for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); if (this.isBigEndian) { for (int x = 0; x < pixelRow.Length; x++) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32FloatTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32FloatTiffColor{TPixel}.cs index ff34a29eb2..ee9bf8a9ce 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32FloatTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32FloatTiffColor{TPixel}.cs @@ -35,7 +35,7 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in int offset = 0; for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); if (this.isBigEndian) { for (int x = 0; x < pixelRow.Length; x++) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32TiffColor{TPixel}.cs index f54a794840..7367a78e34 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero32TiffColor{TPixel}.cs @@ -33,7 +33,7 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in int offset = 0; for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); if (this.isBigEndian) { for (int x = 0; x < pixelRow.Length; x++) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor{TPixel}.cs index f62cf29528..c06239a4d0 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZero8TiffColor{TPixel}.cs @@ -24,7 +24,7 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); int byteCount = pixelRow.Length; PixelOperations.Instance.FromL8Bytes( this.configuration, diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor{TPixel}.cs index 9956db5230..a40fa76675 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/BlackIsZeroTiffColor{TPixel}.cs @@ -34,7 +34,7 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); for (int x = 0; x < pixelRow.Length; x++) { int value = bitReader.ReadBits(this.bitsPerSample0); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs index b392fe1a36..29e03c6c6a 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/PaletteTiffColor{TPixel}.cs @@ -35,7 +35,7 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); for (int x = 0; x < pixelRow.Length; x++) { int index = bitReader.ReadBits(this.bitsPerSample0); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs index e5d8c8da2f..a4d725bcf4 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb161616TiffColor{TPixel}.cs @@ -42,7 +42,7 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); if (this.isBigEndian) { diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs index 9a6d4631ac..1c61b0991c 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb16PlanarTiffColor{TPixel}.cs @@ -39,7 +39,7 @@ public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int offset = 0; for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); if (this.isBigEndian) { for (int x = 0; x < pixelRow.Length; x++) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb242424TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb242424TiffColor{TPixel}.cs index 3be0540a03..985ffeb182 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb242424TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb242424TiffColor{TPixel}.cs @@ -36,7 +36,7 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in Span bufferSpan = buffer.AsSpan(bufferStartIdx); for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); if (this.isBigEndian) { diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb24PlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb24PlanarTiffColor{TPixel}.cs index 9c3e57e2a4..ac4435db63 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb24PlanarTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb24PlanarTiffColor{TPixel}.cs @@ -41,7 +41,7 @@ public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int offset = 0; for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); if (this.isBigEndian) { for (int x = 0; x < pixelRow.Length; x++) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb323232TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb323232TiffColor{TPixel}.cs index e2ba085e1f..bf1e65e1c7 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb323232TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb323232TiffColor{TPixel}.cs @@ -33,7 +33,7 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); if (this.isBigEndian) { diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb32PlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb32PlanarTiffColor{TPixel}.cs index a7432549ce..cdc6942bd7 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb32PlanarTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb32PlanarTiffColor{TPixel}.cs @@ -38,7 +38,7 @@ public override void Decode(IMemoryOwner[] data, Buffer2D pixels, int offset = 0; for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); if (this.isBigEndian) { for (int x = 0; x < pixelRow.Length; x++) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb444TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb444TiffColor{TPixel}.cs index daad50e989..3dfffe0ce8 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb444TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb444TiffColor{TPixel}.cs @@ -23,7 +23,7 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in var bgra = default(Bgra4444); for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y); + Span pixelRow = pixels.DangerousGetRowSpan(y); for (int x = left; x < left + width; x += 2) { diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb888TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb888TiffColor{TPixel}.cs index 2a86eb2ee9..1b5432c28c 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb888TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/Rgb888TiffColor{TPixel}.cs @@ -24,7 +24,7 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); int byteCount = pixelRow.Length * 3; PixelOperations.Instance.FromRgb24Bytes( this.configuration, diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs index f3f27d5c4b..4dc3295a44 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbFloat323232TiffColor{TPixel}.cs @@ -35,7 +35,7 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); if (this.isBigEndian) { diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs index b442c4ae47..54466e05bc 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbPlanarTiffColor{TPixel}.cs @@ -58,7 +58,7 @@ public override void Decode(IMemoryOwner[] data, Buffer2D pixels, for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); for (int x = 0; x < pixelRow.Length; x++) { float r = rBitReader.ReadBits(this.bitsPerSampleR) / this.rFactor; diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs index 1377598cc9..4a887c426f 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/RgbTiffColor{TPixel}.cs @@ -47,7 +47,7 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); for (int x = 0; x < pixelRow.Length; x++) { float r = bitReader.ReadBits(this.bitsPerSampleR) / this.rFactor; diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs index 18b5300b27..038281c998 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero16TiffColor{TPixel}.cs @@ -34,7 +34,7 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in int offset = 0; for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); if (this.isBigEndian) { for (int x = 0; x < pixelRow.Length; x++) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero24TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero24TiffColor{TPixel}.cs index 10182f250f..807023b6bf 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero24TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero24TiffColor{TPixel}.cs @@ -37,7 +37,7 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in int offset = 0; for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); if (this.isBigEndian) { for (int x = 0; x < pixelRow.Length; x++) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32FloatTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32FloatTiffColor{TPixel}.cs index d532247fe3..71323c7bae 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32FloatTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32FloatTiffColor{TPixel}.cs @@ -35,7 +35,7 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in int offset = 0; for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); if (this.isBigEndian) { for (int x = 0; x < pixelRow.Length; x++) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32TiffColor{TPixel}.cs index ef62b4f441..e433956f09 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero32TiffColor{TPixel}.cs @@ -34,7 +34,7 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in int offset = 0; for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); if (this.isBigEndian) { for (int x = 0; x < pixelRow.Length; x++) diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor{TPixel}.cs index 15ebed58f9..8945e55f2a 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZero8TiffColor{TPixel}.cs @@ -24,7 +24,7 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in var l8 = default(L8); for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); for (int x = 0; x < pixelRow.Length; x++) { byte intensity = (byte)(byte.MaxValue - data[offset++]); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor{TPixel}.cs index 9129559647..d692fc7897 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/WhiteIsZeroTiffColor{TPixel}.cs @@ -34,7 +34,7 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); for (int x = 0; x < pixelRow.Length; x++) { int value = bitReader.ReadBits(this.bitsPerSample0); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs index 70578a7442..465c8fba3a 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrPlanarTiffColor{TPixel}.cs @@ -45,7 +45,7 @@ public override void Decode(IMemoryOwner[] data, Buffer2D pixels, for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); for (int x = 0; x < pixelRow.Length; x++) { Rgba32 rgba = this.converter.ConvertToRgba32(yData[offset], cbData[offset], crData[offset]); diff --git a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs index e31b4984d3..52cc1f0f17 100644 --- a/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/PhotometricInterpretation/YCbCrTiffColor{TPixel}.cs @@ -52,7 +52,7 @@ public override void Decode(ReadOnlySpan data, Buffer2D pixels, in for (int y = top; y < top + height; y++) { - Span pixelRow = pixels.GetRowSpan(y).Slice(left, width); + Span pixelRow = pixels.DangerousGetRowSpan(y).Slice(left, width); for (int x = 0; x < pixelRow.Length; x++) { Rgba32 rgba = this.converter.ConvertToRgba32(ycbcrData[offset], ycbcrData[offset + 1], ycbcrData[offset + 2]); diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs index bd20d644f6..5fec09ef14 100644 --- a/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffBiColorWriter{TPixel}.cs @@ -42,18 +42,21 @@ protected override void EncodeStrip(int y, int height, TiffBaseCompressor compre // Special case for T4BitCompressor. int stripPixels = width * height; this.pixelsAsGray ??= this.MemoryAllocator.Allocate(stripPixels); - Span pixelAsGraySpan = this.pixelsAsGray.GetSpan(); - int lastRow = y + height; - int grayRowIdx = 0; - for (int row = y; row < lastRow; row++) + this.imageBlackWhite.ProcessPixelRows(accessor => { - Span pixelsBlackWhiteRow = this.imageBlackWhite.GetPixelRowSpan(row); - Span pixelAsGrayRow = pixelAsGraySpan.Slice(grayRowIdx * width, width); - PixelOperations.Instance.ToL8Bytes(this.Configuration, pixelsBlackWhiteRow, pixelAsGrayRow, width); - grayRowIdx++; - } + Span pixelAsGraySpan = this.pixelsAsGray.GetSpan(); + int lastRow = y + height; + int grayRowIdx = 0; + for (int row = y; row < lastRow; row++) + { + Span pixelsBlackWhiteRow = accessor.GetRowSpan(row); + Span pixelAsGrayRow = pixelAsGraySpan.Slice(grayRowIdx * width, width); + PixelOperations.Instance.ToL8Bytes(this.Configuration, pixelsBlackWhiteRow, pixelAsGrayRow, width); + grayRowIdx++; + } - compressor.CompressStrip(pixelAsGraySpan.Slice(0, stripPixels), height); + compressor.CompressStrip(pixelAsGraySpan.Slice(0, stripPixels), height); + }); } else { @@ -65,6 +68,7 @@ protected override void EncodeStrip(int y, int height, TiffBaseCompressor compre Span rows = this.bitStrip.Slice(0, bytesPerStrip); rows.Clear(); + Buffer2D blackWhiteBuffer = this.imageBlackWhite.Frames.RootFrame.PixelBuffer; int outputRowIdx = 0; int lastRow = y + height; @@ -73,7 +77,7 @@ protected override void EncodeStrip(int y, int height, TiffBaseCompressor compre int bitIndex = 0; int byteIndex = 0; Span outputRow = rows.Slice(outputRowIdx * this.BytesPerRow); - Span pixelsBlackWhiteRow = this.imageBlackWhite.GetPixelRowSpan(row); + Span pixelsBlackWhiteRow = blackWhiteBuffer.DangerousGetRowSpan(row); PixelOperations.Instance.ToL8Bytes(this.Configuration, pixelsBlackWhiteRow, pixelAsGraySpan, width); for (int x = 0; x < this.Image.Width; x++) { diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs index 88c5f33ddd..5d190e0af0 100644 --- a/src/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffCompositeColorWriter{TPixel}.cs @@ -40,7 +40,7 @@ protected override void EncodeStrip(int y, int height, TiffBaseCompressor compre int stripPixelsRowIdx = 0; for (int row = y; row < lastRow; row++) { - Span stripPixelsRow = this.Image.PixelBuffer.GetRowSpan(row); + Span stripPixelsRow = this.Image.PixelBuffer.DangerousGetRowSpan(row); stripPixelsRow.CopyTo(stripPixels.Slice(stripPixelsRowIdx * width, width)); stripPixelsRowIdx++; } diff --git a/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs b/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs index 6d517294d1..900969a6ce 100644 --- a/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs +++ b/src/ImageSharp/Formats/Tiff/Writers/TiffPaletteWriter{TPixel}.cs @@ -67,7 +67,7 @@ protected override void EncodeStrip(int y, int height, TiffBaseCompressor compre int lastRow = y + height; for (int row = y; row < lastRow; row++) { - ReadOnlySpan indexedPixelRow = this.quantizedImage.GetPixelRowSpan(row); + ReadOnlySpan indexedPixelRow = this.quantizedImage.DangerousGetRowSpan(row); int idxPixels = 0; for (int x = 0; x < halfWidth; x++) { @@ -94,7 +94,7 @@ protected override void EncodeStrip(int y, int height, TiffBaseCompressor compre int indexedPixelsRowIdx = 0; for (int row = y; row < lastRow; row++) { - ReadOnlySpan indexedPixelRow = this.quantizedImage.GetPixelRowSpan(row); + ReadOnlySpan indexedPixelRow = this.quantizedImage.DangerousGetRowSpan(row); indexedPixelRow.CopyTo(indexedPixels.Slice(indexedPixelsRowIdx * width, width)); indexedPixelsRowIdx++; } diff --git a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs index 7769a0a6c8..8566566f60 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/Vp8LEncoder.cs @@ -407,13 +407,14 @@ private void EncodeStream(Image image) private bool ConvertPixelsToBgra(Image image, int width, int height) where TPixel : unmanaged, IPixel { + Buffer2D imageBuffer = image.Frames.RootFrame.PixelBuffer; bool nonOpaque = false; Span bgra = this.Bgra.GetSpan(); Span bgraBytes = MemoryMarshal.Cast(bgra); int widthBytes = width * 4; for (int y = 0; y < height; y++) { - Span rowSpan = image.GetPixelRowSpan(y); + Span rowSpan = imageBuffer.DangerousGetRowSpan(y); Span rowBytes = bgraBytes.Slice(y * widthBytes, widthBytes); PixelOperations.Instance.ToBgra32Bytes(this.configuration, rowSpan, rowBytes, width); if (!nonOpaque) diff --git a/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs index 82bd32a020..f517ad520f 100644 --- a/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossless/WebpLosslessDecoder.cs @@ -196,7 +196,7 @@ private void DecodePixelValues(Vp8LDecoder decoder, Buffer2D pix for (int y = 0; y < height; y++) { Span rowAsBytes = pixelDataAsBytes.Slice(y * bytesPerRow, bytesPerRow); - Span pixelRow = pixels.GetRowSpan(y); + Span pixelRow = pixels.DangerousGetRowSpan(y); PixelOperations.Instance.FromBgra32Bytes( this.configuration, rowAsBytes.Slice(0, bytesPerRow), diff --git a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs index 202df9039e..b74f6969e1 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/WebpLossyDecoder.cs @@ -128,7 +128,7 @@ private void DecodePixelValues(int width, int height, Span pixelDa for (int y = 0; y < height; y++) { Span row = pixelData.Slice(y * widthMul3, widthMul3); - Span decodedPixelRow = decodedPixels.GetRowSpan(y); + Span decodedPixelRow = decodedPixels.DangerousGetRowSpan(y); PixelOperations.Instance.FromBgr24Bytes( this.configuration, row, @@ -146,7 +146,7 @@ private void DecodePixelValues(int width, int height, Span pixelDa for (int y = 0; y < height; y++) { int yMulWidth = y * width; - Span decodedPixelRow = decodedPixels.GetRowSpan(y); + Span decodedPixelRow = decodedPixels.DangerousGetRowSpan(y); for (int x = 0; x < width; x++) { int offset = yMulWidth + x; diff --git a/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs b/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs index 16d458ed88..7a731f4284 100644 --- a/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs +++ b/src/ImageSharp/Formats/Webp/Lossy/YuvConversion.cs @@ -321,8 +321,9 @@ private static void PackAndStore(Vector128 a, Vector128 b, Vector128 public static void ConvertRgbToYuv(Image image, Configuration configuration, MemoryAllocator memoryAllocator, Span y, Span u, Span v) where TPixel : unmanaged, IPixel { - int width = image.Width; - int height = image.Height; + Buffer2D imageBuffer = image.Frames.RootFrame.PixelBuffer; + int width = imageBuffer.Width; + int height = imageBuffer.Height; int uvWidth = (width + 1) >> 1; // Temporary storage for accumulated R/G/B values during conversion to U/V. @@ -336,8 +337,8 @@ public static void ConvertRgbToYuv(Image image, Configuration co int rowIndex; for (rowIndex = 0; rowIndex < height - 1; rowIndex += 2) { - Span rowSpan = image.GetPixelRowSpan(rowIndex); - Span nextRowSpan = image.GetPixelRowSpan(rowIndex + 1); + Span rowSpan = imageBuffer.DangerousGetRowSpan(rowIndex); + Span nextRowSpan = imageBuffer.DangerousGetRowSpan(rowIndex + 1); PixelOperations.Instance.ToBgra32(configuration, rowSpan, bgraRow0); PixelOperations.Instance.ToBgra32(configuration, nextRowSpan, bgraRow1); @@ -363,7 +364,7 @@ public static void ConvertRgbToYuv(Image image, Configuration co // Extra last row. if ((height & 1) != 0) { - Span rowSpan = image.GetPixelRowSpan(rowIndex); + Span rowSpan = imageBuffer.DangerousGetRowSpan(rowIndex); PixelOperations.Instance.ToBgra32(configuration, rowSpan, bgraRow0); ConvertRgbaToY(bgraRow0, y.Slice(rowIndex * width), width); diff --git a/src/ImageSharp/ImageFrame{TPixel}.cs b/src/ImageSharp/ImageFrame{TPixel}.cs index a336c9c591..4753e90e0c 100644 --- a/src/ImageSharp/ImageFrame{TPixel}.cs +++ b/src/ImageSharp/ImageFrame{TPixel}.cs @@ -58,7 +58,11 @@ internal ImageFrame(Configuration configuration, int width, int height, ImageFra Guard.MustBeGreaterThan(width, 0, nameof(width)); Guard.MustBeGreaterThan(height, 0, nameof(height)); - this.PixelBuffer = this.GetConfiguration().MemoryAllocator.Allocate2D(width, height, AllocationOptions.Clean); + this.PixelBuffer = this.GetConfiguration().MemoryAllocator.Allocate2D( + width, + height, + configuration.PreferContiguousImageBuffers, + AllocationOptions.Clean); } /// @@ -87,7 +91,10 @@ internal ImageFrame(Configuration configuration, int width, int height, TPixel b Guard.MustBeGreaterThan(width, 0, nameof(width)); Guard.MustBeGreaterThan(height, 0, nameof(height)); - this.PixelBuffer = this.GetConfiguration().MemoryAllocator.Allocate2D(width, height); + this.PixelBuffer = this.GetConfiguration().MemoryAllocator.Allocate2D( + width, + height, + configuration.PreferContiguousImageBuffers); this.Clear(backgroundColor); } @@ -131,7 +138,10 @@ internal ImageFrame(Configuration configuration, ImageFrame source) Guard.NotNull(configuration, nameof(configuration)); Guard.NotNull(source, nameof(source)); - this.PixelBuffer = this.GetConfiguration().MemoryAllocator.Allocate2D(source.PixelBuffer.Width, source.PixelBuffer.Height); + this.PixelBuffer = this.GetConfiguration().MemoryAllocator.Allocate2D( + source.PixelBuffer.Width, + source.PixelBuffer.Height, + configuration.PreferContiguousImageBuffers); source.PixelBuffer.FastMemoryGroup.CopyTo(this.PixelBuffer.FastMemoryGroup); } @@ -168,36 +178,128 @@ internal ImageFrame(Configuration configuration, ImageFrame source) } /// - /// Gets the representation of the pixels as a of contiguous memory - /// at row beginning from the first pixel on that row. + /// Execute to process image pixels in a safe and efficient manner. /// - /// The row. - /// The - /// Thrown when row index is out of range. - public Span GetPixelRowSpan(int rowIndex) + /// The defining the pixel operations. + public void ProcessPixelRows(PixelAccessorAction processPixels) { - Guard.MustBeGreaterThanOrEqualTo(rowIndex, 0, nameof(rowIndex)); - Guard.MustBeLessThan(rowIndex, this.Height, nameof(rowIndex)); + Guard.NotNull(processPixels, nameof(processPixels)); - return this.PixelBuffer.GetRowSpan(rowIndex); + this.PixelBuffer.FastMemoryGroup.IncreaseRefCounts(); + + try + { + var accessor = new PixelAccessor(this.PixelBuffer); + processPixels(accessor); + } + finally + { + this.PixelBuffer.FastMemoryGroup.DecreaseRefCounts(); + } + } + + /// + /// Execute to process pixels of multiple image frames in a safe and efficient manner. + /// + /// The second image frame. + /// The defining the pixel operations. + /// The pixel type of the second image frame. + public void ProcessPixelRows( + ImageFrame frame2, + PixelAccessorAction processPixels) + where TPixel2 : unmanaged, IPixel + { + Guard.NotNull(frame2, nameof(frame2)); + Guard.NotNull(processPixels, nameof(processPixels)); + + this.PixelBuffer.FastMemoryGroup.IncreaseRefCounts(); + frame2.PixelBuffer.FastMemoryGroup.IncreaseRefCounts(); + + try + { + var accessor1 = new PixelAccessor(this.PixelBuffer); + var accessor2 = new PixelAccessor(frame2.PixelBuffer); + processPixels(accessor1, accessor2); + } + finally + { + frame2.PixelBuffer.FastMemoryGroup.DecreaseRefCounts(); + this.PixelBuffer.FastMemoryGroup.DecreaseRefCounts(); + } } /// - /// Gets the representation of the pixels as a in the source image's pixel format + /// Execute to process pixels of multiple image frames in a safe and efficient manner. + /// + /// The second image frame. + /// The third image frame. + /// The defining the pixel operations. + /// The pixel type of the second image frame. + /// The pixel type of the third image frame. + public void ProcessPixelRows( + ImageFrame frame2, + ImageFrame frame3, + PixelAccessorAction processPixels) + where TPixel2 : unmanaged, IPixel + where TPixel3 : unmanaged, IPixel + { + Guard.NotNull(frame2, nameof(frame2)); + Guard.NotNull(frame3, nameof(frame3)); + Guard.NotNull(processPixels, nameof(processPixels)); + + this.PixelBuffer.FastMemoryGroup.IncreaseRefCounts(); + frame2.PixelBuffer.FastMemoryGroup.IncreaseRefCounts(); + frame3.PixelBuffer.FastMemoryGroup.IncreaseRefCounts(); + + try + { + var accessor1 = new PixelAccessor(this.PixelBuffer); + var accessor2 = new PixelAccessor(frame2.PixelBuffer); + var accessor3 = new PixelAccessor(frame3.PixelBuffer); + processPixels(accessor1, accessor2, accessor3); + } + finally + { + frame3.PixelBuffer.FastMemoryGroup.DecreaseRefCounts(); + frame2.PixelBuffer.FastMemoryGroup.DecreaseRefCounts(); + this.PixelBuffer.FastMemoryGroup.DecreaseRefCounts(); + } + } + + /// + /// Copy image pixels to . + /// + /// The to copy image pixels to. + public void CopyPixelDataTo(Span destination) => this.GetPixelMemoryGroup().CopyTo(destination); + + /// + /// Copy image pixels to . + /// + /// The of to copy image pixels to. + public void CopyPixelDataTo(Span destination) => this.GetPixelMemoryGroup().CopyTo(MemoryMarshal.Cast(destination)); + + /// + /// 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. + /// + /// To ensure the memory is contiguous, should be set + /// to true, preferably on a non-global configuration instance (not ). + /// + /// WARNING: Disposing or leaking the underlying image while still working with the 's + /// might lead to memory corruption. /// - /// The . - /// The . - public bool TryGetSinglePixelSpan(out Span span) + /// The referencing the image buffer. + /// The indicating the success. + public bool DangerousTryGetSinglePixelMemory(out Memory memory) { IMemoryGroup mg = this.GetPixelMemoryGroup(); if (mg.Count > 1) { - span = default; + memory = default; return false; } - span = mg.Single().Span; + memory = mg.Single(); return true; } @@ -310,7 +412,7 @@ internal ImageFrame CloneAs(Configuration configuration) } var target = new ImageFrame(configuration, this.Width, this.Height, this.Metadata.DeepClone()); - var operation = new RowIntervalOperation(this, target, configuration); + var operation = new RowIntervalOperation(this.PixelBuffer, target.PixelBuffer, configuration); ParallelRowIterator.IterateRowIntervals( configuration, @@ -364,14 +466,14 @@ private static void ThrowArgumentOutOfRangeException(string paramName) private readonly struct RowIntervalOperation : IRowIntervalOperation where TPixel2 : unmanaged, IPixel { - private readonly ImageFrame source; - private readonly ImageFrame target; + private readonly Buffer2D source; + private readonly Buffer2D target; private readonly Configuration configuration; [MethodImpl(InliningOptions.ShortMethod)] public RowIntervalOperation( - ImageFrame source, - ImageFrame target, + Buffer2D source, + Buffer2D target, Configuration configuration) { this.source = source; @@ -385,8 +487,8 @@ public void Invoke(in RowInterval rows) { for (int y = rows.Min; y < rows.Max; y++) { - Span sourceRow = this.source.GetPixelRowSpan(y); - Span targetRow = this.target.GetPixelRowSpan(y); + Span sourceRow = this.source.DangerousGetRowSpan(y); + Span targetRow = this.target.DangerousGetRowSpan(y); PixelOperations.Instance.To(this.configuration, sourceRow, targetRow); } } diff --git a/src/ImageSharp/Image{TPixel}.cs b/src/ImageSharp/Image{TPixel}.cs index 2aa9c53945..d01706b01d 100644 --- a/src/ImageSharp/Image{TPixel}.cs +++ b/src/ImageSharp/Image{TPixel}.cs @@ -2,9 +2,11 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; @@ -204,39 +206,136 @@ internal Image(Configuration configuration, ImageMetadata metadata, IEnumerable< } /// - /// Gets the representation of the pixels as a of contiguous memory - /// at row beginning from the first pixel on that row. + /// Execute to process image pixels in a safe and efficient manner. /// - /// The row. - /// The - /// Thrown when row index is out of range. - public Span GetPixelRowSpan(int rowIndex) + /// The defining the pixel operations. + public void ProcessPixelRows(PixelAccessorAction processPixels) { - Guard.MustBeGreaterThanOrEqualTo(rowIndex, 0, nameof(rowIndex)); - Guard.MustBeLessThan(rowIndex, this.Height, nameof(rowIndex)); + Guard.NotNull(processPixels, nameof(processPixels)); + Buffer2D buffer = this.Frames.RootFrame.PixelBuffer; + buffer.FastMemoryGroup.IncreaseRefCounts(); - this.EnsureNotDisposed(); + try + { + var accessor = new PixelAccessor(buffer); + processPixels(accessor); + } + finally + { + buffer.FastMemoryGroup.DecreaseRefCounts(); + } + } + + /// + /// Execute to process pixels of multiple images in a safe and efficient manner. + /// + /// The second image. + /// The defining the pixel operations. + /// The pixel type of the second image. + public void ProcessPixelRows( + Image image2, + PixelAccessorAction processPixels) + where TPixel2 : unmanaged, IPixel + { + Guard.NotNull(image2, nameof(image2)); + Guard.NotNull(processPixels, nameof(processPixels)); + + Buffer2D buffer1 = this.Frames.RootFrame.PixelBuffer; + Buffer2D buffer2 = image2.Frames.RootFrame.PixelBuffer; + + buffer1.FastMemoryGroup.IncreaseRefCounts(); + buffer2.FastMemoryGroup.IncreaseRefCounts(); + + try + { + var accessor1 = new PixelAccessor(buffer1); + var accessor2 = new PixelAccessor(buffer2); + processPixels(accessor1, accessor2); + } + finally + { + buffer2.FastMemoryGroup.DecreaseRefCounts(); + buffer1.FastMemoryGroup.DecreaseRefCounts(); + } + } + + /// + /// Execute to process pixels of multiple images in a safe and efficient manner. + /// + /// The second image. + /// The third image. + /// The defining the pixel operations. + /// The pixel type of the second image. + /// The pixel type of the third image. + public void ProcessPixelRows( + Image image2, + Image image3, + PixelAccessorAction processPixels) + where TPixel2 : unmanaged, IPixel + where TPixel3 : unmanaged, IPixel + { + Guard.NotNull(image2, nameof(image2)); + Guard.NotNull(image3, nameof(image3)); + Guard.NotNull(processPixels, nameof(processPixels)); - return this.PixelSourceUnsafe.PixelBuffer.GetRowSpan(rowIndex); + Buffer2D buffer1 = this.Frames.RootFrame.PixelBuffer; + Buffer2D buffer2 = image2.Frames.RootFrame.PixelBuffer; + Buffer2D buffer3 = image3.Frames.RootFrame.PixelBuffer; + + buffer1.FastMemoryGroup.IncreaseRefCounts(); + buffer2.FastMemoryGroup.IncreaseRefCounts(); + buffer3.FastMemoryGroup.IncreaseRefCounts(); + + try + { + var accessor1 = new PixelAccessor(buffer1); + var accessor2 = new PixelAccessor(buffer2); + var accessor3 = new PixelAccessor(buffer3); + processPixels(accessor1, accessor2, accessor3); + } + finally + { + buffer3.FastMemoryGroup.DecreaseRefCounts(); + buffer2.FastMemoryGroup.DecreaseRefCounts(); + buffer1.FastMemoryGroup.DecreaseRefCounts(); + } } /// - /// Gets the representation of the pixels as a in the source image's pixel format + /// Copy image pixels to . + /// + /// The to copy image pixels to. + public void CopyPixelDataTo(Span destination) => this.GetPixelMemoryGroup().CopyTo(destination); + + /// + /// Copy image pixels to . + /// + /// The of to copy image pixels to. + public void CopyPixelDataTo(Span destination) => this.GetPixelMemoryGroup().CopyTo(MemoryMarshal.Cast(destination)); + + /// + /// 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. + /// + /// To ensure the memory is contiguous, should be set + /// to true, preferably on a non-global configuration instance (not ). + /// + /// WARNING: Disposing or leaking the underlying image while still working with the 's + /// might lead to memory corruption. /// - /// The . - /// The . - public bool TryGetSinglePixelSpan(out Span span) + /// The referencing the image buffer. + /// The indicating the success. + public bool DangerousTryGetSinglePixelMemory(out Memory memory) { IMemoryGroup mg = this.GetPixelMemoryGroup(); - if (mg.Count == 1) + if (mg.Count > 1) { - span = mg[0].Span; - return true; + memory = default; + return false; } - span = default; - return false; + memory = mg.Single(); + return true; } /// diff --git a/src/ImageSharp/IndexedImageFrame{TPixel}.cs b/src/ImageSharp/IndexedImageFrame{TPixel}.cs index 7668d7600a..18b44de82f 100644 --- a/src/ImageSharp/IndexedImageFrame{TPixel}.cs +++ b/src/ImageSharp/IndexedImageFrame{TPixel}.cs @@ -75,11 +75,14 @@ internal IndexedImageFrame(Configuration configuration, int width, int height, R /// /// Gets the representation of the pixels as a of contiguous memory /// at row beginning from the first pixel on that row. + /// + /// WARNING: Disposing or leaking the underlying while still working with it's + /// might lead to memory corruption. /// /// The row index in the pixel buffer. /// The pixel row as a . [MethodImpl(InliningOptions.ShortMethod)] - public ReadOnlySpan GetPixelRowSpan(int rowIndex) + public ReadOnlySpan DangerousGetRowSpan(int rowIndex) => this.GetWritablePixelRowSpanUnsafe(rowIndex); /// @@ -96,7 +99,7 @@ public ReadOnlySpan GetPixelRowSpan(int rowIndex) /// The pixel row as a . [MethodImpl(InliningOptions.ShortMethod)] public Span GetWritablePixelRowSpanUnsafe(int rowIndex) - => this.pixelBuffer.GetRowSpan(rowIndex); + => this.pixelBuffer.DangerousGetRowSpan(rowIndex); /// public void Dispose() diff --git a/src/ImageSharp/Memory/Allocators/AllocationOptions.cs b/src/ImageSharp/Memory/Allocators/AllocationOptions.cs index 3c865f3578..ae856c978c 100644 --- a/src/ImageSharp/Memory/Allocators/AllocationOptions.cs +++ b/src/ImageSharp/Memory/Allocators/AllocationOptions.cs @@ -1,21 +1,24 @@ // Copyright (c) Six Labors. // Licensed under the Apache License, Version 2.0. +using System; + namespace SixLabors.ImageSharp.Memory { /// /// Options for allocating buffers. /// + [Flags] public enum AllocationOptions { /// /// Indicates that the buffer should just be allocated. /// - None, + None = 0, /// /// Indicates that the allocated buffer should be cleaned following allocation. /// - Clean + Clean = 1 } } diff --git a/src/ImageSharp/Memory/Allocators/AllocationOptionsExtensions.cs b/src/ImageSharp/Memory/Allocators/AllocationOptionsExtensions.cs new file mode 100644 index 0000000000..d3e5bca6ee --- /dev/null +++ b/src/ImageSharp/Memory/Allocators/AllocationOptionsExtensions.cs @@ -0,0 +1,10 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Memory +{ + internal static class AllocationOptionsExtensions + { + public static bool Has(this AllocationOptions options, AllocationOptions flag) => (options & flag) == flag; + } +} diff --git a/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.Buffer{T}.cs b/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.Buffer{T}.cs deleted file mode 100644 index 0c35c88286..0000000000 --- a/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.Buffer{T}.cs +++ /dev/null @@ -1,105 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.Buffers; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using SixLabors.ImageSharp.Memory.Internals; - -namespace SixLabors.ImageSharp.Memory -{ - /// - /// Contains and . - /// - public partial class ArrayPoolMemoryAllocator - { - /// - /// The buffer implementation of . - /// - private class Buffer : ManagedBufferBase - where T : struct - { - /// - /// The length of the buffer. - /// - private readonly int length; - - /// - /// A weak reference to the source pool. - /// - /// - /// By using a weak reference here, we are making sure that array pools and their retained arrays are always GC-ed - /// after a call to , regardless of having buffer instances still being in use. - /// - private WeakReference> sourcePoolReference; - - public Buffer(byte[] data, int length, ArrayPool sourcePool) - { - this.Data = data; - this.length = length; - this.sourcePoolReference = new WeakReference>(sourcePool); - } - - /// - /// Gets the buffer as a byte array. - /// - protected byte[] Data { get; private set; } - - /// - public override Span GetSpan() - { - if (this.Data is null) - { - ThrowObjectDisposedException(); - } -#if SUPPORTS_CREATESPAN - ref byte r0 = ref MemoryMarshal.GetReference(this.Data); - return MemoryMarshal.CreateSpan(ref Unsafe.As(ref r0), this.length); -#else - return MemoryMarshal.Cast(this.Data.AsSpan()).Slice(0, this.length); -#endif - - } - - /// - protected override void Dispose(bool disposing) - { - if (!disposing || this.Data is null || this.sourcePoolReference is null) - { - return; - } - - if (this.sourcePoolReference.TryGetTarget(out ArrayPool pool)) - { - pool.Return(this.Data); - } - - this.sourcePoolReference = null; - this.Data = null; - } - - protected override object GetPinnableObject() => this.Data; - - [MethodImpl(InliningOptions.ColdPath)] - private static void ThrowObjectDisposedException() - { - throw new ObjectDisposedException("ArrayPoolMemoryAllocator.Buffer"); - } - } - - /// - /// The implementation of . - /// - private sealed class ManagedByteBuffer : Buffer, IManagedByteBuffer - { - public ManagedByteBuffer(byte[] data, int length, ArrayPool sourcePool) - : base(data, length, sourcePool) - { - } - - /// - public byte[] Array => this.Data; - } - } -} diff --git a/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.CommonFactoryMethods.cs b/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.CommonFactoryMethods.cs deleted file mode 100644 index 8aa1b90634..0000000000 --- a/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.CommonFactoryMethods.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -namespace SixLabors.ImageSharp.Memory -{ - /// - /// Contains common factory methods and configuration constants. - /// - public partial class ArrayPoolMemoryAllocator - { - /// - /// The default value for: maximum size of pooled arrays in bytes. - /// Currently set to 24MB, which is equivalent to 8 megapixels of raw RGBA32 data. - /// - internal const int DefaultMaxPooledBufferSizeInBytes = 24 * 1024 * 1024; - - /// - /// The value for: The threshold to pool arrays in which has less buckets for memory safety. - /// - private const int DefaultBufferSelectorThresholdInBytes = 8 * 1024 * 1024; - - /// - /// The default bucket count for . - /// - private const int DefaultLargePoolBucketCount = 6; - - /// - /// The default bucket count for . - /// - 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. - /// - /// The memory manager. - public static ArrayPoolMemoryAllocator CreateDefault() - { - return new ArrayPoolMemoryAllocator( - DefaultMaxPooledBufferSizeInBytes, - DefaultBufferSelectorThresholdInBytes, - DefaultLargePoolBucketCount, - DefaultNormalPoolBucketCount, - DefaultBufferCapacityInBytes); - } - - /// - /// For environments with very limited memory capabilities, only small buffers like image rows are pooled. - /// - /// The memory manager. - public static ArrayPoolMemoryAllocator CreateWithMinimalPooling() - { - return new ArrayPoolMemoryAllocator(64 * 1024, 32 * 1024, 8, 24); - } - - /// - /// For environments with limited memory capabilities, only small array requests are pooled, which can result in reduced throughput. - /// - /// The memory manager. - public static ArrayPoolMemoryAllocator CreateWithModeratePooling() - { - return new ArrayPoolMemoryAllocator(1024 * 1024, 32 * 1024, 16, 24); - } - - /// - /// For environments where memory capabilities are not an issue, the maximum amount of array requests are pooled which results in optimal throughput. - /// - /// The memory manager. - public static ArrayPoolMemoryAllocator CreateWithAggressivePooling() - { - return new ArrayPoolMemoryAllocator(128 * 1024 * 1024, 32 * 1024 * 1024, 16, 32); - } - } -} diff --git a/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.cs deleted file mode 100644 index a79e042a32..0000000000 --- a/src/ImageSharp/Memory/Allocators/ArrayPoolMemoryAllocator.cs +++ /dev/null @@ -1,185 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.Buffers; -using System.Runtime.CompilerServices; - -namespace SixLabors.ImageSharp.Memory -{ - /// - /// Implements by allocating memory from . - /// - public sealed partial class ArrayPoolMemoryAllocator : MemoryAllocator - { - private readonly int maxArraysPerBucketNormalPool; - - private readonly int maxArraysPerBucketLargePool; - - /// - /// The for small-to-medium buffers which is not kept clean. - /// - private ArrayPool normalArrayPool; - - /// - /// The for huge buffers, which is not kept clean. - /// - private ArrayPool largeArrayPool; - - /// - /// Initializes a new instance of the class. - /// - public ArrayPoolMemoryAllocator() - : this(DefaultMaxPooledBufferSizeInBytes, DefaultBufferSelectorThresholdInBytes) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The maximum size of pooled arrays. Arrays over the thershold are gonna be always allocated. - public ArrayPoolMemoryAllocator(int maxPoolSizeInBytes) - : this(maxPoolSizeInBytes, GetLargeBufferThresholdInBytes(maxPoolSizeInBytes)) - { - } - - /// - /// Initializes a new instance of the class. - /// - /// The maximum size of pooled arrays. Arrays over the thershold are gonna be always allocated. - /// Arrays over this threshold will be pooled in which has less buckets for memory safety. - public ArrayPoolMemoryAllocator(int maxPoolSizeInBytes, int poolSelectorThresholdInBytes) - : this(maxPoolSizeInBytes, poolSelectorThresholdInBytes, DefaultLargePoolBucketCount, DefaultNormalPoolBucketCount) - { - } - - /// - /// 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. - 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; - - this.InitArrayPools(); - } - - /// - /// Gets the maximum size of pooled arrays in bytes. - /// - public int MaxPoolSizeInBytes { get; } - - /// - /// Gets the threshold to pool arrays in which has less buckets for memory safety. - /// - 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; - - ArrayPool pool = this.GetArrayPool(bufferSizeInBytes); - byte[] byteArray = pool.Rent(bufferSizeInBytes); - - var buffer = new Buffer(byteArray, length, pool); - if (options == AllocationOptions.Clean) - { - buffer.GetSpan().Clear(); - } - - return buffer; - } - - /// - public override IManagedByteBuffer AllocateManagedByteBuffer(int length, AllocationOptions options = AllocationOptions.None) - { - Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length)); - - ArrayPool pool = this.GetArrayPool(length); - byte[] byteArray = pool.Rent(length); - - var buffer = new ManagedByteBuffer(byteArray, length, pool); - if (options == AllocationOptions.Clean) - { - buffer.GetSpan().Clear(); - } - - return buffer; - } - - private static int GetLargeBufferThresholdInBytes(int maxPoolSizeInBytes) - { - return maxPoolSizeInBytes / 4; - } - - [MethodImpl(InliningOptions.ColdPath)] - private static void ThrowInvalidAllocationException(int length, int max) => - throw new InvalidMemoryOperationException( - $"Requested allocation: '{length}' elements of '{typeof(T).Name}' is over the capacity in bytes '{max}' of the MemoryAllocator."); - - private ArrayPool GetArrayPool(int bufferSizeInBytes) - { - return bufferSizeInBytes <= this.PoolSelectorThresholdInBytes ? this.normalArrayPool : this.largeArrayPool; - } - - private void InitArrayPools() - { - this.largeArrayPool = ArrayPool.Create(this.MaxPoolSizeInBytes, this.maxArraysPerBucketLargePool); - this.normalArrayPool = ArrayPool.Create(this.PoolSelectorThresholdInBytes, this.maxArraysPerBucketNormalPool); - } - } -} diff --git a/src/ImageSharp/Memory/Allocators/IManagedByteBuffer.cs b/src/ImageSharp/Memory/Allocators/IManagedByteBuffer.cs deleted file mode 100644 index b8298edcde..0000000000 --- a/src/ImageSharp/Memory/Allocators/IManagedByteBuffer.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -using System.Buffers; - -namespace SixLabors.ImageSharp.Memory -{ - /// - /// Represents a byte buffer backed by a managed array. Useful for interop with classic .NET API-s. - /// - public interface IManagedByteBuffer : IMemoryOwner - { - /// - /// Gets the managed array backing this buffer instance. - /// - byte[] Array { get; } - } -} diff --git a/src/ImageSharp/Memory/Allocators/Internals/BasicArrayBuffer.cs b/src/ImageSharp/Memory/Allocators/Internals/BasicArrayBuffer.cs index d708116004..7dbbabff3a 100644 --- a/src/ImageSharp/Memory/Allocators/Internals/BasicArrayBuffer.cs +++ b/src/ImageSharp/Memory/Allocators/Internals/BasicArrayBuffer.cs @@ -2,11 +2,12 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers; namespace SixLabors.ImageSharp.Memory.Internals { /// - /// Wraps an array as an instance. + /// Wraps an array as an instance. /// /// internal class BasicArrayBuffer : ManagedBufferBase diff --git a/src/ImageSharp/Memory/Allocators/Internals/BasicByteBuffer.cs b/src/ImageSharp/Memory/Allocators/Internals/BasicByteBuffer.cs deleted file mode 100644 index e21592a12e..0000000000 --- a/src/ImageSharp/Memory/Allocators/Internals/BasicByteBuffer.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -namespace SixLabors.ImageSharp.Memory.Internals -{ - /// - /// Provides an based on . - /// - internal sealed class BasicByteBuffer : BasicArrayBuffer, IManagedByteBuffer - { - /// - /// Initializes a new instance of the class. - /// - /// The byte array. - internal BasicByteBuffer(byte[] array) - : base(array) - { - } - } -} diff --git a/src/ImageSharp/Memory/Allocators/Internals/Gen2GcCallback.cs b/src/ImageSharp/Memory/Allocators/Internals/Gen2GcCallback.cs new file mode 100644 index 0000000000..b0552936e7 --- /dev/null +++ b/src/ImageSharp/Memory/Allocators/Internals/Gen2GcCallback.cs @@ -0,0 +1,115 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// Port of BCL internal utility: +// https://github.com/dotnet/runtime/blob/57bfe474518ab5b7cfe6bf7424a79ce3af9d6657/src/libraries/System.Private.CoreLib/src/System/Gen2GcCallback.cs +#if NETCOREAPP3_1_OR_GREATER +using System; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Memory.Internals +{ + /// + /// Schedules a callback roughly every gen 2 GC (you may see a Gen 0 an Gen 1 but only once) + /// (We can fix this by capturing the Gen 2 count at startup and testing, but I mostly don't care) + /// + internal sealed class Gen2GcCallback : CriticalFinalizerObject + { + private readonly Func callback0; + private readonly Func callback1; + private GCHandle weakTargetObj; + + private Gen2GcCallback(Func callback) + { + this.callback0 = callback; + } + + private Gen2GcCallback(Func callback, object targetObj) + { + this.callback1 = callback; + this.weakTargetObj = GCHandle.Alloc(targetObj, GCHandleType.Weak); + } + + ~Gen2GcCallback() + { + if (this.weakTargetObj.IsAllocated) + { + // Check to see if the target object is still alive. + object targetObj = this.weakTargetObj.Target; + if (targetObj == null) + { + // The target object is dead, so this callback object is no longer needed. + this.weakTargetObj.Free(); + return; + } + + // Execute the callback method. + try + { + if (!this.callback1(targetObj)) + { + // If the callback returns false, this callback object is no longer needed. + this.weakTargetObj.Free(); + return; + } + } + catch + { + // Ensure that we still get a chance to resurrect this object, even if the callback throws an exception. +#if DEBUG + // Except in DEBUG, as we really shouldn't be hitting any exceptions here. + throw; +#endif + } + } + else + { + // Execute the callback method. + try + { + if (!this.callback0()) + { + // If the callback returns false, this callback object is no longer needed. + return; + } + } + catch + { + // Ensure that we still get a chance to resurrect this object, even if the callback throws an exception. +#if DEBUG + // Except in DEBUG, as we really shouldn't be hitting any exceptions here. + throw; +#endif + } + } + + // Resurrect ourselves by re-registering for finalization. + GC.ReRegisterForFinalize(this); + } + + /// + /// Schedule 'callback' to be called in the next GC. If the callback returns true it is + /// rescheduled for the next Gen 2 GC. Otherwise the callbacks stop. + /// + public static void Register(Func callback) + { + // Create a unreachable object that remembers the callback function and target object. + _ = new Gen2GcCallback(callback); + } + + /// + /// Schedule 'callback' to be called in the next GC. If the callback returns true it is + /// rescheduled for the next Gen 2 GC. Otherwise the callbacks stop. + /// + /// NOTE: This callback will be kept alive until either the callback function returns false, + /// or the target object dies. + /// + public static void Register(Func callback, object targetObj) + { + // Create a unreachable object that remembers the callback function and target object. + _ = new Gen2GcCallback(callback, targetObj); + } + } +} +#endif diff --git a/src/ImageSharp/Memory/Allocators/Internals/IRefCounted.cs b/src/ImageSharp/Memory/Allocators/Internals/IRefCounted.cs new file mode 100644 index 0000000000..363b680483 --- /dev/null +++ b/src/ImageSharp/Memory/Allocators/Internals/IRefCounted.cs @@ -0,0 +1,21 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Memory.Internals +{ + /// + /// Defines an common interface for ref-counted objects. + /// + internal interface IRefCounted + { + /// + /// Increments the reference counter. + /// + void AddRef(); + + /// + /// Decrements the reference counter. + /// + void ReleaseRef(); + } +} diff --git a/src/ImageSharp/Memory/Allocators/Internals/ManagedBufferBase.cs b/src/ImageSharp/Memory/Allocators/Internals/ManagedBufferBase.cs index 296a8bd3a7..7207e9f561 100644 --- a/src/ImageSharp/Memory/Allocators/Internals/ManagedBufferBase.cs +++ b/src/ImageSharp/Memory/Allocators/Internals/ManagedBufferBase.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System.Buffers; +using System.Runtime.CompilerServices; using System.Runtime.InteropServices; namespace SixLabors.ImageSharp.Memory.Internals @@ -23,7 +24,7 @@ public override unsafe MemoryHandle Pin(int elementIndex = 0) this.pinHandle = GCHandle.Alloc(this.GetPinnableObject(), GCHandleType.Pinned); } - void* ptr = (void*)this.pinHandle.AddrOfPinnedObject(); + void* ptr = Unsafe.Add((void*)this.pinHandle.AddrOfPinnedObject(), elementIndex); // We should only pass pinnable:this, when GCHandle lifetime is managed by the MemoryManager instance. return new MemoryHandle(ptr, pinnable: this); diff --git a/src/ImageSharp/Memory/Allocators/Internals/RefCountedLifetimeGuard.cs b/src/ImageSharp/Memory/Allocators/Internals/RefCountedLifetimeGuard.cs new file mode 100644 index 0000000000..61682aa567 --- /dev/null +++ b/src/ImageSharp/Memory/Allocators/Internals/RefCountedLifetimeGuard.cs @@ -0,0 +1,56 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.InteropServices; +using System.Threading; + +namespace SixLabors.ImageSharp.Memory.Internals +{ + /// + /// Implements reference counting lifetime guard mechanism similar to the one provided by , + /// but without the restriction of the guarded object being a handle. + /// + internal abstract class RefCountedLifetimeGuard : IDisposable + { + private int refCount = 1; + private int disposed; + private int released; + + ~RefCountedLifetimeGuard() + { + Interlocked.Exchange(ref this.disposed, 1); + this.ReleaseRef(); + } + + public bool IsDisposed => this.disposed == 1; + + public void AddRef() => Interlocked.Increment(ref this.refCount); + + public void ReleaseRef() + { + Interlocked.Decrement(ref this.refCount); + if (this.refCount == 0) + { + int wasReleased = Interlocked.Exchange(ref this.released, 1); + + if (wasReleased == 0) + { + this.Release(); + } + } + } + + public void Dispose() + { + int wasDisposed = Interlocked.Exchange(ref this.disposed, 1); + if (wasDisposed == 0) + { + this.ReleaseRef(); + GC.SuppressFinalize(this); + } + } + + protected abstract void Release(); + } +} diff --git a/src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs b/src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs new file mode 100644 index 0000000000..21673215ae --- /dev/null +++ b/src/ImageSharp/Memory/Allocators/Internals/SharedArrayPoolBuffer{T}.cs @@ -0,0 +1,80 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace SixLabors.ImageSharp.Memory.Internals +{ + internal class SharedArrayPoolBuffer : ManagedBufferBase, IRefCounted + where T : struct + { + private readonly int lengthInBytes; + private byte[] array; + private LifetimeGuard lifetimeGuard; + + public SharedArrayPoolBuffer(int lengthInElements) + { + this.lengthInBytes = lengthInElements * Unsafe.SizeOf(); + this.array = ArrayPool.Shared.Rent(this.lengthInBytes); + this.lifetimeGuard = new LifetimeGuard(this.array); + } + + protected override void Dispose(bool disposing) + { + if (this.array == null) + { + return; + } + + this.lifetimeGuard.Dispose(); + this.array = null; + } + + public override Span GetSpan() + { + this.CheckDisposed(); + return MemoryMarshal.Cast(this.array.AsSpan(0, this.lengthInBytes)); + } + + protected override object GetPinnableObject() => this.array; + + public void AddRef() + { + this.CheckDisposed(); + this.lifetimeGuard.AddRef(); + } + + public void ReleaseRef() => this.lifetimeGuard.ReleaseRef(); + + [Conditional("DEBUG")] + private void CheckDisposed() + { + if (this.array == null) + { + throw new ObjectDisposedException("SharedArrayPoolBuffer"); + } + } + + private sealed class LifetimeGuard : RefCountedLifetimeGuard + { + private byte[] array; + + public LifetimeGuard(byte[] array) => this.array = array; + + protected override void Release() + { + // If this is called by a finalizer, we will end storing the first array of this bucket + // on the thread local storage of the finalizer thread. + // This is not ideal, but subsequent leaks will end up returning arrays to per-cpu buckets, + // meaning likely a different bucket than it was rented from, + // but this is PROBABLY better than not returning the arrays at all. + ArrayPool.Shared.Return(this.array); + this.array = null; + } + } + } +} diff --git a/src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.LifetimeGuards.cs b/src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.LifetimeGuards.cs new file mode 100644 index 0000000000..666b248552 --- /dev/null +++ b/src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.LifetimeGuards.cs @@ -0,0 +1,65 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Memory.Internals +{ + internal partial class UniformUnmanagedMemoryPool + { + public UnmanagedBuffer CreateGuardedBuffer( + UnmanagedMemoryHandle handle, + int lengthInElements, + bool clear) + where T : struct + { + var buffer = new UnmanagedBuffer(lengthInElements, new ReturnToPoolBufferLifetimeGuard(this, handle)); + if (clear) + { + buffer.Clear(); + } + + return buffer; + } + + public RefCountedLifetimeGuard CreateGroupLifetimeGuard(UnmanagedMemoryHandle[] handles) => new GroupLifetimeGuard(this, handles); + + private sealed class GroupLifetimeGuard : RefCountedLifetimeGuard + { + private readonly UniformUnmanagedMemoryPool pool; + private readonly UnmanagedMemoryHandle[] handles; + + public GroupLifetimeGuard(UniformUnmanagedMemoryPool pool, UnmanagedMemoryHandle[] handles) + { + this.pool = pool; + this.handles = handles; + } + + protected override void Release() + { + if (!this.pool.Return(this.handles)) + { + foreach (UnmanagedMemoryHandle handle in this.handles) + { + handle.Free(); + } + } + } + } + + private sealed class ReturnToPoolBufferLifetimeGuard : UnmanagedBufferLifetimeGuard + { + private readonly UniformUnmanagedMemoryPool pool; + + public ReturnToPoolBufferLifetimeGuard(UniformUnmanagedMemoryPool pool, UnmanagedMemoryHandle handle) + : base(handle) => + this.pool = pool; + + protected override void Release() + { + if (!this.pool.Return(this.Handle)) + { + this.Handle.Free(); + } + } + } + } +} diff --git a/src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.cs b/src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.cs new file mode 100644 index 0000000000..6504787a84 --- /dev/null +++ b/src/ImageSharp/Memory/Allocators/Internals/UniformUnmanagedMemoryPool.cs @@ -0,0 +1,356 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading; + +namespace SixLabors.ImageSharp.Memory.Internals +{ + internal partial class UniformUnmanagedMemoryPool +#if !NETSTANDARD1_3 + // In case UniformUnmanagedMemoryPool is finalized, we prefer to run its finalizer after the guard finalizers, + // but we should not rely on this. + : System.Runtime.ConstrainedExecution.CriticalFinalizerObject +#endif + { + private static int minTrimPeriodMilliseconds = int.MaxValue; + private static readonly List> AllPools = new(); + private static Timer trimTimer; + + private static readonly Stopwatch Stopwatch = Stopwatch.StartNew(); + + private readonly TrimSettings trimSettings; + private readonly UnmanagedMemoryHandle[] buffers; + private int index; + private long lastTrimTimestamp; + private int finalized; + + public UniformUnmanagedMemoryPool(int bufferLength, int capacity) + : this(bufferLength, capacity, TrimSettings.Default) + { + } + + public UniformUnmanagedMemoryPool(int bufferLength, int capacity, TrimSettings trimSettings) + { + this.trimSettings = trimSettings; + this.Capacity = capacity; + this.BufferLength = bufferLength; + this.buffers = new UnmanagedMemoryHandle[capacity]; + + if (trimSettings.Enabled) + { + UpdateTimer(trimSettings, this); +#if NETCOREAPP3_1_OR_GREATER + Gen2GcCallback.Register(s => ((UniformUnmanagedMemoryPool)s).Trim(), this); +#endif + this.lastTrimTimestamp = Stopwatch.ElapsedMilliseconds; + } + } + + // We don't want UniformUnmanagedMemoryPool and MemoryAllocator to be IDisposable, + // since the types don't really match Disposable semantics. + // If a user wants to drop a MemoryAllocator after they finished using it, they should call allocator.ReleaseRetainedResources(), + // which normally should free the already returned (!) buffers. + // However in case if this doesn't happen, we need the retained memory to be freed by the finalizer. + ~UniformUnmanagedMemoryPool() + { + Interlocked.Exchange(ref this.finalized, 1); + this.TrimAll(this.buffers); + } + + public int BufferLength { get; } + + public int Capacity { get; } + + private bool Finalized => this.finalized == 1; + + /// + /// Rent a single buffer. If the pool is full, return . + /// + public UnmanagedMemoryHandle Rent() + { + UnmanagedMemoryHandle[] buffersLocal = this.buffers; + + // Avoid taking the lock if the pool is is over it's limit: + if (this.index == buffersLocal.Length || this.Finalized) + { + return UnmanagedMemoryHandle.NullHandle; + } + + UnmanagedMemoryHandle buffer; + lock (buffersLocal) + { + // Check again after taking the lock: + if (this.index == buffersLocal.Length || this.Finalized) + { + return UnmanagedMemoryHandle.NullHandle; + } + + buffer = buffersLocal[this.index]; + buffersLocal[this.index++] = default; + } + + if (buffer.IsInvalid) + { + buffer = UnmanagedMemoryHandle.Allocate(this.BufferLength); + } + + return buffer; + } + + /// + /// Rent buffers or return 'null' if the pool is full. + /// + public UnmanagedMemoryHandle[] Rent(int bufferCount) + { + UnmanagedMemoryHandle[] buffersLocal = this.buffers; + + // Avoid taking the lock if the pool is is over it's limit: + if (this.index + bufferCount >= buffersLocal.Length + 1 || this.Finalized) + { + return null; + } + + UnmanagedMemoryHandle[] result; + lock (buffersLocal) + { + // Check again after taking the lock: + if (this.index + bufferCount >= buffersLocal.Length + 1 || this.Finalized) + { + return null; + } + + result = new UnmanagedMemoryHandle[bufferCount]; + for (int i = 0; i < bufferCount; i++) + { + result[i] = buffersLocal[this.index]; + buffersLocal[this.index++] = UnmanagedMemoryHandle.NullHandle; + } + } + + for (int i = 0; i < result.Length; i++) + { + if (result[i].IsInvalid) + { + result[i] = UnmanagedMemoryHandle.Allocate(this.BufferLength); + } + } + + return result; + } + + // The Return methods return false if and only if: + // (1) More buffers are returned than rented OR + // (2) The pool has been finalized. + // This is defensive programming, since neither of the cases should happen normally + // (case 1 would be a programming mistake in the library, case 2 should be prevented by the CriticalFinalizerObject contract), + // so we throw in Debug instead of returning false. + // In Release, the caller should Free() the handles if false is returned to avoid memory leaks. + public bool Return(UnmanagedMemoryHandle bufferHandle) + { + Guard.IsTrue(bufferHandle.IsValid, nameof(bufferHandle), "Returning NullHandle to the pool is not allowed."); + lock (this.buffers) + { + if (this.Finalized || this.index == 0) + { + this.DebugThrowInvalidReturn(); + return false; + } + + this.buffers[--this.index] = bufferHandle; + } + + return true; + } + + public bool Return(Span bufferHandles) + { + lock (this.buffers) + { + if (this.Finalized || this.index - bufferHandles.Length + 1 <= 0) + { + this.DebugThrowInvalidReturn(); + return false; + } + + for (int i = bufferHandles.Length - 1; i >= 0; i--) + { + ref UnmanagedMemoryHandle h = ref bufferHandles[i]; + Guard.IsTrue(h.IsValid, nameof(bufferHandles), "Returning NullHandle to the pool is not allowed."); + this.buffers[--this.index] = h; + } + } + + return true; + } + + public void Release() + { + lock (this.buffers) + { + for (int i = this.index; i < this.buffers.Length; i++) + { + ref UnmanagedMemoryHandle buffer = ref this.buffers[i]; + if (buffer.IsInvalid) + { + break; + } + + buffer.Free(); + } + } + } + + [Conditional("DEBUG")] + private void DebugThrowInvalidReturn() + { + if (this.Finalized) + { + throw new ObjectDisposedException( + nameof(UniformUnmanagedMemoryPool), + "Invalid handle return to the pool! The pool has been finalized."); + } + + throw new InvalidOperationException( + "Invalid handle return to the pool! Returning more buffers than rented."); + } + + private static void UpdateTimer(TrimSettings settings, UniformUnmanagedMemoryPool pool) + { + lock (AllPools) + { + AllPools.Add(new WeakReference(pool)); + + // Invoke the timer callback more frequently, than trimSettings.TrimPeriodMilliseconds. + // We are checking in the callback if enough time passed since the last trimming. If not, we do nothing. + int period = settings.TrimPeriodMilliseconds / 4; + if (trimTimer == null) + { + trimTimer = new Timer(_ => TimerCallback(), null, period, period); + } + else if (settings.TrimPeriodMilliseconds < minTrimPeriodMilliseconds) + { + trimTimer.Change(period, period); + } + + minTrimPeriodMilliseconds = Math.Min(minTrimPeriodMilliseconds, settings.TrimPeriodMilliseconds); + } + } + + private static void TimerCallback() + { + lock (AllPools) + { + // Remove lost references from the list: + for (int i = AllPools.Count - 1; i >= 0; i--) + { + if (!AllPools[i].TryGetTarget(out _)) + { + AllPools.RemoveAt(i); + } + } + + foreach (WeakReference weakPoolRef in AllPools) + { + if (weakPoolRef.TryGetTarget(out UniformUnmanagedMemoryPool pool)) + { + pool.Trim(); + } + } + } + } + + private bool Trim() + { + if (this.Finalized) + { + return false; + } + + UnmanagedMemoryHandle[] buffersLocal = this.buffers; + + bool isHighPressure = this.IsHighMemoryPressure(); + + if (isHighPressure) + { + this.TrimAll(buffersLocal); + return true; + } + + long millisecondsSinceLastTrim = Stopwatch.ElapsedMilliseconds - this.lastTrimTimestamp; + if (millisecondsSinceLastTrim > this.trimSettings.TrimPeriodMilliseconds) + { + return this.TrimLowPressure(buffersLocal); + } + + return true; + } + + private void TrimAll(UnmanagedMemoryHandle[] buffersLocal) + { + lock (buffersLocal) + { + // Trim all: + for (int i = this.index; i < buffersLocal.Length && buffersLocal[i].IsValid; i++) + { + buffersLocal[i].Free(); + } + } + } + + private bool TrimLowPressure(UnmanagedMemoryHandle[] buffersLocal) + { + lock (buffersLocal) + { + // Count the buffers in the pool: + int retainedCount = 0; + for (int i = this.index; i < buffersLocal.Length && buffersLocal[i].IsValid; i++) + { + retainedCount++; + } + + // Trim 'trimRate' of 'retainedCount': + int trimCount = (int)Math.Ceiling(retainedCount * this.trimSettings.Rate); + int trimStart = this.index + retainedCount - 1; + int trimStop = this.index + retainedCount - trimCount; + for (int i = trimStart; i >= trimStop; i--) + { + buffersLocal[i].Free(); + } + + this.lastTrimTimestamp = Stopwatch.ElapsedMilliseconds; + } + + return true; + } + + private bool IsHighMemoryPressure() + { +#if NETCOREAPP3_1_OR_GREATER + GCMemoryInfo memoryInfo = GC.GetGCMemoryInfo(); + return memoryInfo.MemoryLoadBytes >= memoryInfo.HighMemoryLoadThresholdBytes * this.trimSettings.HighPressureThresholdRate; +#else + // We don't have high pressure detection triggering full trimming on other platforms, + // to counterpart this, the maximum pool size is small. + return false; +#endif + } + + public class TrimSettings + { + // Trim half of the retained pool buffers every minute. + public int TrimPeriodMilliseconds { get; set; } = 60_000; + + public float Rate { get; set; } = 0.5f; + + // Be more strict about high pressure on 32 bit. + public unsafe float HighPressureThresholdRate { get; set; } = sizeof(IntPtr) == 8 ? 0.9f : 0.6f; + + public bool Enabled => this.Rate > 0; + + public static TrimSettings Default => new TrimSettings(); + } + } +} diff --git a/src/ImageSharp/Memory/Allocators/Internals/UnmanagedBufferLifetimeGuard.cs b/src/ImageSharp/Memory/Allocators/Internals/UnmanagedBufferLifetimeGuard.cs new file mode 100644 index 0000000000..5f0759f203 --- /dev/null +++ b/src/ImageSharp/Memory/Allocators/Internals/UnmanagedBufferLifetimeGuard.cs @@ -0,0 +1,27 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Memory.Internals +{ + /// + /// Defines a strategy for managing unmanaged memory ownership. + /// + internal abstract class UnmanagedBufferLifetimeGuard : RefCountedLifetimeGuard + { + private UnmanagedMemoryHandle handle; + + protected UnmanagedBufferLifetimeGuard(UnmanagedMemoryHandle handle) => this.handle = handle; + + public ref UnmanagedMemoryHandle Handle => ref this.handle; + + public sealed class FreeHandle : UnmanagedBufferLifetimeGuard + { + public FreeHandle(UnmanagedMemoryHandle handle) + : base(handle) + { + } + + protected override void Release() => this.Handle.Free(); + } + } +} diff --git a/src/ImageSharp/Memory/Allocators/Internals/UnmanagedBuffer{T}.cs b/src/ImageSharp/Memory/Allocators/Internals/UnmanagedBuffer{T}.cs new file mode 100644 index 0000000000..5d0c6dd613 --- /dev/null +++ b/src/ImageSharp/Memory/Allocators/Internals/UnmanagedBuffer{T}.cs @@ -0,0 +1,80 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; + +namespace SixLabors.ImageSharp.Memory.Internals +{ + /// + /// Allocates and provides an implementation giving + /// access to unmanaged buffers allocated by . + /// + /// The element type. + internal sealed unsafe class UnmanagedBuffer : MemoryManager, IRefCounted + where T : struct + { + private readonly int lengthInElements; + + private readonly UnmanagedBufferLifetimeGuard lifetimeGuard; + + private int disposed; + + public UnmanagedBuffer(int lengthInElements, UnmanagedBufferLifetimeGuard lifetimeGuard) + { + DebugGuard.NotNull(lifetimeGuard, nameof(lifetimeGuard)); + + this.lengthInElements = lengthInElements; + this.lifetimeGuard = lifetimeGuard; + } + + private void* Pointer => this.lifetimeGuard.Handle.Pointer; + + public override Span GetSpan() + { + DebugGuard.NotDisposed(this.disposed == 1, this.GetType().Name); + DebugGuard.NotDisposed(this.lifetimeGuard.IsDisposed, this.lifetimeGuard.GetType().Name); + return new(this.Pointer, this.lengthInElements); + } + + /// + public override MemoryHandle Pin(int elementIndex = 0) + { + DebugGuard.NotDisposed(this.disposed == 1, this.GetType().Name); + DebugGuard.NotDisposed(this.lifetimeGuard.IsDisposed, this.lifetimeGuard.GetType().Name); + + // Will be released in Unpin + this.lifetimeGuard.AddRef(); + + void* pbData = Unsafe.Add(this.Pointer, elementIndex); + return new MemoryHandle(pbData, pinnable: this); + } + + /// + protected override void Dispose(bool disposing) + { + DebugGuard.IsTrue(disposing, nameof(disposing), "Unmanaged buffers should not have finalizer!"); + + if (Interlocked.Exchange(ref this.disposed, 1) == 1) + { + // Already disposed + return; + } + + this.lifetimeGuard.Dispose(); + } + + /// + public override void Unpin() => this.lifetimeGuard.ReleaseRef(); + + public void AddRef() => this.lifetimeGuard.AddRef(); + + public void ReleaseRef() => this.lifetimeGuard.ReleaseRef(); + + public static UnmanagedBuffer Allocate(int lengthInElements) => + new(lengthInElements, new UnmanagedBufferLifetimeGuard.FreeHandle(UnmanagedMemoryHandle.Allocate(lengthInElements * Unsafe.SizeOf()))); + } +} diff --git a/src/ImageSharp/Memory/Allocators/Internals/UnmanagedMemoryHandle.cs b/src/ImageSharp/Memory/Allocators/Internals/UnmanagedMemoryHandle.cs new file mode 100644 index 0000000000..59d4d5bda4 --- /dev/null +++ b/src/ImageSharp/Memory/Allocators/Internals/UnmanagedMemoryHandle.cs @@ -0,0 +1,140 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; + +namespace SixLabors.ImageSharp.Memory.Internals +{ + /// + /// Encapsulates the functionality around allocating and releasing unmanaged memory. NOT a . + /// + internal struct UnmanagedMemoryHandle : IEquatable + { + // Number of allocation re-attempts when detecting OutOfMemoryException. + private const int MaxAllocationAttempts = 1000; + + // Track allocations for testing purposes: + private static int totalOutstandingHandles; + + private static long totalOomRetries; + + // A Monitor to wait/signal when we are low on memory. + private static object lowMemoryMonitor; + + public static readonly UnmanagedMemoryHandle NullHandle = default; + + private IntPtr handle; + private int lengthInBytes; + + private UnmanagedMemoryHandle(IntPtr handle, int lengthInBytes) + { + this.handle = handle; + this.lengthInBytes = lengthInBytes; + + if (lengthInBytes > 0) + { + GC.AddMemoryPressure(lengthInBytes); + } + + Interlocked.Increment(ref totalOutstandingHandles); + } + + public IntPtr Handle => this.handle; + + public bool IsInvalid => this.Handle == IntPtr.Zero; + + public bool IsValid => this.Handle != IntPtr.Zero; + + public unsafe void* Pointer => (void*)this.Handle; + + /// + /// Gets the total outstanding handle allocations for testing purposes. + /// + internal static int TotalOutstandingHandles => totalOutstandingHandles; + + /// + /// Gets the total number -s retried. + /// + internal static long TotalOomRetries => totalOomRetries; + + public static bool operator ==(UnmanagedMemoryHandle a, UnmanagedMemoryHandle b) => a.Equals(b); + + public static bool operator !=(UnmanagedMemoryHandle a, UnmanagedMemoryHandle b) => !a.Equals(b); + + public static UnmanagedMemoryHandle Allocate(int lengthInBytes) + { + IntPtr handle = AllocateHandle(lengthInBytes); + return new UnmanagedMemoryHandle(handle, lengthInBytes); + } + + private static IntPtr AllocateHandle(int lengthInBytes) + { + int counter = 0; + IntPtr handle = IntPtr.Zero; + while (handle == IntPtr.Zero) + { + try + { + handle = Marshal.AllocHGlobal(lengthInBytes); + } + catch (OutOfMemoryException) + { + // We are low on memory, but expect some memory to be freed soon. + // Block the thread & retry to avoid OOM. + if (counter < MaxAllocationAttempts) + { + counter++; + Interlocked.Increment(ref totalOomRetries); + + Interlocked.CompareExchange(ref lowMemoryMonitor, new object(), null); + Monitor.Enter(lowMemoryMonitor); + Monitor.Wait(lowMemoryMonitor, millisecondsTimeout: 1); + Monitor.Exit(lowMemoryMonitor); + } + else + { + throw; + } + } + } + + return handle; + } + + public void Free() + { + IntPtr h = Interlocked.Exchange(ref this.handle, IntPtr.Zero); + + if (h == IntPtr.Zero) + { + return; + } + + Marshal.FreeHGlobal(h); + Interlocked.Decrement(ref totalOutstandingHandles); + if (this.lengthInBytes > 0) + { + GC.RemoveMemoryPressure(this.lengthInBytes); + } + + if (Volatile.Read(ref lowMemoryMonitor) != null) + { + // We are low on memory. Signal all threads waiting in AllocateHandle(). + Monitor.Enter(lowMemoryMonitor); + Monitor.PulseAll(lowMemoryMonitor); + Monitor.Exit(lowMemoryMonitor); + } + + this.lengthInBytes = 0; + } + + public bool Equals(UnmanagedMemoryHandle other) => this.handle.Equals(other.handle); + + public override bool Equals(object obj) => obj is UnmanagedMemoryHandle other && this.Equals(other); + + public override int GetHashCode() => this.handle.GetHashCode(); + } +} diff --git a/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs index af56b99a08..4df78d9d93 100644 --- a/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs +++ b/src/ImageSharp/Memory/Allocators/MemoryAllocator.cs @@ -11,12 +11,37 @@ namespace SixLabors.ImageSharp.Memory /// public abstract class MemoryAllocator { + /// + /// Gets the default platform-specific global instance that + /// serves as the default value for . + /// + /// This is a get-only property, + /// you should set 's + /// to change the default allocator used by and it's operations. + /// + public static MemoryAllocator Default { get; } = Create(); + /// /// 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(); + /// + /// Creates a default instance of a optimized for the executing platform. + /// + /// The . + public static MemoryAllocator Create() => + new UniformUnmanagedMemoryPoolMemoryAllocator(null); + + /// + /// Creates the default using the provided options. + /// + /// The . + /// The . + public static MemoryAllocator Create(MemoryAllocatorOptions options) => + new UniformUnmanagedMemoryPoolMemoryAllocator(options.MaximumPoolSizeMegabytes); + /// /// Allocates an , holding a of length . /// @@ -29,16 +54,6 @@ public abstract class MemoryAllocator public abstract IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None) where T : struct; - /// - /// Allocates an . - /// - /// 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); - /// /// Releases all retained resources not being in use. /// Eg: by resetting array pools and letting GC to free the arrays. @@ -46,5 +61,20 @@ public abstract IMemoryOwner Allocate(int length, AllocationOptions option public virtual void ReleaseRetainedResources() { } + + /// + /// Allocates a . + /// + /// 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 virtual MemoryGroup AllocateGroup( + long totalLength, + int bufferAlignment, + AllocationOptions options = AllocationOptions.None) + where T : struct + => MemoryGroup.Allocate(this, totalLength, bufferAlignment, options); } } diff --git a/src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs b/src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs new file mode 100644 index 0000000000..22a0410755 --- /dev/null +++ b/src/ImageSharp/Memory/Allocators/MemoryAllocatorOptions.cs @@ -0,0 +1,31 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +namespace SixLabors.ImageSharp.Memory +{ + /// + /// Defines options for creating the default . + /// + public struct MemoryAllocatorOptions + { + private int? maximumPoolSizeMegabytes; + + /// + /// Gets or sets a value defining the maximum size of the 's internal memory pool + /// in Megabytes. means platform default. + /// + public int? MaximumPoolSizeMegabytes + { + get => this.maximumPoolSizeMegabytes; + set + { + if (value.HasValue) + { + Guard.MustBeGreaterThanOrEqualTo(value.Value, 0, nameof(this.MaximumPoolSizeMegabytes)); + } + + this.maximumPoolSizeMegabytes = value; + } + } + } +} diff --git a/src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs index 84494f6856..a53ecbc66e 100644 --- a/src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs +++ b/src/ImageSharp/Memory/Allocators/SimpleGcMemoryAllocator.cs @@ -21,13 +21,5 @@ public override IMemoryOwner Allocate(int length, AllocationOptions option return new BasicArrayBuffer(new T[length]); } - - /// - public override IManagedByteBuffer AllocateManagedByteBuffer(int length, AllocationOptions options = AllocationOptions.None) - { - Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length)); - - return new BasicByteBuffer(new byte[length]); - } } } diff --git a/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs new file mode 100644 index 0000000000..16a3cb73d1 --- /dev/null +++ b/src/ImageSharp/Memory/Allocators/UniformUnmanagedMemoryPoolMemoryAllocator.cs @@ -0,0 +1,164 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using SixLabors.ImageSharp.Memory.Internals; + +namespace SixLabors.ImageSharp.Memory +{ + internal sealed class UniformUnmanagedMemoryPoolMemoryAllocator : MemoryAllocator + { + private const int OneMegabyte = 1 << 20; + + // 4 MB seemed to perform slightly better in benchmarks than 2MB or higher values: + private const int DefaultContiguousPoolBlockSizeBytes = 4 * OneMegabyte; + private const int DefaultNonPoolBlockSizeBytes = 32 * OneMegabyte; + private readonly int sharedArrayPoolThresholdInBytes; + private readonly int poolBufferSizeInBytes; + private readonly int poolCapacity; + private readonly UniformUnmanagedMemoryPool.TrimSettings trimSettings; + + private UniformUnmanagedMemoryPool pool; + private readonly UnmanagedMemoryAllocator nonPoolAllocator; + + public UniformUnmanagedMemoryPoolMemoryAllocator(int? maxPoolSizeMegabytes) + : this( + DefaultContiguousPoolBlockSizeBytes, + maxPoolSizeMegabytes.HasValue ? (long)maxPoolSizeMegabytes.Value * OneMegabyte : GetDefaultMaxPoolSizeBytes(), + DefaultNonPoolBlockSizeBytes) + { + } + + public UniformUnmanagedMemoryPoolMemoryAllocator( + int poolBufferSizeInBytes, + long maxPoolSizeInBytes, + int unmanagedBufferSizeInBytes) + : this( + OneMegabyte, + poolBufferSizeInBytes, + maxPoolSizeInBytes, + unmanagedBufferSizeInBytes) + { + } + + internal UniformUnmanagedMemoryPoolMemoryAllocator( + int sharedArrayPoolThresholdInBytes, + int poolBufferSizeInBytes, + long maxPoolSizeInBytes, + int unmanagedBufferSizeInBytes) + : this( + sharedArrayPoolThresholdInBytes, + poolBufferSizeInBytes, + maxPoolSizeInBytes, + unmanagedBufferSizeInBytes, + UniformUnmanagedMemoryPool.TrimSettings.Default) + { + } + + internal UniformUnmanagedMemoryPoolMemoryAllocator( + int sharedArrayPoolThresholdInBytes, + int poolBufferSizeInBytes, + long maxPoolSizeInBytes, + int unmanagedBufferSizeInBytes, + UniformUnmanagedMemoryPool.TrimSettings trimSettings) + { + this.sharedArrayPoolThresholdInBytes = sharedArrayPoolThresholdInBytes; + this.poolBufferSizeInBytes = poolBufferSizeInBytes; + this.poolCapacity = (int)(maxPoolSizeInBytes / poolBufferSizeInBytes); + this.trimSettings = trimSettings; + this.pool = new UniformUnmanagedMemoryPool(this.poolBufferSizeInBytes, this.poolCapacity, this.trimSettings); + this.nonPoolAllocator = new UnmanagedMemoryAllocator(unmanagedBufferSizeInBytes); + } + + /// + protected internal override int GetBufferCapacityInBytes() => this.poolBufferSizeInBytes; + + /// + public override IMemoryOwner Allocate( + int length, + AllocationOptions options = AllocationOptions.None) + { + Guard.MustBeGreaterThanOrEqualTo(length, 0, nameof(length)); + int lengthInBytes = length * Unsafe.SizeOf(); + + if (lengthInBytes <= this.sharedArrayPoolThresholdInBytes) + { + var buffer = new SharedArrayPoolBuffer(length); + if (options.Has(AllocationOptions.Clean)) + { + buffer.GetSpan().Clear(); + } + + return buffer; + } + + if (lengthInBytes <= this.poolBufferSizeInBytes) + { + UnmanagedMemoryHandle mem = this.pool.Rent(); + if (mem.IsValid) + { + UnmanagedBuffer buffer = this.pool.CreateGuardedBuffer(mem, length, options.Has(AllocationOptions.Clean)); + return buffer; + } + } + + return this.nonPoolAllocator.Allocate(length, options); + } + + /// + internal override MemoryGroup AllocateGroup( + long totalLength, + int bufferAlignment, + AllocationOptions options = AllocationOptions.None) + { + long totalLengthInBytes = totalLength * Unsafe.SizeOf(); + if (totalLengthInBytes <= this.sharedArrayPoolThresholdInBytes) + { + var buffer = new SharedArrayPoolBuffer((int)totalLength); + return MemoryGroup.CreateContiguous(buffer, options.Has(AllocationOptions.Clean)); + } + + if (totalLengthInBytes <= this.poolBufferSizeInBytes) + { + // Optimized path renting single array from the pool + UnmanagedMemoryHandle mem = this.pool.Rent(); + if (mem.IsValid) + { + UnmanagedBuffer buffer = this.pool.CreateGuardedBuffer(mem, (int)totalLength, options.Has(AllocationOptions.Clean)); + return MemoryGroup.CreateContiguous(buffer, options.Has(AllocationOptions.Clean)); + } + } + + // Attempt to rent the whole group from the pool, allocate a group of unmanaged buffers if the attempt fails: + if (MemoryGroup.TryAllocate(this.pool, totalLength, bufferAlignment, options, out MemoryGroup poolGroup)) + { + return poolGroup; + } + + return MemoryGroup.Allocate(this.nonPoolAllocator, totalLength, bufferAlignment, options); + } + + public override void ReleaseRetainedResources() => this.pool.Release(); + + private static long GetDefaultMaxPoolSizeBytes() + { +#if NETCOREAPP3_1_OR_GREATER + // On 64 bit .NET Core 3.1+, set the pool size to a portion of the total available memory. + // There is a bug in GC.GetGCMemoryInfo() on .NET 5 + 32 bit, making TotalAvailableMemoryBytes unreliable: + // https://github.com/dotnet/runtime/issues/55126#issuecomment-876779327 + if (Environment.Is64BitProcess || !RuntimeInformation.FrameworkDescription.StartsWith(".NET 5.0")) + { + GCMemoryInfo info = GC.GetGCMemoryInfo(); + return info.TotalAvailableMemoryBytes / 8; + } +#endif + + // Stick to a conservative value of 128 Megabytes on other platforms and 32 bit .NET 5.0: + return 128 * OneMegabyte; + } + } +} diff --git a/src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs b/src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs new file mode 100644 index 0000000000..74197b0a11 --- /dev/null +++ b/src/ImageSharp/Memory/Allocators/UnmanagedMemoryAllocator.cs @@ -0,0 +1,33 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using SixLabors.ImageSharp.Memory.Internals; + +namespace SixLabors.ImageSharp.Memory +{ + /// + /// A implementation that allocates memory on the unmanaged heap + /// without any pooling. + /// + internal class UnmanagedMemoryAllocator : MemoryAllocator + { + private readonly int bufferCapacityInBytes; + + public UnmanagedMemoryAllocator(int bufferCapacityInBytes) => this.bufferCapacityInBytes = bufferCapacityInBytes; + + protected internal override int GetBufferCapacityInBytes() => this.bufferCapacityInBytes; + + public override IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None) + { + var buffer = UnmanagedBuffer.Allocate(length); + if (options.Has(AllocationOptions.Clean)) + { + buffer.GetSpan().Clear(); + } + + return buffer; + } + } +} diff --git a/src/ImageSharp/Memory/Buffer2DExtensions.cs b/src/ImageSharp/Memory/Buffer2DExtensions.cs index 6458ad7e4c..58143de4ec 100644 --- a/src/ImageSharp/Memory/Buffer2DExtensions.cs +++ b/src/ImageSharp/Memory/Buffer2DExtensions.cs @@ -111,10 +111,16 @@ internal static Buffer2DRegion GetRegion(this Buffer2D buffer) /// The /// The of the buffer internal static Size Size(this Buffer2D buffer) - where T : struct - { - return new Size(buffer.Width, buffer.Height); - } + where T : struct => + new(buffer.Width, buffer.Height); + + /// + /// Gets the bounds of the buffer. + /// + /// The + internal static Rectangle Bounds(this Buffer2D buffer) + where T : struct => + new(0, 0, buffer.Width, buffer.Height); [Conditional("DEBUG")] private static void CheckColumnRegionsDoNotOverlap( diff --git a/src/ImageSharp/Memory/Buffer2DRegion{T}.cs b/src/ImageSharp/Memory/Buffer2DRegion{T}.cs index 8c59889442..d1c39ccbf5 100644 --- a/src/ImageSharp/Memory/Buffer2DRegion{T}.cs +++ b/src/ImageSharp/Memory/Buffer2DRegion{T}.cs @@ -94,7 +94,7 @@ public Span GetRowSpan(int y) int xx = this.Rectangle.X; int width = this.Rectangle.Width; - return this.Buffer.GetRowSpan(yy).Slice(xx, width); + return this.Buffer.DangerousGetRowSpan(yy).Slice(xx, width); } /// @@ -138,7 +138,7 @@ internal ref T GetReferenceToOrigin() { int y = this.Rectangle.Y; int x = this.Rectangle.X; - return ref this.Buffer.GetRowSpan(y)[x]; + return ref this.Buffer.DangerousGetRowSpan(y)[x]; } internal void Clear() diff --git a/src/ImageSharp/Memory/Buffer2D{T}.cs b/src/ImageSharp/Memory/Buffer2D{T}.cs index b62c9beb54..ba39a924ea 100644 --- a/src/ImageSharp/Memory/Buffer2D{T}.cs +++ b/src/ImageSharp/Memory/Buffer2D{T}.cs @@ -19,8 +19,6 @@ namespace SixLabors.ImageSharp.Memory public sealed class Buffer2D : IDisposable where T : struct { - private Memory cachedMemory = default; - /// /// Initializes a new instance of the class. /// @@ -32,11 +30,6 @@ internal Buffer2D(MemoryGroup memoryGroup, int width, int height) this.FastMemoryGroup = memoryGroup; this.Width = width; this.Height = height; - - if (memoryGroup.Count == 1) - { - this.cachedMemory = memoryGroup[0]; - } } /// @@ -82,18 +75,14 @@ internal Buffer2D(MemoryGroup memoryGroup, int width, int height) DebugGuard.MustBeLessThan(x, this.Width, nameof(x)); DebugGuard.MustBeLessThan(y, this.Height, nameof(y)); - return ref this.GetRowSpan(y)[x]; + return ref this.DangerousGetRowSpan(y)[x]; } } /// /// Disposes the instance /// - public void Dispose() - { - this.FastMemoryGroup.Dispose(); - this.cachedMemory = default; - } + public void Dispose() => this.FastMemoryGroup.Dispose(); /// /// Gets a to the row 'y' beginning from the pixel at the first pixel on that row. @@ -106,14 +95,12 @@ public void Dispose() /// The of the pixels in the row. /// Thrown when row index is out of range. [MethodImpl(InliningOptions.ShortMethod)] - public Span GetRowSpan(int y) + public Span DangerousGetRowSpan(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; + return this.GetRowMemoryCore(y).Span; } internal bool TryGetPaddedRowSpan(int y, int padding, out Span paddedSpan) @@ -122,17 +109,6 @@ internal bool TryGetPaddedRowSpan(int y, int padding, out Span paddedSpan) DebugGuard.MustBeLessThan(y, this.Height, nameof(y)); int stride = this.Width + padding; - if (this.cachedMemory.Length > 0) - { - paddedSpan = this.cachedMemory.Span.Slice(y * this.Width); - if (paddedSpan.Length < stride) - { - return false; - } - - paddedSpan = paddedSpan.Slice(0, stride); - return true; - } Memory memory = this.FastMemoryGroup.GetRemainingSliceOfBuffer(y * (long)this.Width); @@ -149,31 +125,8 @@ internal bool TryGetPaddedRowSpan(int y, int padding, out Span paddedSpan) [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); + Span span = this.GetRowMemoryCore(y).Span; + return ref span[x]; } /// @@ -198,11 +151,7 @@ internal Memory GetSafeRowMemory(int y) /// Thrown when the backing group is discontiguous. /// [MethodImpl(InliningOptions.ShortMethod)] - internal Span DangerousGetSingleSpan() - { - // TODO: If we need a public version of this method, we need to cache the non-fast Memory of this.MemoryGroup - return this.cachedMemory.Length != 0 ? this.cachedMemory.Span : this.DangerousGetSingleSpanSlow(); - } + internal Span DangerousGetSingleSpan() => this.FastMemoryGroup.Single().Span; /// /// Gets a to the backing data of if the backing group consists of a single contiguous memory buffer. @@ -213,11 +162,7 @@ internal Span DangerousGetSingleSpan() /// Thrown when the backing group is discontiguous. /// [MethodImpl(InliningOptions.ShortMethod)] - internal Memory DangerousGetSingleMemory() - { - // TODO: If we need a public version of this method, we need to cache the non-fast Memory of this.MemoryGroup - return this.cachedMemory.Length != 0 ? this.cachedMemory : this.DangerousGetSingleMemorySlow(); - } + internal Memory DangerousGetSingleMemory() => this.FastMemoryGroup.Single(); /// /// Swaps the contents of 'destination' with 'source' if the buffers are owned (1), @@ -225,27 +170,14 @@ internal Memory DangerousGetSingleMemory() /// internal static void SwapOrCopyContent(Buffer2D destination, Buffer2D source) { - bool swap = MemoryGroup.SwapOrCopyContent(destination.FastMemoryGroup, source.FastMemoryGroup); - SwapOwnData(destination, source, swap); + MemoryGroup.SwapOrCopyContent(destination.FastMemoryGroup, source.FastMemoryGroup); + SwapOwnData(destination, source); } - [MethodImpl(InliningOptions.ColdPath)] - private Memory GetRowMemorySlow(int y) => this.FastMemoryGroup.GetBoundedSlice(y * (long)this.Width, this.Width); - - [MethodImpl(InliningOptions.ColdPath)] - private Memory DangerousGetSingleMemorySlow() => this.FastMemoryGroup.Single(); - - [MethodImpl(InliningOptions.ColdPath)] - private Span DangerousGetSingleSpanSlow() => this.FastMemoryGroup.Single().Span; - - [MethodImpl(InliningOptions.ColdPath)] - private ref T GetElementSlow(int x, int y) - { - Span span = this.GetRowMemorySlow(y).Span; - return ref span[x]; - } + [MethodImpl(InliningOptions.ShortMethod)] + private Memory GetRowMemoryCore(int y) => this.FastMemoryGroup.GetBoundedSlice(y * (long)this.Width, this.Width); - private static void SwapOwnData(Buffer2D a, Buffer2D b, bool swapCachedMemory) + private static void SwapOwnData(Buffer2D a, Buffer2D b) { Size aSize = a.Size(); Size bSize = b.Size(); @@ -255,13 +187,6 @@ private static void SwapOwnData(Buffer2D a, Buffer2D b, bool swapCachedMem 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/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs index cc2a2f17c9..7c58c9c01e 100644 --- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs +++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Consumed.cs @@ -50,10 +50,7 @@ IEnumerator> IEnumerable>.GetEnumerator() return ((IList>)this.source).GetEnumerator(); } - public override void Dispose() - { - this.View.Invalidate(); - } + 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 index 35290c109e..3b92413833 100644 --- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs +++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.Owned.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory.Internals; namespace SixLabors.ImageSharp.Memory { @@ -17,6 +18,7 @@ internal abstract partial class MemoryGroup public sealed class Owned : MemoryGroup, IEnumerable> { private IMemoryOwner[] memoryOwners; + private RefCountedLifetimeGuard groupLifetimeGuard; public Owned(IMemoryOwner[] memoryOwners, int bufferLength, long totalLength, bool swappable) : base(bufferLength, totalLength) @@ -26,6 +28,16 @@ public Owned(IMemoryOwner[] memoryOwners, int bufferLength, long totalLength, this.View = new MemoryGroupView(this); } + public Owned( + UniformUnmanagedMemoryPool pool, + UnmanagedMemoryHandle[] pooledHandles, + int bufferLength, + long totalLength, + int sizeOfLastBuffer, + AllocationOptions options) + : this(CreateBuffers(pooledHandles, bufferLength, sizeOfLastBuffer, options), bufferLength, totalLength, true) => + this.groupLifetimeGuard = pool.CreateGroupLifetimeGuard(pooledHandles); + public bool Swappable { get; } private bool IsDisposed => this.memoryOwners == null; @@ -49,11 +61,65 @@ public override Memory this[int index] } } + private static IMemoryOwner[] CreateBuffers( + UnmanagedMemoryHandle[] pooledBuffers, + int bufferLength, + int sizeOfLastBuffer, + AllocationOptions options) + { + var result = new IMemoryOwner[pooledBuffers.Length]; + for (int i = 0; i < pooledBuffers.Length - 1; i++) + { + var currentBuffer = ObservedBuffer.Create(pooledBuffers[i], bufferLength, options); + result[i] = currentBuffer; + } + + var lastBuffer = ObservedBuffer.Create(pooledBuffers[pooledBuffers.Length - 1], sizeOfLastBuffer, options); + result[result.Length - 1] = lastBuffer; + return result; + } + /// [MethodImpl(InliningOptions.ShortMethod)] - public override MemoryGroupEnumerator GetEnumerator() + public override MemoryGroupEnumerator GetEnumerator() => new(this); + + public override void IncreaseRefCounts() { - return new MemoryGroupEnumerator(this); + this.EnsureNotDisposed(); + + if (this.groupLifetimeGuard != null) + { + this.groupLifetimeGuard.AddRef(); + } + else + { + foreach (IMemoryOwner memoryOwner in this.memoryOwners) + { + if (memoryOwner is IRefCounted unmanagedBuffer) + { + unmanagedBuffer.AddRef(); + } + } + } + } + + public override void DecreaseRefCounts() + { + this.EnsureNotDisposed(); + if (this.groupLifetimeGuard != null) + { + this.groupLifetimeGuard.ReleaseRef(); + } + else + { + foreach (IMemoryOwner memoryOwner in this.memoryOwners) + { + if (memoryOwner is IRefCounted unmanagedBuffer) + { + unmanagedBuffer.ReleaseRef(); + } + } + } } /// @@ -72,13 +138,21 @@ public override void Dispose() this.View.Invalidate(); - foreach (IMemoryOwner memoryOwner in this.memoryOwners) + if (this.groupLifetimeGuard != null) + { + this.groupLifetimeGuard.Dispose(); + } + else { - memoryOwner.Dispose(); + foreach (IMemoryOwner memoryOwner in this.memoryOwners) + { + memoryOwner.Dispose(); + } } this.memoryOwners = null; this.IsValid = false; + this.groupLifetimeGuard = null; } [MethodImpl(InliningOptions.ShortMethod)] @@ -91,10 +165,7 @@ private void EnsureNotDisposed() } [MethodImpl(MethodImplOptions.NoInlining)] - private static void ThrowObjectDisposedException() - { - throw new ObjectDisposedException(nameof(MemoryGroup)); - } + private static void ThrowObjectDisposedException() => throw new ObjectDisposedException(nameof(MemoryGroup)); internal static void SwapContents(Owned a, Owned b) { @@ -104,20 +175,69 @@ internal static void SwapContents(Owned a, Owned b) IMemoryOwner[] tempOwners = a.memoryOwners; long tempTotalLength = a.TotalLength; int tempBufferLength = a.BufferLength; + RefCountedLifetimeGuard tempGroupOwner = a.groupLifetimeGuard; a.memoryOwners = b.memoryOwners; a.TotalLength = b.TotalLength; a.BufferLength = b.BufferLength; + a.groupLifetimeGuard = b.groupLifetimeGuard; b.memoryOwners = tempOwners; b.TotalLength = tempTotalLength; b.BufferLength = tempBufferLength; + b.groupLifetimeGuard = tempGroupOwner; a.View.Invalidate(); b.View.Invalidate(); a.View = new MemoryGroupView(a); b.View = new MemoryGroupView(b); } + + // When the MemoryGroup points to multiple buffers via `groupLifetimeGuard`, + // the lifetime of the individual buffers is managed by the guard. + // Group buffer IMemoryOwner-s d not manage ownership. + private sealed class ObservedBuffer : MemoryManager + { + private readonly UnmanagedMemoryHandle handle; + private readonly int lengthInElements; + + private ObservedBuffer(UnmanagedMemoryHandle handle, int lengthInElements) + { + this.handle = handle; + this.lengthInElements = lengthInElements; + } + + public static ObservedBuffer Create( + UnmanagedMemoryHandle handle, + int lengthInElements, + AllocationOptions options) + { + var buffer = new ObservedBuffer(handle, lengthInElements); + if (options.Has(AllocationOptions.Clean)) + { + buffer.GetSpan().Clear(); + } + + return buffer; + } + + protected override void Dispose(bool disposing) + { + // No-op. + } + + public override unsafe Span GetSpan() => new(this.handle.Pointer, this.lengthInElements); + + public override unsafe MemoryHandle Pin(int elementIndex = 0) + { + void* pbData = Unsafe.Add(this.handle.Pointer, elementIndex); + return new MemoryHandle(pbData); + } + + public override void Unpin() + { + } + } } } } diff --git a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs index 451a8f7e39..cdd8e6a758 100644 --- a/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs +++ b/src/ImageSharp/Memory/DiscontiguousBuffers/MemoryGroup{T}.cs @@ -6,6 +6,7 @@ using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory.Internals; namespace SixLabors.ImageSharp.Memory { @@ -67,44 +68,45 @@ IEnumerator> IEnumerable>.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 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, + long totalLengthInElements, + int bufferAlignmentInElements, AllocationOptions options = AllocationOptions.None) { + int bufferCapacityInBytes = allocator.GetBufferCapacityInBytes(); Guard.NotNull(allocator, nameof(allocator)); - Guard.MustBeGreaterThanOrEqualTo(totalLength, 0, nameof(totalLength)); - Guard.MustBeGreaterThanOrEqualTo(bufferAlignment, 0, nameof(bufferAlignment)); + Guard.MustBeGreaterThanOrEqualTo(totalLengthInElements, 0, nameof(totalLengthInElements)); + Guard.MustBeGreaterThanOrEqualTo(bufferAlignmentInElements, 0, nameof(bufferAlignmentInElements)); - int blockCapacityInElements = allocator.GetBufferCapacityInBytes() / ElementSize; + int blockCapacityInElements = bufferCapacityInBytes / ElementSize; - if (bufferAlignment > blockCapacityInElements) + if (bufferAlignmentInElements > blockCapacityInElements) { throw new InvalidMemoryOperationException( - $"The buffer capacity of the provided MemoryAllocator is insufficient for the requested buffer alignment: {bufferAlignment}."); + $"The buffer capacity of the provided MemoryAllocator is insufficient for the requested buffer alignment: {bufferAlignmentInElements}."); } - if (totalLength == 0) + if (totalLengthInElements == 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) + int numberOfAlignedSegments = blockCapacityInElements / bufferAlignmentInElements; + int bufferLength = numberOfAlignedSegments * bufferAlignmentInElements; + if (totalLengthInElements > 0 && totalLengthInElements < bufferLength) { - bufferLength = (int)totalLength; + bufferLength = (int)totalLengthInElements; } - int sizeOfLastBuffer = (int)(totalLength % bufferLength); - long bufferCount = totalLength / bufferLength; + int sizeOfLastBuffer = (int)(totalLengthInElements % bufferLength); + long bufferCount = totalLengthInElements / bufferLength; if (sizeOfLastBuffer == 0) { @@ -126,7 +128,75 @@ public static MemoryGroup Allocate( buffers[buffers.Length - 1] = allocator.Allocate(sizeOfLastBuffer, options); } - return new Owned(buffers, bufferLength, totalLength, true); + return new Owned(buffers, bufferLength, totalLengthInElements, true); + } + + public static MemoryGroup CreateContiguous(IMemoryOwner buffer, bool clear) + { + if (clear) + { + buffer.GetSpan().Clear(); + } + + int length = buffer.Memory.Length; + var buffers = new IMemoryOwner[1] { buffer }; + return new Owned(buffers, length, length, true); + } + + public static bool TryAllocate( + UniformUnmanagedMemoryPool pool, + long totalLengthInElements, + int bufferAlignmentInElements, + AllocationOptions options, + out MemoryGroup memoryGroup) + { + Guard.NotNull(pool, nameof(pool)); + Guard.MustBeGreaterThanOrEqualTo(totalLengthInElements, 0, nameof(totalLengthInElements)); + Guard.MustBeGreaterThanOrEqualTo(bufferAlignmentInElements, 0, nameof(bufferAlignmentInElements)); + + int blockCapacityInElements = pool.BufferLength / ElementSize; + + if (bufferAlignmentInElements > blockCapacityInElements) + { + memoryGroup = null; + return false; + } + + if (totalLengthInElements == 0) + { + throw new InvalidMemoryOperationException("Allocating 0 length buffer from UniformByteArrayPool is disallowed"); + } + + int numberOfAlignedSegments = blockCapacityInElements / bufferAlignmentInElements; + int bufferLength = numberOfAlignedSegments * bufferAlignmentInElements; + if (totalLengthInElements > 0 && totalLengthInElements < bufferLength) + { + bufferLength = (int)totalLengthInElements; + } + + int sizeOfLastBuffer = (int)(totalLengthInElements % bufferLength); + int bufferCount = (int)(totalLengthInElements / bufferLength); + + if (sizeOfLastBuffer == 0) + { + sizeOfLastBuffer = bufferLength; + } + else + { + bufferCount++; + } + + UnmanagedMemoryHandle[] arrays = pool.Rent(bufferCount); + + if (arrays == null) + { + // Pool is full + memoryGroup = null; + return false; + } + + memoryGroup = new Owned(pool, arrays, bufferLength, totalLengthInElements, sizeOfLastBuffer, options); + return true; } public static MemoryGroup Wrap(params Memory[] source) @@ -196,5 +266,13 @@ public static bool SwapOrCopyContent(MemoryGroup target, MemoryGroup sourc return false; } } + + public virtual void IncreaseRefCounts() + { + } + + public virtual void DecreaseRefCounts() + { + } } } diff --git a/src/ImageSharp/Memory/MemoryAllocatorExtensions.cs b/src/ImageSharp/Memory/MemoryAllocatorExtensions.cs index 2f70ac05e8..abcf078ac7 100644 --- a/src/ImageSharp/Memory/MemoryAllocatorExtensions.cs +++ b/src/ImageSharp/Memory/MemoryAllocatorExtensions.cs @@ -20,20 +20,50 @@ public static class MemoryAllocatorExtensions /// The memory allocator. /// The buffer width. /// The buffer height. + /// A value indicating whether the allocated buffer should be contiguous, unless bigger than . /// The allocation options. /// The . public static Buffer2D Allocate2D( this MemoryAllocator memoryAllocator, int width, int height, + bool preferContiguosImageBuffers, AllocationOptions options = AllocationOptions.None) where T : struct { long groupLength = (long)width * height; - MemoryGroup memoryGroup = memoryAllocator.AllocateGroup(groupLength, width, options); + MemoryGroup memoryGroup; + if (preferContiguosImageBuffers && groupLength < int.MaxValue) + { + IMemoryOwner buffer = memoryAllocator.Allocate((int)groupLength, options); + memoryGroup = MemoryGroup.CreateContiguous(buffer, false); + } + else + { + memoryGroup = memoryAllocator.AllocateGroup(groupLength, width, options); + } + return new Buffer2D(memoryGroup, width, height); } + /// + /// Allocates a buffer of value type objects interpreted as a 2D region + /// of x elements. + /// + /// The type of buffer items to allocate. + /// The memory allocator. + /// The buffer width. + /// The buffer height. + /// The allocation options. + /// The . + public static Buffer2D Allocate2D( + this MemoryAllocator memoryAllocator, + int width, + int height, + AllocationOptions options = AllocationOptions.None) + where T : struct => + Allocate2D(memoryAllocator, width, height, false, options); + /// /// Allocates a buffer of value type objects interpreted as a 2D region /// of width x height elements. @@ -41,14 +71,32 @@ public static Buffer2D Allocate2D( /// The type of buffer items to allocate. /// The memory allocator. /// The buffer size. + /// A value indicating whether the allocated buffer should be contiguous, unless bigger than . /// The allocation options. /// The . public static Buffer2D Allocate2D( this MemoryAllocator memoryAllocator, Size size, + bool preferContiguosImageBuffers, AllocationOptions options = AllocationOptions.None) where T : struct => - Allocate2D(memoryAllocator, size.Width, size.Height, options); + Allocate2D(memoryAllocator, size.Width, size.Height, preferContiguosImageBuffers, options); + + /// + /// Allocates a buffer of value type objects interpreted as a 2D region + /// of width x height elements. + /// + /// The type of buffer items to allocate. + /// The memory allocator. + /// The buffer size. + /// The allocation options. + /// The . + public static Buffer2D Allocate2D( + this MemoryAllocator memoryAllocator, + Size size, + AllocationOptions options = AllocationOptions.None) + where T : struct => + Allocate2D(memoryAllocator, size.Width, size.Height, false, options); internal static Buffer2D Allocate2DOveraligned( this MemoryAllocator memoryAllocator, @@ -83,22 +131,5 @@ internal static IMemoryOwner AllocatePaddedPixelRowBuffer( int length = (width * pixelSizeInBytes) + paddingInBytes; return memoryAllocator.Allocate(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/UnmanagedMemoryManager{T}.cs b/src/ImageSharp/Memory/UnmanagedMemoryManager{T}.cs index 58eaee320a..8e8d1aa2fc 100644 --- a/src/ImageSharp/Memory/UnmanagedMemoryManager{T}.cs +++ b/src/ImageSharp/Memory/UnmanagedMemoryManager{T}.cs @@ -49,7 +49,7 @@ public override Span GetSpan() /// public override MemoryHandle Pin(int elementIndex = 0) { - return new MemoryHandle(((T*)this.pointer) + elementIndex); + return new MemoryHandle(((T*)this.pointer) + elementIndex, pinnable: this); } /// diff --git a/src/ImageSharp/PixelAccessor{TPixel}.cs b/src/ImageSharp/PixelAccessor{TPixel}.cs new file mode 100644 index 0000000000..36671439ec --- /dev/null +++ b/src/ImageSharp/PixelAccessor{TPixel}.cs @@ -0,0 +1,72 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.PixelFormats; + +namespace SixLabors.ImageSharp +{ + /// + /// A delegate to be executed on a . + /// + /// The pixel type. + public delegate void PixelAccessorAction(PixelAccessor pixelAccessor) + where TPixel : unmanaged, IPixel; + + /// + /// A delegate to be executed on two instances of . + /// + /// The first pixel type. + /// The second pixel type. + public delegate void PixelAccessorAction( + PixelAccessor pixelAccessor1, + PixelAccessor pixelAccessor2) + where TPixel1 : unmanaged, IPixel + where TPixel2 : unmanaged, IPixel; + + /// + /// A delegate to be executed on three instances of . + /// + /// The first pixel type. + /// The second pixel type. + /// The third pixel type. + public delegate void PixelAccessorAction( + PixelAccessor pixelAccessor1, + PixelAccessor pixelAccessor2, + PixelAccessor pixelAccessor3) + where TPixel1 : unmanaged, IPixel + where TPixel2 : unmanaged, IPixel + where TPixel3 : unmanaged, IPixel; + + /// + /// Provides efficient access the pixel buffers of an . + /// + /// The pixel type. + public ref struct PixelAccessor + where TPixel : unmanaged, IPixel + { + private Buffer2D buffer; + + internal PixelAccessor(Buffer2D buffer) => this.buffer = buffer; + + /// + /// Gets the width of the backing . + /// + public int Width => this.buffer.Width; + + /// + /// Gets the height of the backing . + /// + public int Height => this.buffer.Height; + + /// + /// Gets the representation of the pixels as a of contiguous memory + /// at row beginning from the first pixel on that row. + /// + /// The row index. + /// The . + /// Thrown when row index is out of range. + public Span GetRowSpan(int rowIndex) => this.buffer.DangerousGetRowSpan(rowIndex); + } +} diff --git a/src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs b/src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs index af6d32b216..a336cfec36 100644 --- a/src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs +++ b/src/ImageSharp/Processing/Extensions/ProcessingExtensions.IntegralImage.cs @@ -30,12 +30,13 @@ public static Buffer2D CalculateIntegralImage(this Image Buffer2D intImage = configuration.MemoryAllocator.Allocate2D(source.Width, source.Height); ulong sumX0 = 0; + Buffer2D sourceBuffer = source.Frames.RootFrame.PixelBuffer; using (IMemoryOwner tempRow = configuration.MemoryAllocator.Allocate(source.Width)) { Span tempSpan = tempRow.GetSpan(); - Span sourceRow = source.GetPixelRowSpan(0); - Span destRow = intImage.GetRowSpan(0); + Span sourceRow = sourceBuffer.DangerousGetRowSpan(0); + Span destRow = intImage.DangerousGetRowSpan(0); PixelOperations.Instance.ToL8(configuration, sourceRow, tempSpan); @@ -51,8 +52,8 @@ public static Buffer2D CalculateIntegralImage(this Image // All other rows for (int y = 1; y < endY; y++) { - sourceRow = source.GetPixelRowSpan(y); - destRow = intImage.GetRowSpan(y); + sourceRow = sourceBuffer.DangerousGetRowSpan(y); + destRow = intImage.DangerousGetRowSpan(y); PixelOperations.Instance.ToL8(configuration, sourceRow, tempSpan); diff --git a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs index 254ba5a7ed..bf6690dcff 100644 --- a/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Binarization/AdaptiveThresholdProcessor{TPixel}.cs @@ -52,6 +52,8 @@ protected override void OnFrameApply(ImageFrame source) // ClusterSize defines the size of cluster to used to check for average. Tweaked to support up to 4k wide pixels and not more. 4096 / 16 is 256 thus the '-1' byte clusterSize = (byte)Math.Truncate((width / 16f) - 1); + Buffer2D sourceBuffer = source.PixelBuffer; + // Using pooled 2d buffer for integer image table and temp memory to hold Rgb24 converted pixel data. using (Buffer2D intImage = this.Configuration.MemoryAllocator.Allocate2D(width, height)) { @@ -61,7 +63,7 @@ protected override void OnFrameApply(ImageFrame source) ulong sum = 0; for (int y = startY; y < endY; y++) { - Span row = source.GetPixelRowSpan(y); + Span row = sourceBuffer.DangerousGetRowSpan(y); ref TPixel rowRef = ref MemoryMarshal.GetReference(row); ref TPixel color = ref Unsafe.Add(ref rowRef, x); color.ToRgba32(ref rgb); @@ -79,7 +81,7 @@ protected override void OnFrameApply(ImageFrame source) } } - var operation = new RowOperation(intersect, source, intImage, upper, lower, thresholdLimit, clusterSize, startX, endX, startY); + var operation = new RowOperation(intersect, source.PixelBuffer, intImage, upper, lower, thresholdLimit, clusterSize, startX, endX, startY); ParallelRowIterator.IterateRows( configuration, intersect, @@ -90,7 +92,7 @@ protected override void OnFrameApply(ImageFrame source) private readonly struct RowOperation : IRowOperation { private readonly Rectangle bounds; - private readonly ImageFrame source; + private readonly Buffer2D source; private readonly Buffer2D intImage; private readonly TPixel upper; private readonly TPixel lower; @@ -103,7 +105,7 @@ protected override void OnFrameApply(ImageFrame source) [MethodImpl(InliningOptions.ShortMethod)] public RowOperation( Rectangle bounds, - ImageFrame source, + Buffer2D source, Buffer2D intImage, TPixel upper, TPixel lower, @@ -130,7 +132,7 @@ public RowOperation( public void Invoke(int y) { Rgba32 rgb = default; - Span pixelRow = this.source.GetPixelRowSpan(y); + Span pixelRow = this.source.DangerousGetRowSpan(y); for (int x = this.startX; x < this.endX; x++) { diff --git a/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs index 5942c71641..00cb015bcd 100644 --- a/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Binarization/BinaryThresholdProcessor{TPixel}.cs @@ -4,6 +4,7 @@ using System; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors.Binarization @@ -41,7 +42,7 @@ protected override void OnFrameApply(ImageFrame source) var interest = Rectangle.Intersect(sourceRectangle, source.Bounds()); var operation = new RowOperation( interest.X, - source, + source.PixelBuffer, upper, lower, threshold, @@ -59,7 +60,7 @@ protected override void OnFrameApply(ImageFrame source) /// private readonly struct RowOperation : IRowOperation { - private readonly ImageFrame source; + private readonly Buffer2D source; private readonly TPixel upper; private readonly TPixel lower; private readonly byte threshold; @@ -70,7 +71,7 @@ protected override void OnFrameApply(ImageFrame source) [MethodImpl(InliningOptions.ShortMethod)] public RowOperation( int startX, - ImageFrame source, + Buffer2D source, TPixel upper, TPixel lower, byte threshold, @@ -93,7 +94,7 @@ public void Invoke(int y, Span span) TPixel upper = this.upper; TPixel lower = this.lower; - Span rowSpan = this.source.GetPixelRowSpan(y).Slice(this.startX, span.Length); + Span rowSpan = this.source.DangerousGetRowSpan(y).Slice(this.startX, span.Length); PixelOperations.Instance.ToRgb24(this.configuration, rowSpan, span); switch (this.mode) diff --git a/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs index 241ff9db28..a55ce91e3e 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/BokehBlurProcessor{TPixel}.cs @@ -231,11 +231,11 @@ public void Invoke(int y, Span span) int kernelSize = this.kernel.Length; // Clear the target buffer for each row run - Span targetBuffer = this.targetValues.GetRowSpan(y); + Span targetBuffer = this.targetValues.DangerousGetRowSpan(y); targetBuffer.Clear(); // Execute the bulk pixel format conversion for the current row - Span sourceRow = this.sourcePixels.GetRowSpan(y).Slice(boundsX, boundsWidth); + Span sourceRow = this.sourcePixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); PixelOperations.Instance.ToVector4(this.configuration, sourceRow, span); ref Vector4 sourceBase = ref MemoryMarshal.GetReference(span); @@ -295,7 +295,7 @@ public ApplyGammaExposureRowOperation( [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(int y, Span span) { - Span targetRowSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X); + Span targetRowSpan = this.targetPixels.DangerousGetRowSpan(y).Slice(this.bounds.X); PixelOperations.Instance.ToVector4(this.configuration, targetRowSpan.Slice(0, span.Length), span, PixelConversionModifiers.Premultiply); ref Vector4 baseRef = ref MemoryMarshal.GetReference(span); @@ -335,7 +335,7 @@ public ApplyGamma3ExposureRowOperation( [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(int y, Span span) { - Span targetRowSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X); + Span targetRowSpan = this.targetPixels.DangerousGetRowSpan(y).Slice(this.bounds.X); PixelOperations.Instance.ToVector4(this.configuration, targetRowSpan.Slice(0, span.Length), span, PixelConversionModifiers.Premultiply); @@ -378,8 +378,8 @@ public void Invoke(int y) Vector4 low = Vector4.Zero; var high = new Vector4(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity); - Span targetPixelSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X); - Span sourceRowSpan = this.sourceValues.GetRowSpan(y).Slice(this.bounds.X); + Span targetPixelSpan = this.targetPixels.DangerousGetRowSpan(y).Slice(this.bounds.X); + Span sourceRowSpan = this.sourceValues.DangerousGetRowSpan(y).Slice(this.bounds.X); ref Vector4 sourceRef = ref MemoryMarshal.GetReference(sourceRowSpan); for (int x = 0; x < this.bounds.Width; x++) @@ -422,13 +422,13 @@ public ApplyInverseGamma3ExposureRowOperation( [MethodImpl(InliningOptions.ShortMethod)] public unsafe void Invoke(int y) { - Span sourceRowSpan = this.sourceValues.GetRowSpan(y).Slice(this.bounds.X, this.bounds.Width); + Span sourceRowSpan = this.sourceValues.DangerousGetRowSpan(y).Slice(this.bounds.X, this.bounds.Width); ref Vector4 sourceRef = ref MemoryMarshal.GetReference(sourceRowSpan); Numerics.Clamp(MemoryMarshal.Cast(sourceRowSpan), 0, float.PositiveInfinity); Numerics.CubeRootOnXYZ(sourceRowSpan); - Span targetPixelSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X); + Span targetPixelSpan = this.targetPixels.DangerousGetRowSpan(y).Slice(this.bounds.X); PixelOperations.Instance.FromVector4Destructive(this.configuration, sourceRowSpan.Slice(0, this.bounds.Width), targetPixelSpan, PixelConversionModifiers.Premultiply); } diff --git a/src/ImageSharp/Processing/Processors/Convolution/Convolution2DRowOperation{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/Convolution2DRowOperation{TPixel}.cs index 802d1809f2..01288e80fa 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/Convolution2DRowOperation{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/Convolution2DRowOperation{TPixel}.cs @@ -87,7 +87,7 @@ private void Convolve3(int y, Span span) { // Get the precalculated source sample row for this kernel row and copy to our buffer. int sampleY = Unsafe.Add(ref sampleRowBase, kY); - sourceRow = this.sourcePixels.GetRowSpan(sampleY).Slice(boundsX, boundsWidth); + sourceRow = this.sourcePixels.DangerousGetRowSpan(sampleY).Slice(boundsX, boundsWidth); PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); ref Vector4 sourceBase = ref MemoryMarshal.GetReference(sourceBuffer); @@ -110,7 +110,7 @@ private void Convolve3(int y, Span span) // Now we need to combine the values and copy the original alpha values // from the source row. - sourceRow = this.sourcePixels.GetRowSpan(y).Slice(boundsX, boundsWidth); + sourceRow = this.sourcePixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); for (int x = 0; x < sourceRow.Length; x++) @@ -123,7 +123,7 @@ private void Convolve3(int y, Span span) target.W = Unsafe.Add(ref MemoryMarshal.GetReference(sourceBuffer), x).W; } - Span targetRowSpan = this.targetPixels.GetRowSpan(y).Slice(boundsX, boundsWidth); + Span targetRowSpan = this.targetPixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); PixelOperations.Instance.FromVector4Destructive(this.configuration, targetYBuffer, targetRowSpan); } @@ -152,7 +152,7 @@ private void Convolve4(int y, Span span) { // Get the precalculated source sample row for this kernel row and copy to our buffer. int sampleY = Unsafe.Add(ref sampleRowBase, kY); - Span sourceRow = this.sourcePixels.GetRowSpan(sampleY).Slice(boundsX, boundsWidth); + Span sourceRow = this.sourcePixels.DangerousGetRowSpan(sampleY).Slice(boundsX, boundsWidth); PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); Numerics.Premultiply(sourceBuffer); @@ -186,7 +186,7 @@ private void Convolve4(int y, Span span) Numerics.UnPremultiply(targetYBuffer); - Span targetRow = this.targetPixels.GetRowSpan(y).Slice(boundsX, boundsWidth); + Span targetRow = this.targetPixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); PixelOperations.Instance.FromVector4Destructive(this.configuration, targetYBuffer, targetRow); } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs index 3f4809c115..fa58422dc6 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/Convolution2PassProcessor{TPixel}.cs @@ -157,7 +157,7 @@ private void Convolve3(int y, Span span) targetBuffer.Clear(); // Get the precalculated source sample row for this kernel row and copy to our buffer. - Span sourceRow = this.sourcePixels.GetRowSpan(y).Slice(boundsX, boundsWidth); + Span sourceRow = this.sourcePixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); ref Vector4 sourceBase = ref MemoryMarshal.GetReference(sourceBuffer); @@ -187,7 +187,7 @@ private void Convolve3(int y, Span span) } // Now we need to copy the original alpha values from the source row. - sourceRow = this.sourcePixels.GetRowSpan(y).Slice(boundsX, boundsWidth); + sourceRow = this.sourcePixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); targetStart = ref MemoryMarshal.GetReference(targetBuffer); @@ -200,7 +200,7 @@ private void Convolve3(int y, Span span) sourceBase = ref Unsafe.Add(ref sourceBase, 1); } - Span targetRow = this.targetPixels.GetRowSpan(y).Slice(boundsX, boundsWidth); + Span targetRow = this.targetPixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); PixelOperations.Instance.FromVector4Destructive(this.configuration, targetBuffer, targetRow); } @@ -219,7 +219,7 @@ private void Convolve4(int y, Span span) targetBuffer.Clear(); // Get the precalculated source sample row for this kernel row and copy to our buffer. - Span sourceRow = this.sourcePixels.GetRowSpan(y).Slice(boundsX, boundsWidth); + Span sourceRow = this.sourcePixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); Numerics.Premultiply(sourceBuffer); @@ -252,7 +252,7 @@ private void Convolve4(int y, Span span) Numerics.UnPremultiply(targetBuffer); - Span targetRow = this.targetPixels.GetRowSpan(y).Slice(boundsX, boundsWidth); + Span targetRow = this.targetPixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); PixelOperations.Instance.FromVector4Destructive(this.configuration, targetBuffer, targetRow); } } @@ -327,7 +327,7 @@ private void Convolve3(int y, Span span) while (Unsafe.IsAddressLessThan(ref kernelStart, ref kernelEnd)) { // Get the precalculated source sample row for this kernel row and copy to our buffer. - sourceRow = this.sourcePixels.GetRowSpan(sampleRowBase).Slice(boundsX, boundsWidth); + sourceRow = this.sourcePixels.DangerousGetRowSpan(sampleRowBase).Slice(boundsX, boundsWidth); PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); @@ -349,7 +349,7 @@ private void Convolve3(int y, Span span) } // Now we need to copy the original alpha values from the source row. - sourceRow = this.sourcePixels.GetRowSpan(y).Slice(boundsX, boundsWidth); + sourceRow = this.sourcePixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); { ref Vector4 sourceBase = ref MemoryMarshal.GetReference(sourceBuffer); @@ -364,7 +364,7 @@ private void Convolve3(int y, Span span) } } - Span targetRow = this.targetPixels.GetRowSpan(y).Slice(boundsX, boundsWidth); + Span targetRow = this.targetPixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); PixelOperations.Instance.FromVector4Destructive(this.configuration, targetBuffer, targetRow); } @@ -392,7 +392,7 @@ private void Convolve4(int y, Span span) while (Unsafe.IsAddressLessThan(ref kernelStart, ref kernelEnd)) { // Get the precalculated source sample row for this kernel row and copy to our buffer. - sourceRow = this.sourcePixels.GetRowSpan(sampleRowBase).Slice(boundsX, boundsWidth); + sourceRow = this.sourcePixels.DangerousGetRowSpan(sampleRowBase).Slice(boundsX, boundsWidth); PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); @@ -417,7 +417,7 @@ private void Convolve4(int y, Span span) Numerics.UnPremultiply(targetBuffer); - Span targetRow = this.targetPixels.GetRowSpan(y).Slice(boundsX, boundsWidth); + Span targetRow = this.targetPixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); PixelOperations.Instance.FromVector4Destructive(this.configuration, targetBuffer, targetRow); } } diff --git a/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs index 924a1125bd..82b7312778 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/ConvolutionProcessor{TPixel}.cs @@ -118,7 +118,7 @@ public void Invoke(int y, Span span) Span targetBuffer = span.Slice(this.bounds.Width); ref Vector4 targetRowRef = ref MemoryMarshal.GetReference(span); - Span targetRowSpan = this.targetPixels.GetRowSpan(y).Slice(boundsX, boundsWidth); + Span targetRowSpan = this.targetPixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); var state = new ConvolutionState(in this.kernel, this.map); int row = y - this.bounds.Y; @@ -135,7 +135,7 @@ public void Invoke(int y, Span span) { // Get the precalculated source sample row for this kernel row and copy to our buffer. int offsetY = Unsafe.Add(ref sampleRowBase, kY); - sourceRow = this.sourcePixels.GetRowSpan(offsetY).Slice(boundsX, boundsWidth); + sourceRow = this.sourcePixels.DangerousGetRowSpan(offsetY).Slice(boundsX, boundsWidth); PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); ref Vector4 sourceBase = ref MemoryMarshal.GetReference(sourceBuffer); @@ -155,7 +155,7 @@ public void Invoke(int y, Span span) } // Now we need to copy the original alpha values from the source row. - sourceRow = this.sourcePixels.GetRowSpan(y).Slice(boundsX, boundsWidth); + sourceRow = this.sourcePixels.DangerousGetRowSpan(y).Slice(boundsX, boundsWidth); PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); for (int x = 0; x < sourceRow.Length; x++) @@ -174,7 +174,7 @@ public void Invoke(int y, Span span) { // Get the precalculated source sample row for this kernel row and copy to our buffer. int offsetY = Unsafe.Add(ref sampleRowBase, kY); - Span sourceRow = this.sourcePixels.GetRowSpan(offsetY).Slice(boundsX, boundsWidth); + Span sourceRow = this.sourcePixels.DangerousGetRowSpan(offsetY).Slice(boundsX, boundsWidth); PixelOperations.Instance.ToVector4(this.configuration, sourceRow, sourceBuffer); Numerics.Premultiply(sourceBuffer); diff --git a/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs index 27963613e1..360b496c30 100644 --- a/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Convolution/EdgeDetectorCompassProcessor{TPixel}.cs @@ -117,8 +117,8 @@ public RowOperation( [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(int y) { - ref TPixel passPixelsBase = ref MemoryMarshal.GetReference(this.passPixels.GetRowSpan(y)); - ref TPixel targetPixelsBase = ref MemoryMarshal.GetReference(this.targetPixels.GetRowSpan(y)); + ref TPixel passPixelsBase = ref MemoryMarshal.GetReference(this.passPixels.DangerousGetRowSpan(y)); + ref TPixel targetPixelsBase = ref MemoryMarshal.GetReference(this.targetPixels.DangerousGetRowSpan(y)); for (int x = this.minX; x < this.maxX; x++) { diff --git a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs index 0fe2d4b2c3..5b049e55af 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/ErrorDither.cs @@ -5,6 +5,7 @@ using System.Numerics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Quantization; @@ -105,10 +106,11 @@ public void ApplyQuantizationDither( int offsetY = bounds.Top; int offsetX = bounds.Left; float scale = quantizer.Options.DitherScale; + Buffer2D sourceBuffer = source.PixelBuffer; for (int y = bounds.Top; y < bounds.Bottom; y++) { - ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y)); + ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(sourceBuffer.DangerousGetRowSpan(y)); ref byte destinationRowRef = ref MemoryMarshal.GetReference(destination.GetWritablePixelRowSpanUnsafe(y - offsetY)); for (int x = bounds.Left; x < bounds.Right; x++) @@ -134,10 +136,11 @@ public void ApplyPaletteDither( ThrowDefaultInstance(); } + Buffer2D sourceBuffer = source.PixelBuffer; float scale = processor.DitherScale; for (int y = bounds.Top; y < bounds.Bottom; y++) { - ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(source.GetPixelRowSpan(y)); + ref TPixel sourceRowRef = ref MemoryMarshal.GetReference(sourceBuffer.DangerousGetRowSpan(y)); for (int x = bounds.Left; x < bounds.Right; x++) { ref TPixel sourcePixel = ref Unsafe.Add(ref sourceRowRef, x); @@ -171,6 +174,7 @@ internal TPixel Dither( int offset = this.offset; DenseMatrix matrix = this.matrix; + Buffer2D imageBuffer = image.PixelBuffer; // Loop through and distribute the error amongst neighboring pixels. for (int row = 0, targetY = y; row < matrix.Rows; row++, targetY++) @@ -180,7 +184,7 @@ internal TPixel Dither( continue; } - Span rowSpan = image.GetPixelRowSpan(targetY); + Span rowSpan = imageBuffer.DangerousGetRowSpan(targetY); for (int col = 0; col < matrix.Columns; col++) { diff --git a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs index 2f5a5cf85e..da0a852b8c 100644 --- a/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs +++ b/src/ImageSharp/Processing/Processors/Dithering/OrderedDither.cs @@ -3,6 +3,7 @@ using System; using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing.Processors.Quantization; @@ -118,10 +119,11 @@ public void ApplyQuantizationDither( int spread = CalculatePaletteSpread(destination.Palette.Length); float scale = quantizer.Options.DitherScale; + Buffer2D sourceBuffer = source.PixelBuffer; for (int y = bounds.Top; y < bounds.Bottom; y++) { - ReadOnlySpan sourceRow = source.GetPixelRowSpan(y).Slice(bounds.X, bounds.Width); + ReadOnlySpan sourceRow = sourceBuffer.DangerousGetRowSpan(y).Slice(bounds.X, bounds.Width); Span destRow = destination.GetWritablePixelRowSpanUnsafe(y - bounds.Y).Slice(0, sourceRow.Length); for (int x = 0; x < sourceRow.Length; x++) @@ -148,10 +150,11 @@ public void ApplyPaletteDither( int spread = CalculatePaletteSpread(processor.Palette.Length); float scale = processor.DitherScale; + Buffer2D sourceBuffer = source.PixelBuffer; for (int y = bounds.Top; y < bounds.Bottom; y++) { - Span row = source.GetPixelRowSpan(y).Slice(bounds.X, bounds.Width); + Span row = sourceBuffer.DangerousGetRowSpan(y).Slice(bounds.X, bounds.Width); for (int x = 0; x < row.Length; x++) { diff --git a/src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs b/src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs index 9b3dbcaa36..86009b9d9b 100644 --- a/src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs +++ b/src/ImageSharp/Processing/Processors/Drawing/DrawImageProcessor{TPixelBg,TPixelFg}.cs @@ -99,7 +99,7 @@ protected override void OnFrameApply(ImageFrame source) "Cannot draw image because the source image does not overlap the target image."); } - var operation = new RowOperation(source, targetImage, blender, configuration, minX, width, locationY, targetX, this.Opacity); + var operation = new RowOperation(source.PixelBuffer, targetImage.Frames.RootFrame.PixelBuffer, blender, configuration, minX, width, locationY, targetX, this.Opacity); ParallelRowIterator.IterateRows( configuration, workingRect, @@ -111,8 +111,8 @@ protected override void OnFrameApply(ImageFrame source) /// private readonly struct RowOperation : IRowOperation { - private readonly ImageFrame sourceFrame; - private readonly Image targetImage; + private readonly Buffer2D source; + private readonly Buffer2D target; private readonly PixelBlender blender; private readonly Configuration configuration; private readonly int minX; @@ -123,8 +123,8 @@ protected override void OnFrameApply(ImageFrame source) [MethodImpl(InliningOptions.ShortMethod)] public RowOperation( - ImageFrame sourceFrame, - Image targetImage, + Buffer2D source, + Buffer2D target, PixelBlender blender, Configuration configuration, int minX, @@ -133,8 +133,8 @@ public RowOperation( int targetX, float opacity) { - this.sourceFrame = sourceFrame; - this.targetImage = targetImage; + this.source = source; + this.target = target; this.blender = blender; this.configuration = configuration; this.minX = minX; @@ -148,8 +148,8 @@ public RowOperation( [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(int y) { - Span background = this.sourceFrame.GetPixelRowSpan(y).Slice(this.minX, this.width); - Span foreground = this.targetImage.GetPixelRowSpan(y - this.locationY).Slice(this.targetX, this.width); + Span background = this.source.DangerousGetRowSpan(y).Slice(this.minX, this.width); + Span foreground = this.target.DangerousGetRowSpan(y - this.locationY).Slice(this.targetX, this.width); this.blender.Blend(this.configuration, background, background, foreground, this.opacity); } } diff --git a/src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs index 42216417ee..eb18c10f4e 100644 --- a/src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Effects/OilPaintingProcessor{TPixel}.cs @@ -47,7 +47,7 @@ protected override void OnFrameApply(ImageFrame source) source.CopyTo(targetPixels); - var operation = new RowIntervalOperation(this.SourceRectangle, targetPixels, source, this.Configuration, brushSize >> 1, this.definition.Levels); + var operation = new RowIntervalOperation(this.SourceRectangle, targetPixels, source.PixelBuffer, this.Configuration, brushSize >> 1, this.definition.Levels); ParallelRowIterator.IterateRowIntervals( this.Configuration, this.SourceRectangle, @@ -63,7 +63,7 @@ protected override void OnFrameApply(ImageFrame source) { private readonly Rectangle bounds; private readonly Buffer2D targetPixels; - private readonly ImageFrame source; + private readonly Buffer2D source; private readonly Configuration configuration; private readonly int radius; private readonly int levels; @@ -72,7 +72,7 @@ protected override void OnFrameApply(ImageFrame source) public RowIntervalOperation( Rectangle bounds, Buffer2D targetPixels, - ImageFrame source, + Buffer2D source, Configuration configuration, int radius, int levels) @@ -120,7 +120,7 @@ public void Invoke(in RowInterval rows) for (int y = rows.Min; y < rows.Max; y++) { - Span sourceRowPixelSpan = this.source.GetPixelRowSpan(y); + Span sourceRowPixelSpan = this.source.DangerousGetRowSpan(y); Span sourceRowAreaPixelSpan = sourceRowPixelSpan.Slice(this.bounds.X, this.bounds.Width); PixelOperations.Instance.ToVector4(this.configuration, sourceRowAreaPixelSpan, sourceRowAreaVector4Span); @@ -139,7 +139,7 @@ public void Invoke(in RowInterval rows) int offsetY = y + fyr; offsetY = Numerics.Clamp(offsetY, 0, maxY); - Span sourceOffsetRow = this.source.GetPixelRowSpan(offsetY); + Span sourceOffsetRow = this.source.DangerousGetRowSpan(offsetY); for (int fx = 0; fx <= this.radius; fx++) { @@ -176,7 +176,7 @@ public void Invoke(in RowInterval rows) } } - Span targetRowAreaPixelSpan = this.targetPixels.GetRowSpan(y).Slice(this.bounds.X, this.bounds.Width); + Span targetRowAreaPixelSpan = this.targetPixels.DangerousGetRowSpan(y).Slice(this.bounds.X, this.bounds.Width); PixelOperations.Instance.FromVector4Destructive(this.configuration, targetRowAreaVector4Span, targetRowAreaPixelSpan); } diff --git a/src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor{TPixel,TDelegate}.cs b/src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor{TPixel,TDelegate}.cs index 6b63c885a0..bc1445d890 100644 --- a/src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor{TPixel,TDelegate}.cs +++ b/src/ImageSharp/Processing/Processors/Effects/PixelRowDelegateProcessor{TPixel,TDelegate}.cs @@ -50,7 +50,7 @@ public PixelRowDelegateProcessor( protected override void OnFrameApply(ImageFrame source) { var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); - var operation = new RowOperation(interest.X, source, this.Configuration, this.modifiers, this.rowDelegate); + var operation = new RowOperation(interest.X, source.PixelBuffer, this.Configuration, this.modifiers, this.rowDelegate); ParallelRowIterator.IterateRows( this.Configuration, @@ -64,7 +64,7 @@ protected override void OnFrameApply(ImageFrame source) private readonly struct RowOperation : IRowOperation { private readonly int startX; - private readonly ImageFrame source; + private readonly Buffer2D source; private readonly Configuration configuration; private readonly PixelConversionModifiers modifiers; private readonly TDelegate rowProcessor; @@ -72,7 +72,7 @@ protected override void OnFrameApply(ImageFrame source) [MethodImpl(InliningOptions.ShortMethod)] public RowOperation( int startX, - ImageFrame source, + Buffer2D source, Configuration configuration, PixelConversionModifiers modifiers, in TDelegate rowProcessor) @@ -88,7 +88,7 @@ public RowOperation( [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(int y, Span span) { - Span rowSpan = this.source.GetPixelRowSpan(y).Slice(this.startX, span.Length); + Span rowSpan = this.source.DangerousGetRowSpan(y).Slice(this.startX, span.Length); PixelOperations.Instance.ToVector4(this.configuration, rowSpan, span, this.modifiers); // Run the user defined pixel shader to the current row of pixels diff --git a/src/ImageSharp/Processing/Processors/Effects/PixelateProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Effects/PixelateProcessor{TPixel}.cs index 0f307f8f15..f6aecabe10 100644 --- a/src/ImageSharp/Processing/Processors/Effects/PixelateProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Effects/PixelateProcessor{TPixel}.cs @@ -6,6 +6,7 @@ using System.Runtime.CompilerServices; using System.Threading.Tasks; using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors.Effects @@ -48,7 +49,7 @@ protected override void OnFrameApply(ImageFrame source) Parallel.ForEach( range, this.Configuration.GetParallelOptions(), - new RowOperation(interest, size, source).Invoke); + new RowOperation(interest, size, source.PixelBuffer).Invoke); } private readonly struct RowOperation @@ -60,13 +61,13 @@ private readonly struct RowOperation private readonly int maxYIndex; private readonly int size; private readonly int radius; - private readonly ImageFrame source; + private readonly Buffer2D source; [MethodImpl(InliningOptions.ShortMethod)] public RowOperation( Rectangle bounds, int size, - ImageFrame source) + Buffer2D source) { this.minX = bounds.X; this.maxX = bounds.Right; @@ -81,7 +82,7 @@ public RowOperation( [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(int y) { - Span rowSpan = this.source.GetPixelRowSpan(Math.Min(y + this.radius, this.maxYIndex)); + Span rowSpan = this.source.DangerousGetRowSpan(Math.Min(y + this.radius, this.maxYIndex)); for (int x = this.minX; x < this.maxX; x += this.size) { diff --git a/src/ImageSharp/Processing/Processors/Filters/FilterProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Filters/FilterProcessor{TPixel}.cs index d0c8ff40d7..e3323dd6c1 100644 --- a/src/ImageSharp/Processing/Processors/Filters/FilterProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Filters/FilterProcessor{TPixel}.cs @@ -36,7 +36,7 @@ public FilterProcessor(Configuration configuration, FilterProcessor definition, protected override void OnFrameApply(ImageFrame source) { var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); - var operation = new RowOperation(interest.X, source, this.definition.Matrix, this.Configuration); + var operation = new RowOperation(interest.X, source.PixelBuffer, this.definition.Matrix, this.Configuration); ParallelRowIterator.IterateRows( this.Configuration, @@ -50,14 +50,14 @@ protected override void OnFrameApply(ImageFrame source) private readonly struct RowOperation : IRowOperation { private readonly int startX; - private readonly ImageFrame source; + private readonly Buffer2D source; private readonly ColorMatrix matrix; private readonly Configuration configuration; [MethodImpl(InliningOptions.ShortMethod)] public RowOperation( int startX, - ImageFrame source, + Buffer2D source, ColorMatrix matrix, Configuration configuration) { @@ -71,7 +71,7 @@ public RowOperation( [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(int y, Span span) { - Span rowSpan = this.source.GetPixelRowSpan(y).Slice(this.startX, span.Length); + Span rowSpan = this.source.DangerousGetRowSpan(y).Slice(this.startX, span.Length); PixelOperations.Instance.ToVector4(this.configuration, rowSpan, span, PixelConversionModifiers.Scale); ColorNumerics.Transform(span, ref Unsafe.AsRef(this.matrix)); diff --git a/src/ImageSharp/Processing/Processors/Filters/OpaqueProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Filters/OpaqueProcessor{TPixel}.cs index 9bb3644762..a230fc7616 100644 --- a/src/ImageSharp/Processing/Processors/Filters/OpaqueProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Filters/OpaqueProcessor{TPixel}.cs @@ -6,6 +6,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors.Filters @@ -25,20 +26,20 @@ protected override void OnFrameApply(ImageFrame source) { var interest = Rectangle.Intersect(this.SourceRectangle, source.Bounds()); - var operation = new OpaqueRowOperation(this.Configuration, source, interest); + var operation = new OpaqueRowOperation(this.Configuration, source.PixelBuffer, interest); ParallelRowIterator.IterateRows(this.Configuration, interest, in operation); } private readonly struct OpaqueRowOperation : IRowOperation { private readonly Configuration configuration; - private readonly ImageFrame target; + private readonly Buffer2D target; private readonly Rectangle bounds; [MethodImpl(InliningOptions.ShortMethod)] public OpaqueRowOperation( Configuration configuration, - ImageFrame target, + Buffer2D target, Rectangle bounds) { this.configuration = configuration; @@ -50,7 +51,7 @@ public OpaqueRowOperation( [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(int y, Span span) { - Span targetRowSpan = this.target.GetPixelRowSpan(y).Slice(this.bounds.X); + Span targetRowSpan = this.target.DangerousGetRowSpan(y).Slice(this.bounds.X); PixelOperations.Instance.ToVector4(this.configuration, targetRowSpan.Slice(0, span.Length), span, PixelConversionModifiers.Scale); ref Vector4 baseRef = ref MemoryMarshal.GetReference(span); diff --git a/src/ImageSharp/Processing/Processors/ImageProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/ImageProcessor{TPixel}.cs index b0896636ea..b0c81dbd7a 100644 --- a/src/ImageSharp/Processing/Processors/ImageProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/ImageProcessor{TPixel}.cs @@ -45,27 +45,14 @@ protected ImageProcessor(Configuration configuration, Image source, Rect /// void IImageProcessor.Execute() { - try - { - this.BeforeImageApply(); - - foreach (ImageFrame sourceFrame in this.Source.Frames) - { - this.Apply(sourceFrame); - } + this.BeforeImageApply(); - this.AfterImageApply(); - } -#if DEBUG - catch (Exception) + foreach (ImageFrame sourceFrame in this.Source.Frames) { - throw; -#else - catch (Exception ex) - { - throw new ImageProcessingException($"An error occurred when processing the image using {this.GetType().Name}. See the inner exception for more detail.", ex); -#endif + this.Apply(sourceFrame); } + + this.AfterImageApply(); } /// @@ -74,22 +61,9 @@ void IImageProcessor.Execute() /// the source image. public void Apply(ImageFrame source) { - try - { - this.BeforeFrameApply(source); - this.OnFrameApply(source); - this.AfterFrameApply(source); - } -#if DEBUG - catch (Exception) - { - throw; -#else - catch (Exception ex) - { - throw new ImageProcessingException($"An error occurred when processing the image using {this.GetType().Name}. See the inner exception for more detail.", ex); -#endif - } + this.BeforeFrameApply(source); + this.OnFrameApply(source); + this.AfterFrameApply(source); } /// diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs index 883f85be3b..0afdef9057 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationProcessor{TPixel}.cs @@ -80,37 +80,37 @@ protected override void OnFrameApply(ImageFrame source) yStart += tileHeight; } - var operation = new RowIntervalOperation(cdfData, tileYStartPositions, tileWidth, tileHeight, tileCount, halfTileWidth, luminanceLevels, source); + var operation = new RowIntervalOperation(cdfData, tileYStartPositions, tileWidth, tileHeight, tileCount, halfTileWidth, luminanceLevels, source.PixelBuffer); ParallelRowIterator.IterateRowIntervals( this.Configuration, new Rectangle(0, 0, sourceWidth, tileYStartPositions.Count), in operation); // Fix left column - ProcessBorderColumn(source, cdfData, 0, sourceHeight, this.Tiles, tileHeight, xStart: 0, xEnd: halfTileWidth, luminanceLevels); + ProcessBorderColumn(source.PixelBuffer, cdfData, 0, sourceHeight, this.Tiles, tileHeight, xStart: 0, xEnd: halfTileWidth, luminanceLevels); // Fix right column int rightBorderStartX = ((this.Tiles - 1) * tileWidth) + halfTileWidth; - ProcessBorderColumn(source, cdfData, this.Tiles - 1, sourceHeight, this.Tiles, tileHeight, xStart: rightBorderStartX, xEnd: sourceWidth, luminanceLevels); + ProcessBorderColumn(source.PixelBuffer, cdfData, this.Tiles - 1, sourceHeight, this.Tiles, tileHeight, xStart: rightBorderStartX, xEnd: sourceWidth, luminanceLevels); // Fix top row - ProcessBorderRow(source, cdfData, 0, sourceWidth, this.Tiles, tileWidth, yStart: 0, yEnd: halfTileHeight, luminanceLevels); + ProcessBorderRow(source.PixelBuffer, cdfData, 0, sourceWidth, this.Tiles, tileWidth, yStart: 0, yEnd: halfTileHeight, luminanceLevels); // Fix bottom row int bottomBorderStartY = ((this.Tiles - 1) * tileHeight) + halfTileHeight; - ProcessBorderRow(source, cdfData, this.Tiles - 1, sourceWidth, this.Tiles, tileWidth, yStart: bottomBorderStartY, yEnd: sourceHeight, luminanceLevels); + ProcessBorderRow(source.PixelBuffer, cdfData, this.Tiles - 1, sourceWidth, this.Tiles, tileWidth, yStart: bottomBorderStartY, yEnd: sourceHeight, luminanceLevels); // Left top corner - ProcessCornerTile(source, cdfData, 0, 0, xStart: 0, xEnd: halfTileWidth, yStart: 0, yEnd: halfTileHeight, luminanceLevels); + ProcessCornerTile(source.PixelBuffer, cdfData, 0, 0, xStart: 0, xEnd: halfTileWidth, yStart: 0, yEnd: halfTileHeight, luminanceLevels); // Left bottom corner - ProcessCornerTile(source, cdfData, 0, this.Tiles - 1, xStart: 0, xEnd: halfTileWidth, yStart: bottomBorderStartY, yEnd: sourceHeight, luminanceLevels); + ProcessCornerTile(source.PixelBuffer, cdfData, 0, this.Tiles - 1, xStart: 0, xEnd: halfTileWidth, yStart: bottomBorderStartY, yEnd: sourceHeight, luminanceLevels); // Right top corner - ProcessCornerTile(source, cdfData, this.Tiles - 1, 0, xStart: rightBorderStartX, xEnd: sourceWidth, yStart: 0, yEnd: halfTileHeight, luminanceLevels); + ProcessCornerTile(source.PixelBuffer, cdfData, this.Tiles - 1, 0, xStart: rightBorderStartX, xEnd: sourceWidth, yStart: 0, yEnd: halfTileHeight, luminanceLevels); // Right bottom corner - ProcessCornerTile(source, cdfData, this.Tiles - 1, this.Tiles - 1, xStart: rightBorderStartX, xEnd: sourceWidth, yStart: bottomBorderStartY, yEnd: sourceHeight, luminanceLevels); + ProcessCornerTile(source.PixelBuffer, cdfData, this.Tiles - 1, this.Tiles - 1, xStart: rightBorderStartX, xEnd: sourceWidth, yStart: bottomBorderStartY, yEnd: sourceHeight, luminanceLevels); } } @@ -130,7 +130,7 @@ protected override void OnFrameApply(ImageFrame source) /// or 65536 for 16-bit grayscale images. /// private static void ProcessCornerTile( - ImageFrame source, + Buffer2D source, CdfTileData cdfData, int cdfX, int cdfY, @@ -142,7 +142,7 @@ private static void ProcessCornerTile( { for (int dy = yStart; dy < yEnd; dy++) { - Span rowSpan = source.GetPixelRowSpan(dy); + Span rowSpan = source.DangerousGetRowSpan(dy); for (int dx = xStart; dx < xEnd; dx++) { ref TPixel pixel = ref rowSpan[dx]; @@ -168,7 +168,7 @@ private static void ProcessCornerTile( /// or 65536 for 16-bit grayscale images. /// private static void ProcessBorderColumn( - ImageFrame source, + Buffer2D source, CdfTileData cdfData, int cdfX, int sourceHeight, @@ -188,7 +188,7 @@ private static void ProcessBorderColumn( int tileY = 0; for (int dy = y; dy < yLimit; dy++) { - Span rowSpan = source.GetPixelRowSpan(dy); + Span rowSpan = source.DangerousGetRowSpan(dy); for (int dx = xStart; dx < xEnd; dx++) { ref TPixel pixel = ref rowSpan[dx]; @@ -220,7 +220,7 @@ private static void ProcessBorderColumn( /// or 65536 for 16-bit grayscale images. /// private static void ProcessBorderRow( - ImageFrame source, + Buffer2D source, CdfTileData cdfData, int cdfY, int sourceWidth, @@ -238,7 +238,7 @@ private static void ProcessBorderRow( { for (int dy = yStart; dy < yEnd; dy++) { - Span rowSpan = source.GetPixelRowSpan(dy); + Span rowSpan = source.DangerousGetRowSpan(dy); int tileX = 0; int xLimit = Math.Min(x + tileWidth, sourceWidth - 1); for (int dx = x; dx < xLimit; dx++) @@ -373,7 +373,7 @@ private static float LinearInterpolation(float left, float right, float t) private readonly int tileCount; private readonly int halfTileWidth; private readonly int luminanceLevels; - private readonly ImageFrame source; + private readonly Buffer2D source; private readonly int sourceWidth; private readonly int sourceHeight; @@ -386,7 +386,7 @@ public RowIntervalOperation( int tileCount, int halfTileWidth, int luminanceLevels, - ImageFrame source) + Buffer2D source) { this.cdfData = cdfData; this.tileYStartPositions = tileYStartPositions; @@ -419,7 +419,7 @@ public void Invoke(in RowInterval rows) int xEnd = Math.Min(x + this.tileWidth, this.sourceWidth); for (int dy = y; dy < yEnd; dy++) { - Span rowSpan = this.source.GetPixelRowSpan(dy); + Span rowSpan = this.source.DangerousGetRowSpan(dy); int tileX = 0; for (int dx = x; dx < xEnd; dx++) { @@ -516,7 +516,7 @@ public void CalculateLookupTables(ImageFrame source, HistogramEqualizati this.tileWidth, this.tileHeight, this.luminanceLevels, - source); + source.PixelBuffer); ParallelRowIterator.IterateRowIntervals( this.configuration, @@ -525,7 +525,7 @@ public void CalculateLookupTables(ImageFrame source, HistogramEqualizati } [MethodImpl(InliningOptions.ShortMethod)] - public Span GetCdfLutSpan(int tileX, int tileY) => this.cdfLutBuffer2D.GetRowSpan(tileY).Slice(tileX * this.luminanceLevels, this.luminanceLevels); + public Span GetCdfLutSpan(int tileX, int tileY) => this.cdfLutBuffer2D.DangerousGetRowSpan(tileY).Slice(tileX * this.luminanceLevels, this.luminanceLevels); /// /// Remaps the grey value with the cdf. @@ -560,7 +560,7 @@ public void Dispose() private readonly int tileWidth; private readonly int tileHeight; private readonly int luminanceLevels; - private readonly ImageFrame source; + private readonly Buffer2D source; private readonly int sourceWidth; private readonly int sourceHeight; @@ -574,7 +574,7 @@ public RowIntervalOperation( int tileWidth, int tileHeight, int luminanceLevels, - ImageFrame source) + Buffer2D source) { this.processor = processor; this.allocator = allocator; @@ -599,7 +599,7 @@ public void Invoke(in RowInterval rows) int cdfY = this.tileYStartPositions[index].CdfY; int y = this.tileYStartPositions[index].Y; int endY = Math.Min(y + this.tileHeight, this.sourceHeight); - Span cdfMinSpan = this.cdfMinBuffer2D.GetRowSpan(cdfY); + Span cdfMinSpan = this.cdfMinBuffer2D.DangerousGetRowSpan(cdfY); cdfMinSpan.Clear(); using IMemoryOwner histogramBuffer = this.allocator.Allocate(this.luminanceLevels); @@ -609,13 +609,13 @@ public void Invoke(in RowInterval rows) for (int x = 0; x < this.sourceWidth; x += this.tileWidth) { histogram.Clear(); - Span cdfLutSpan = this.cdfLutBuffer2D.GetRowSpan(index).Slice(cdfX * this.luminanceLevels, this.luminanceLevels); + Span cdfLutSpan = this.cdfLutBuffer2D.DangerousGetRowSpan(index).Slice(cdfX * this.luminanceLevels, this.luminanceLevels); ref int cdfBase = ref MemoryMarshal.GetReference(cdfLutSpan); int xlimit = Math.Min(x + this.tileWidth, this.sourceWidth); for (int dy = y; dy < endY; dy++) { - Span rowSpan = this.source.GetPixelRowSpan(dy); + Span rowSpan = this.source.DangerousGetRowSpan(dy); for (int dx = x; dx < xlimit; dx++) { int luminance = GetLuminance(rowSpan[dx], this.luminanceLevels); diff --git a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor{TPixel}.cs index f17e0d1e48..5e1e016eea 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/AdaptiveHistogramEqualizationSlidingWindowProcessor{TPixel}.cs @@ -189,7 +189,7 @@ private void CopyPixelRow( y = source.Height - diff - 1; } - // Special cases for the left and the right border where GetPixelRowSpan can not be used. + // Special cases for the left and the right border where DangerousGetRowSpan can not be used. if (x < 0) { rowPixels.Clear(); @@ -224,7 +224,7 @@ private void CopyPixelRow( return; } - this.CopyPixelRowFast(source, rowPixels, x, y, tileWidth, configuration); + this.CopyPixelRowFast(source.PixelBuffer, rowPixels, x, y, tileWidth, configuration); } /// @@ -238,13 +238,13 @@ private void CopyPixelRow( /// The configuration. [MethodImpl(InliningOptions.ShortMethod)] private void CopyPixelRowFast( - ImageFrame source, + Buffer2D source, Span rowPixels, int x, int y, int tileWidth, Configuration configuration) - => PixelOperations.Instance.ToVector4(configuration, source.GetPixelRowSpan(y).Slice(start: x, length: tileWidth), rowPixels); + => PixelOperations.Instance.ToVector4(configuration, source.DangerousGetRowSpan(y).Slice(start: x, length: tileWidth), rowPixels); /// /// Adds a column of grey values to the histogram. @@ -356,7 +356,7 @@ public void Invoke(int x) { if (this.useFastPath) { - this.processor.CopyPixelRowFast(this.source, pixelRow, x - this.swInfos.HalfTileWidth, dy, this.swInfos.TileWidth, this.configuration); + this.processor.CopyPixelRowFast(this.source.PixelBuffer, pixelRow, x - this.swInfos.HalfTileWidth, dy, this.swInfos.TileWidth, this.configuration); } else { @@ -390,7 +390,7 @@ public void Invoke(int x) // Remove top most row from the histogram, mirroring rows which exceeds the borders. if (this.useFastPath) { - this.processor.CopyPixelRowFast(this.source, pixelRow, x - this.swInfos.HalfTileWidth, y - this.swInfos.HalfTileWidth, this.swInfos.TileWidth, this.configuration); + this.processor.CopyPixelRowFast(this.source.PixelBuffer, pixelRow, x - this.swInfos.HalfTileWidth, y - this.swInfos.HalfTileWidth, this.swInfos.TileWidth, this.configuration); } else { @@ -402,7 +402,7 @@ public void Invoke(int x) // Add new bottom row to the histogram, mirroring rows which exceeds the borders. if (this.useFastPath) { - this.processor.CopyPixelRowFast(this.source, pixelRow, x - this.swInfos.HalfTileWidth, y + this.swInfos.HalfTileWidth, this.swInfos.TileWidth, this.configuration); + this.processor.CopyPixelRowFast(this.source.PixelBuffer, pixelRow, x - this.swInfos.HalfTileWidth, y + this.swInfos.HalfTileWidth, this.swInfos.TileWidth, this.configuration); } else { diff --git a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs index 70d3e075da..67970821c2 100644 --- a/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Normalization/GlobalHistogramEqualizationProcessor{TPixel}.cs @@ -53,7 +53,7 @@ protected override void OnFrameApply(ImageFrame source) using IMemoryOwner histogramBuffer = memoryAllocator.Allocate(this.LuminanceLevels, AllocationOptions.Clean); // Build the histogram of the grayscale levels. - var grayscaleOperation = new GrayscaleLevelsRowOperation(interest, histogramBuffer, source, this.LuminanceLevels); + var grayscaleOperation = new GrayscaleLevelsRowOperation(interest, histogramBuffer, source.PixelBuffer, this.LuminanceLevels); ParallelRowIterator.IterateRows( this.Configuration, interest, @@ -76,7 +76,7 @@ ref MemoryMarshal.GetReference(histogram), float numberOfPixelsMinusCdfMin = numberOfPixels - cdfMin; // Apply the cdf to each pixel of the image - var cdfOperation = new CdfApplicationRowOperation(interest, cdfBuffer, source, this.LuminanceLevels, numberOfPixelsMinusCdfMin); + var cdfOperation = new CdfApplicationRowOperation(interest, cdfBuffer, source.PixelBuffer, this.LuminanceLevels, numberOfPixelsMinusCdfMin); ParallelRowIterator.IterateRows( this.Configuration, interest, @@ -90,14 +90,14 @@ ref MemoryMarshal.GetReference(histogram), { private readonly Rectangle bounds; private readonly IMemoryOwner histogramBuffer; - private readonly ImageFrame source; + private readonly Buffer2D source; private readonly int luminanceLevels; [MethodImpl(InliningOptions.ShortMethod)] public GrayscaleLevelsRowOperation( Rectangle bounds, IMemoryOwner histogramBuffer, - ImageFrame source, + Buffer2D source, int luminanceLevels) { this.bounds = bounds; @@ -116,7 +116,7 @@ public GrayscaleLevelsRowOperation( public void Invoke(int y) { ref int histogramBase = ref MemoryMarshal.GetReference(this.histogramBuffer.GetSpan()); - Span pixelRow = this.source.GetPixelRowSpan(y); + Span pixelRow = this.source.DangerousGetRowSpan(y); int levels = this.luminanceLevels; for (int x = 0; x < this.bounds.Width; x++) @@ -136,7 +136,7 @@ public void Invoke(int y) { private readonly Rectangle bounds; private readonly IMemoryOwner cdfBuffer; - private readonly ImageFrame source; + private readonly Buffer2D source; private readonly int luminanceLevels; private readonly float numberOfPixelsMinusCdfMin; @@ -144,7 +144,7 @@ public void Invoke(int y) public CdfApplicationRowOperation( Rectangle bounds, IMemoryOwner cdfBuffer, - ImageFrame source, + Buffer2D source, int luminanceLevels, float numberOfPixelsMinusCdfMin) { @@ -165,7 +165,7 @@ public CdfApplicationRowOperation( public void Invoke(int y) { ref int cdfBase = ref MemoryMarshal.GetReference(this.cdfBuffer.GetSpan()); - Span pixelRow = this.source.GetPixelRowSpan(y); + Span pixelRow = this.source.DangerousGetRowSpan(y); int levels = this.luminanceLevels; float noOfPixelsMinusCdfMin = this.numberOfPixelsMinusCdfMin; diff --git a/src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor{TPixel}.cs index 76dcc2194b..636738ca7b 100644 --- a/src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Overlays/BackgroundColorProcessor{TPixel}.cs @@ -49,7 +49,7 @@ protected override void OnFrameApply(ImageFrame source) PixelBlender blender = PixelOperations.Instance.GetPixelBlender(graphicsOptions); - var operation = new RowOperation(configuration, interest, blender, amount, colors, source); + var operation = new RowOperation(configuration, interest, blender, amount, colors, source.PixelBuffer); ParallelRowIterator.IterateRows( configuration, interest, @@ -63,7 +63,7 @@ protected override void OnFrameApply(ImageFrame source) private readonly PixelBlender blender; private readonly IMemoryOwner amount; private readonly IMemoryOwner colors; - private readonly ImageFrame source; + private readonly Buffer2D source; [MethodImpl(InliningOptions.ShortMethod)] public RowOperation( @@ -72,7 +72,7 @@ public RowOperation( PixelBlender blender, IMemoryOwner amount, IMemoryOwner colors, - ImageFrame source) + Buffer2D source) { this.configuration = configuration; this.bounds = bounds; @@ -86,7 +86,7 @@ public RowOperation( public void Invoke(int y) { Span destination = - this.source.GetPixelRowSpan(y) + this.source.DangerousGetRowSpan(y) .Slice(this.bounds.X, this.bounds.Width); // Switch color & destination in the 2nd and 3rd places because we are diff --git a/src/ImageSharp/Processing/Processors/Overlays/GlowProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Overlays/GlowProcessor{TPixel}.cs index 78cf7f3c61..3316090899 100644 --- a/src/ImageSharp/Processing/Processors/Overlays/GlowProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Overlays/GlowProcessor{TPixel}.cs @@ -55,7 +55,7 @@ protected override void OnFrameApply(ImageFrame source) using IMemoryOwner rowColors = allocator.Allocate(interest.Width); rowColors.GetSpan().Fill(glowColor); - var operation = new RowOperation(configuration, interest, rowColors, this.blender, center, maxDistance, blendPercent, source); + var operation = new RowOperation(configuration, interest, rowColors, this.blender, center, maxDistance, blendPercent, source.PixelBuffer); ParallelRowIterator.IterateRows( configuration, interest, @@ -71,7 +71,7 @@ protected override void OnFrameApply(ImageFrame source) private readonly float maxDistance; private readonly float blendPercent; private readonly IMemoryOwner colors; - private readonly ImageFrame source; + private readonly Buffer2D source; [MethodImpl(InliningOptions.ShortMethod)] public RowOperation( @@ -82,7 +82,7 @@ public RowOperation( Vector2 center, float maxDistance, float blendPercent, - ImageFrame source) + Buffer2D source) { this.configuration = configuration; this.bounds = bounds; @@ -105,7 +105,7 @@ public void Invoke(int y, Span span) span[i] = Numerics.Clamp(this.blendPercent * (1 - (.95F * (distance / this.maxDistance))), 0, 1F); } - Span destination = this.source.GetPixelRowSpan(y).Slice(this.bounds.X, this.bounds.Width); + Span destination = this.source.DangerousGetRowSpan(y).Slice(this.bounds.X, this.bounds.Width); this.blender.Blend( this.configuration, diff --git a/src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor{TPixel}.cs index c853377adc..800613eca5 100644 --- a/src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Overlays/VignetteProcessor{TPixel}.cs @@ -63,7 +63,7 @@ protected override void OnFrameApply(ImageFrame source) using IMemoryOwner rowColors = allocator.Allocate(interest.Width); rowColors.GetSpan().Fill(vignetteColor); - var operation = new RowOperation(configuration, interest, rowColors, this.blender, center, maxDistance, blendPercent, source); + var operation = new RowOperation(configuration, interest, rowColors, this.blender, center, maxDistance, blendPercent, source.PixelBuffer); ParallelRowIterator.IterateRows( configuration, interest, @@ -79,7 +79,7 @@ protected override void OnFrameApply(ImageFrame source) private readonly float maxDistance; private readonly float blendPercent; private readonly IMemoryOwner colors; - private readonly ImageFrame source; + private readonly Buffer2D source; [MethodImpl(InliningOptions.ShortMethod)] public RowOperation( @@ -90,7 +90,7 @@ public RowOperation( Vector2 center, float maxDistance, float blendPercent, - ImageFrame source) + Buffer2D source) { this.configuration = configuration; this.bounds = bounds; @@ -113,7 +113,7 @@ public void Invoke(int y, Span span) span[i] = Numerics.Clamp(this.blendPercent * (.9F * (distance / this.maxDistance)), 0, 1F); } - Span destination = this.source.GetPixelRowSpan(y).Slice(this.bounds.X, this.bounds.Width); + Span destination = this.source.DangerousGetRowSpan(y).Slice(this.bounds.X, this.bounds.Width); this.blender.Blend( this.configuration, diff --git a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs index 311a8aa2e0..e28de54c25 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/OctreeQuantizer{TPixel}.cs @@ -80,7 +80,7 @@ public void AddPaletteColors(Buffer2DRegion pixelRegion) // Loop through each row for (int y = bounds.Top; y < bounds.Bottom; y++) { - Span row = source.GetRowSpan(y).Slice(bounds.Left, bounds.Width); + Span row = source.DangerousGetRowSpan(y).Slice(bounds.Left, bounds.Width); PixelOperations.Instance.ToRgba32(this.Configuration, row, bufferSpan); for (int x = 0; x < bufferSpan.Length; x++) diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs index 93bca60756..574b274752 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizeProcessor{TPixel}.cs @@ -44,11 +44,12 @@ protected override void OnFrameApply(ImageFrame source) ReadOnlySpan paletteSpan = quantized.Palette.Span; int offsetY = interest.Top; int offsetX = interest.Left; + Buffer2D sourceBuffer = source.PixelBuffer; for (int y = interest.Y; y < interest.Height; y++) { - Span row = source.GetPixelRowSpan(y); - ReadOnlySpan quantizedRow = quantized.GetPixelRowSpan(y - offsetY); + Span row = sourceBuffer.DangerousGetRowSpan(y); + ReadOnlySpan quantizedRow = quantized.DangerousGetRowSpan(y - offsetY); for (int x = interest.Left; x < interest.Right; x++) { diff --git a/src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs b/src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs index 6c963bfabd..5aa79d732e 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/QuantizerUtilities.cs @@ -122,6 +122,7 @@ private static void SecondPass( where TPixel : unmanaged, IPixel { IDither dither = quantizer.Options.Dither; + Buffer2D sourceBuffer = source.PixelBuffer; if (dither is null) { @@ -130,7 +131,7 @@ private static void SecondPass( for (int y = bounds.Y; y < bounds.Height; y++) { - Span sourceRow = source.GetPixelRowSpan(y); + Span sourceRow = sourceBuffer.DangerousGetRowSpan(y); Span destinationRow = destination.GetWritablePixelRowSpanUnsafe(y - offsetY); for (int x = bounds.Left; x < bounds.Right; x++) diff --git a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs index 4218810d77..cc53299528 100644 --- a/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Quantization/WuQuantizer{TPixel}.cs @@ -384,7 +384,7 @@ private void Build3DHistogram(Buffer2D source, Rectangle bounds) for (int y = bounds.Top; y < bounds.Bottom; y++) { - Span row = source.GetRowSpan(y).Slice(bounds.Left, bounds.Width); + Span row = source.DangerousGetRowSpan(y).Slice(bounds.Left, bounds.Width); PixelOperations.Instance.ToRgba32(this.Configuration, row, bufferSpan); for (int x = 0; x < bufferSpan.Length; x++) diff --git a/src/ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs index df9c1146b8..dfc6ba1c13 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/CropProcessor{TPixel}.cs @@ -51,7 +51,7 @@ protected override void OnFrameApply(ImageFrame source, ImageFrame source, ImageFrame source; - private readonly ImageFrame destination; + private readonly Buffer2D source; + private readonly Buffer2D destination; /// /// Initializes a new instance of the struct. @@ -75,7 +75,7 @@ protected override void OnFrameApply(ImageFrame source, ImageFrameThe source for the current instance. /// The destination for the current instance. [MethodImpl(InliningOptions.ShortMethod)] - public RowOperation(Rectangle bounds, ImageFrame source, ImageFrame destination) + public RowOperation(Rectangle bounds, Buffer2D source, Buffer2D destination) { this.bounds = bounds; this.source = source; @@ -86,8 +86,8 @@ public RowOperation(Rectangle bounds, ImageFrame source, ImageFrame sourceRow = this.source.GetPixelRowSpan(y).Slice(this.bounds.Left); - Span targetRow = this.destination.GetPixelRowSpan(y - this.bounds.Top); + Span sourceRow = this.source.DangerousGetRowSpan(y).Slice(this.bounds.Left); + Span targetRow = this.destination.DangerousGetRowSpan(y - this.bounds.Top); sourceRow.Slice(0, this.bounds.Width).CopyTo(targetRow); } } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs index 5f04918e09..640527fe7c 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/AffineTransformProcessor{TPixel}.cs @@ -73,7 +73,7 @@ public void ApplyTransform(in TResampler sampler) if (sampler is NearestNeighborResampler) { - var nnOperation = new NNAffineOperation(source, destination, matrix); + var nnOperation = new NNAffineOperation(source.PixelBuffer, destination.PixelBuffer, matrix); ParallelRowIterator.IterateRows( configuration, destination.Bounds(), @@ -84,8 +84,8 @@ public void ApplyTransform(in TResampler sampler) var operation = new AffineOperation( configuration, - source, - destination, + source.PixelBuffer, + destination.PixelBuffer, in sampler, matrix); @@ -97,15 +97,15 @@ public void ApplyTransform(in TResampler sampler) private readonly struct NNAffineOperation : IRowOperation { - private readonly ImageFrame source; - private readonly ImageFrame destination; + private readonly Buffer2D source; + private readonly Buffer2D destination; private readonly Rectangle bounds; private readonly Matrix3x2 matrix; [MethodImpl(InliningOptions.ShortMethod)] public NNAffineOperation( - ImageFrame source, - ImageFrame destination, + Buffer2D source, + Buffer2D destination, Matrix3x2 matrix) { this.source = source; @@ -117,8 +117,7 @@ public NNAffineOperation( [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(int y) { - Buffer2D sourceBuffer = this.source.PixelBuffer; - Span destRow = this.destination.GetPixelRowSpan(y); + Span destRow = this.destination.DangerousGetRowSpan(y); for (int x = 0; x < destRow.Length; x++) { @@ -128,7 +127,7 @@ public void Invoke(int y) if (this.bounds.Contains(px, py)) { - destRow[x] = sourceBuffer.GetElementUnsafe(px, py); + destRow[x] = this.source.GetElementUnsafe(px, py); } } } @@ -138,8 +137,8 @@ public void Invoke(int y) where TResampler : struct, IResampler { private readonly Configuration configuration; - private readonly ImageFrame source; - private readonly ImageFrame destination; + private readonly Buffer2D source; + private readonly Buffer2D destination; private readonly TResampler sampler; private readonly Matrix3x2 matrix; private readonly float yRadius; @@ -148,8 +147,8 @@ public void Invoke(int y) [MethodImpl(InliningOptions.ShortMethod)] public AffineOperation( Configuration configuration, - ImageFrame source, - ImageFrame destination, + Buffer2D source, + Buffer2D destination, in TResampler sampler, Matrix3x2 matrix) { @@ -186,11 +185,9 @@ public void Invoke(in RowInterval rows, Span span) int maxY = this.source.Height - 1; int maxX = this.source.Width - 1; - Buffer2D sourceBuffer = this.source.PixelBuffer; - for (int y = rows.Min; y < rows.Max; y++) { - Span rowSpan = this.destination.GetPixelRowSpan(y); + Span rowSpan = this.destination.DangerousGetRowSpan(y); PixelOperations.Instance.ToVector4( this.configuration, rowSpan, @@ -222,7 +219,7 @@ public void Invoke(in RowInterval rows, Span span) { float xWeight = sampler.GetValue(xK - pX); - Vector4 current = sourceBuffer.GetElementUnsafe(xK, yK).ToScaledVector4(); + Vector4 current = this.source.GetElementUnsafe(xK, yK).ToScaledVector4(); Numerics.Premultiply(ref current); sum += current * xWeight * yWeight; } @@ -251,11 +248,9 @@ private void InvokeMacOSX(in RowInterval rows, Span span) int maxY = this.source.Height - 1; int maxX = this.source.Width - 1; - Buffer2D sourceBuffer = this.source.PixelBuffer; - for (int y = rows.Min; y < rows.Max; y++) { - Span rowSpan = this.destination.GetPixelRowSpan(y); + Span rowSpan = this.destination.DangerousGetRowSpan(y); PixelOperations.Instance.ToVector4( this.configuration, rowSpan, @@ -287,7 +282,7 @@ private void InvokeMacOSX(in RowInterval rows, Span span) { float xWeight = sampler.GetValue(xK - pX); - Vector4 current = sourceBuffer.GetElementUnsafe(xK, yK).ToScaledVector4(); + Vector4 current = this.source.GetElementUnsafe(xK, yK).ToScaledVector4(); Numerics.Premultiply(ref current); sum += current * xWeight * yWeight; } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor{TPixel}.cs index 840881b145..8d15a79e5f 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/FlipProcessor{TPixel}.cs @@ -5,6 +5,7 @@ using System.Buffers; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors.Transforms @@ -38,7 +39,7 @@ protected override void OnFrameApply(ImageFrame source) { // No default needed as we have already set the pixels. case FlipMode.Vertical: - this.FlipX(source, this.Configuration); + this.FlipX(source.PixelBuffer, this.Configuration); break; case FlipMode.Horizontal: this.FlipY(source, this.Configuration); @@ -51,7 +52,7 @@ protected override void OnFrameApply(ImageFrame source) /// /// The source image to apply the process to. /// The configuration. - private void FlipX(ImageFrame source, Configuration configuration) + private void FlipX(Buffer2D source, Configuration configuration) { int height = source.Height; using IMemoryOwner tempBuffer = configuration.MemoryAllocator.Allocate(source.Width); @@ -60,8 +61,8 @@ private void FlipX(ImageFrame source, Configuration configuration) for (int yTop = 0; yTop < height / 2; yTop++) { int yBottom = height - yTop - 1; - Span topRow = source.GetPixelRowSpan(yBottom); - Span bottomRow = source.GetPixelRowSpan(yTop); + Span topRow = source.DangerousGetRowSpan(yBottom); + Span bottomRow = source.DangerousGetRowSpan(yTop); topRow.CopyTo(temp); bottomRow.CopyTo(topRow); temp.CopyTo(bottomRow); @@ -75,7 +76,7 @@ private void FlipX(ImageFrame source, Configuration configuration) /// The configuration. private void FlipY(ImageFrame source, Configuration configuration) { - var operation = new RowOperation(source); + var operation = new RowOperation(source.PixelBuffer); ParallelRowIterator.IterateRows( configuration, source.Bounds(), @@ -84,13 +85,13 @@ private void FlipY(ImageFrame source, Configuration configuration) private readonly struct RowOperation : IRowOperation { - private readonly ImageFrame source; + private readonly Buffer2D source; [MethodImpl(InliningOptions.ShortMethod)] - public RowOperation(ImageFrame source) => this.source = source; + public RowOperation(Buffer2D source) => this.source = source; [MethodImpl(InliningOptions.ShortMethod)] - public void Invoke(int y) => this.source.GetPixelRowSpan(y).Reverse(); + public void Invoke(int y) => this.source.DangerousGetRowSpan(y).Reverse(); } } } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs index 9396a018d3..cf6567629f 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/ProjectiveTransformProcessor{TPixel}.cs @@ -72,7 +72,7 @@ public void ApplyTransform(in TResampler sampler) if (sampler is NearestNeighborResampler) { - var nnOperation = new NNProjectiveOperation(source, destination, matrix); + var nnOperation = new NNProjectiveOperation(source.PixelBuffer, destination.PixelBuffer, matrix); ParallelRowIterator.IterateRows( configuration, destination.Bounds(), @@ -83,8 +83,8 @@ public void ApplyTransform(in TResampler sampler) var operation = new ProjectiveOperation( configuration, - source, - destination, + source.PixelBuffer, + destination.PixelBuffer, in sampler, matrix); @@ -96,15 +96,15 @@ public void ApplyTransform(in TResampler sampler) private readonly struct NNProjectiveOperation : IRowOperation { - private readonly ImageFrame source; - private readonly ImageFrame destination; + private readonly Buffer2D source; + private readonly Buffer2D destination; private readonly Rectangle bounds; private readonly Matrix4x4 matrix; [MethodImpl(InliningOptions.ShortMethod)] public NNProjectiveOperation( - ImageFrame source, - ImageFrame destination, + Buffer2D source, + Buffer2D destination, Matrix4x4 matrix) { this.source = source; @@ -116,8 +116,7 @@ public NNProjectiveOperation( [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(int y) { - Buffer2D sourceBuffer = this.source.PixelBuffer; - Span destRow = this.destination.GetPixelRowSpan(y); + Span destRow = this.destination.DangerousGetRowSpan(y); for (int x = 0; x < destRow.Length; x++) { @@ -127,7 +126,7 @@ public void Invoke(int y) if (this.bounds.Contains(px, py)) { - destRow[x] = sourceBuffer.GetElementUnsafe(px, py); + destRow[x] = this.source.GetElementUnsafe(px, py); } } } @@ -137,8 +136,8 @@ public void Invoke(int y) where TResampler : struct, IResampler { private readonly Configuration configuration; - private readonly ImageFrame source; - private readonly ImageFrame destination; + private readonly Buffer2D source; + private readonly Buffer2D destination; private readonly TResampler sampler; private readonly Matrix4x4 matrix; private readonly float yRadius; @@ -147,8 +146,8 @@ public void Invoke(int y) [MethodImpl(InliningOptions.ShortMethod)] public ProjectiveOperation( Configuration configuration, - ImageFrame source, - ImageFrame destination, + Buffer2D source, + Buffer2D destination, in TResampler sampler, Matrix4x4 matrix) { @@ -185,11 +184,9 @@ public void Invoke(in RowInterval rows, Span span) int maxY = this.source.Height - 1; int maxX = this.source.Width - 1; - Buffer2D sourceBuffer = this.source.PixelBuffer; - for (int y = rows.Min; y < rows.Max; y++) { - Span rowSpan = this.destination.GetPixelRowSpan(y); + Span rowSpan = this.destination.DangerousGetRowSpan(y); PixelOperations.Instance.ToVector4( this.configuration, rowSpan, @@ -221,7 +218,7 @@ public void Invoke(in RowInterval rows, Span span) { float xWeight = sampler.GetValue(xK - pX); - Vector4 current = sourceBuffer.GetElementUnsafe(xK, yK).ToScaledVector4(); + Vector4 current = this.source.GetElementUnsafe(xK, yK).ToScaledVector4(); Numerics.Premultiply(ref current); sum += current * xWeight * yWeight; } @@ -250,11 +247,9 @@ public void InvokeMacOSX(in RowInterval rows, Span span) int maxY = this.source.Height - 1; int maxX = this.source.Width - 1; - Buffer2D sourceBuffer = this.source.PixelBuffer; - for (int y = rows.Min; y < rows.Max; y++) { - Span rowSpan = this.destination.GetPixelRowSpan(y); + Span rowSpan = this.destination.DangerousGetRowSpan(y); PixelOperations.Instance.ToVector4( this.configuration, rowSpan, @@ -286,7 +281,7 @@ public void InvokeMacOSX(in RowInterval rows, Span span) { float xWeight = sampler.GetValue(xK - pX); - Vector4 current = sourceBuffer.GetElementUnsafe(xK, yK).ToScaledVector4(); + Vector4 current = this.source.GetElementUnsafe(xK, yK).ToScaledVector4(); Numerics.Premultiply(ref current); sum += current * xWeight * yWeight; } diff --git a/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor{TPixel}.cs index cce6d68605..234f89a710 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Linear/RotateProcessor{TPixel}.cs @@ -131,7 +131,7 @@ private bool OptimizedApply( /// The configuration. private void Rotate180(ImageFrame source, ImageFrame destination, Configuration configuration) { - var operation = new Rotate180RowOperation(source.Width, source.Height, source, destination); + var operation = new Rotate180RowOperation(source.Width, source.Height, source.PixelBuffer, destination.PixelBuffer); ParallelRowIterator.IterateRows( configuration, source.Bounds(), @@ -146,7 +146,7 @@ private void Rotate180(ImageFrame source, ImageFrame destination /// The configuration. private void Rotate270(ImageFrame source, ImageFrame destination, Configuration configuration) { - var operation = new Rotate270RowIntervalOperation(destination.Bounds(), source.Width, source.Height, source, destination); + var operation = new Rotate270RowIntervalOperation(destination.Bounds(), source.Width, source.Height, source.PixelBuffer, destination.PixelBuffer); ParallelRowIterator.IterateRowIntervals( configuration, source.Bounds(), @@ -161,7 +161,7 @@ private void Rotate270(ImageFrame source, ImageFrame destination /// The configuration. private void Rotate90(ImageFrame source, ImageFrame destination, Configuration configuration) { - var operation = new Rotate90RowOperation(destination.Bounds(), source.Width, source.Height, source, destination); + var operation = new Rotate90RowOperation(destination.Bounds(), source.Width, source.Height, source.PixelBuffer, destination.PixelBuffer); ParallelRowIterator.IterateRows( configuration, source.Bounds(), @@ -172,15 +172,15 @@ private void Rotate90(ImageFrame source, ImageFrame destination, { private readonly int width; private readonly int height; - private readonly ImageFrame source; - private readonly ImageFrame destination; + private readonly Buffer2D source; + private readonly Buffer2D destination; [MethodImpl(InliningOptions.ShortMethod)] public Rotate180RowOperation( int width, int height, - ImageFrame source, - ImageFrame destination) + Buffer2D source, + Buffer2D destination) { this.width = width; this.height = height; @@ -191,8 +191,8 @@ public Rotate180RowOperation( [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(int y) { - Span sourceRow = this.source.GetPixelRowSpan(y); - Span targetRow = this.destination.GetPixelRowSpan(this.height - y - 1); + Span sourceRow = this.source.DangerousGetRowSpan(y); + Span targetRow = this.destination.DangerousGetRowSpan(this.height - y - 1); for (int x = 0; x < this.width; x++) { @@ -206,16 +206,16 @@ public void Invoke(int y) private readonly Rectangle bounds; private readonly int width; private readonly int height; - private readonly ImageFrame source; - private readonly ImageFrame destination; + private readonly Buffer2D source; + private readonly Buffer2D destination; [MethodImpl(InliningOptions.ShortMethod)] public Rotate270RowIntervalOperation( Rectangle bounds, int width, int height, - ImageFrame source, - ImageFrame destination) + Buffer2D source, + Buffer2D destination) { this.bounds = bounds; this.width = width; @@ -229,7 +229,7 @@ public void Invoke(in RowInterval rows) { for (int y = rows.Min; y < rows.Max; y++) { - Span sourceRow = this.source.GetPixelRowSpan(y); + Span sourceRow = this.source.DangerousGetRowSpan(y); for (int x = 0; x < this.width; x++) { int newX = this.height - y - 1; @@ -250,16 +250,16 @@ public void Invoke(in RowInterval rows) private readonly Rectangle bounds; private readonly int width; private readonly int height; - private readonly ImageFrame source; - private readonly ImageFrame destination; + private readonly Buffer2D source; + private readonly Buffer2D destination; [MethodImpl(InliningOptions.ShortMethod)] public Rotate90RowOperation( Rectangle bounds, int width, int height, - ImageFrame source, - ImageFrame destination) + Buffer2D source, + Buffer2D destination) { this.bounds = bounds; this.width = width; @@ -271,7 +271,7 @@ public Rotate90RowOperation( [MethodImpl(InliningOptions.ShortMethod)] public void Invoke(int y) { - Span sourceRow = this.source.GetPixelRowSpan(y); + Span sourceRow = this.source.DangerousGetRowSpan(y); int newX = this.height - y - 1; for (int x = 0; x < this.width; x++) { diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs index 9cc4680602..c9dda5f6bc 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeKernelMap.cs @@ -51,7 +51,7 @@ private ResizeKernelMap( this.sourceLength = sourceLength; this.DestinationLength = destinationLength; this.MaxDiameter = (radius * 2) + 1; - this.data = memoryAllocator.Allocate2D(this.MaxDiameter, bufferHeight, AllocationOptions.Clean); + this.data = memoryAllocator.Allocate2D(this.MaxDiameter, bufferHeight, preferContiguosImageBuffers: true, AllocationOptions.Clean); this.pinHandle = this.data.DangerousGetSingleMemory().Pin(); this.kernels = new ResizeKernel[destinationLength]; this.tempValues = new double[this.MaxDiameter]; @@ -252,7 +252,7 @@ private unsafe ResizeKernel CreateKernel(int dataRowIndex, int left, int right) int length = right - left + 1; this.ValidateSizesForCreateKernel(length, dataRowIndex, left, right); - Span rowSpan = this.data.GetRowSpan(dataRowIndex); + Span rowSpan = this.data.DangerousGetRowSpan(dataRowIndex); ref float rowReference = ref MemoryMarshal.GetReference(rowSpan); float* rowPtr = (float*)Unsafe.AsPointer(ref rowReference); diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs index 1b93d01a18..b486e42258 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeProcessor{TPixel}.cs @@ -155,8 +155,8 @@ private static void ApplyNNResizeFrameTransform( interest, widthFactor, heightFactor, - source, - destination); + source.PixelBuffer, + destination.PixelBuffer); ParallelRowIterator.IterateRows( configuration, @@ -223,8 +223,8 @@ private static void ApplyResizeFrameTransform( private readonly Rectangle interest; private readonly float widthFactor; private readonly float heightFactor; - private readonly ImageFrame source; - private readonly ImageFrame destination; + private readonly Buffer2D source; + private readonly Buffer2D destination; [MethodImpl(InliningOptions.ShortMethod)] public NNRowOperation( @@ -233,8 +233,8 @@ public NNRowOperation( Rectangle interest, float widthFactor, float heightFactor, - ImageFrame source, - ImageFrame destination) + Buffer2D source, + Buffer2D destination) { this.sourceBounds = sourceBounds; this.destinationBounds = destinationBounds; @@ -256,8 +256,8 @@ public void Invoke(int y) int destRight = this.interest.Right; // Y coordinates of source points - Span sourceRow = this.source.GetPixelRowSpan((int)(((y - destOriginY) * this.heightFactor) + sourceY)); - Span targetRow = this.destination.GetPixelRowSpan(y); + Span sourceRow = this.source.DangerousGetRowSpan((int)(((y - destOriginY) * this.heightFactor) + sourceY)); + Span targetRow = this.destination.DangerousGetRowSpan(y); for (int x = destLeft; x < destRight; x++) { diff --git a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs index 7ade3aeeea..4e3a08c393 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/Resize/ResizeWorker.cs @@ -105,7 +105,7 @@ public void Dispose() [MethodImpl(InliningOptions.ShortMethod)] public Span GetColumnSpan(int x, int startY) - => this.transposedFirstPassBuffer.GetRowSpan(x).Slice(startY - this.currentWindow.Min); + => this.transposedFirstPassBuffer.DangerousGetRowSpan(x).Slice(startY - this.currentWindow.Min); public void Initialize() => this.CalculateFirstPassValues(this.currentWindow); @@ -140,7 +140,7 @@ public void FillDestinationPixels(RowInterval rowInterval, Buffer2D dest Unsafe.Add(ref tempRowBase, x) = kernel.ConvolveCore(ref firstPassColumnBase); } - Span targetRowSpan = destination.GetRowSpan(y); + Span targetRowSpan = destination.DangerousGetRowSpan(y); PixelOperations.Instance.FromVector4Destructive(this.configuration, tempColSpan, targetRowSpan, this.conversionModifiers); } diff --git a/src/ImageSharp/Processing/Processors/Transforms/SwizzleProcessor{TSwizzler,TPixel}.cs b/src/ImageSharp/Processing/Processors/Transforms/SwizzleProcessor{TSwizzler,TPixel}.cs index aab17d2920..191b6fc3a7 100644 --- a/src/ImageSharp/Processing/Processors/Transforms/SwizzleProcessor{TSwizzler,TPixel}.cs +++ b/src/ImageSharp/Processing/Processors/Transforms/SwizzleProcessor{TSwizzler,TPixel}.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. using System; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Processing.Processors.Transforms @@ -27,9 +28,10 @@ protected override void OnFrameApply(ImageFrame source, ImageFrame sourceBuffer = source.PixelBuffer; for (p.Y = 0; p.Y < source.Height; p.Y++) { - Span rowSpan = source.GetPixelRowSpan(p.Y); + Span rowSpan = sourceBuffer.DangerousGetRowSpan(p.Y); for (p.X = 0; p.X < source.Width; p.X++) { newPoint = this.swizzler.Transform(p); diff --git a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj index 8f0b4a86f2..9a92741997 100644 --- a/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj +++ b/tests/ImageSharp.Benchmarks/ImageSharp.Benchmarks.csproj @@ -43,9 +43,9 @@ - + - + diff --git a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressBenchmarks.cs b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressBenchmarks.cs index b7ec95c4ba..b998863e87 100644 --- a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressBenchmarks.cs +++ b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressBenchmarks.cs @@ -3,6 +3,7 @@ using System; using BenchmarkDotNet.Attributes; +using SixLabors.ImageSharp.Memory; namespace SixLabors.ImageSharp.Benchmarks.LoadResizeSave { @@ -37,9 +38,9 @@ private void ForEachImage(Action action, int maxDegreeOfParallelism) public int[] ParallelismValues { get; } = { Environment.ProcessorCount, - Environment.ProcessorCount / 2, - Environment.ProcessorCount / 4, - 1 + // Environment.ProcessorCount / 2, + // Environment.ProcessorCount / 4, + // 1 }; [Benchmark] @@ -48,7 +49,10 @@ private void ForEachImage(Action action, int maxDegreeOfParallelism) [Benchmark(Baseline = true)] [ArgumentsSource(nameof(ParallelismValues))] - public void ImageSharp(int maxDegreeOfParallelism) => this.ForEachImage(this.runner.ImageSharpResize, maxDegreeOfParallelism); + public void ImageSharp(int maxDegreeOfParallelism) + { + this.ForEachImage(this.runner.ImageSharpResize, maxDegreeOfParallelism); + } [Benchmark] [ArgumentsSource(nameof(ParallelismValues))] diff --git a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs index 7c57d691ab..eda054968e 100644 --- a/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs +++ b/tests/ImageSharp.Benchmarks/LoadResizeSave/LoadResizeSaveStressRunner.cs @@ -13,6 +13,7 @@ using ImageMagick; using PhotoSauce.MagicScaler; using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Tests; using SkiaSharp; @@ -43,13 +44,15 @@ public class LoadResizeSaveStressRunner public double TotalProcessedMegapixels { get; private set; } + public Size LastProcessedImageSize { get; private set; } + private string outputDirectory; public int ImageCount { get; set; } = int.MaxValue; public int MaxDegreeOfParallelism { get; set; } = -1; - public JpegKind Filter { get; set; } + public JpegKind Filter { get; set; } = JpegKind.Any; public int ThumbnailSize { get; set; } = 150; @@ -121,8 +124,9 @@ public void ForEachImageParallel(Action action) => Parallel.ForEach( new ParallelOptions { MaxDegreeOfParallelism = this.MaxDegreeOfParallelism }, action); - private void IncreaseTotalMegapixels(int width, int height) + private void LogImageProcessed(int width, int height) { + this.LastProcessedImageSize = new Size(width, height); double pixels = width * (double)height; this.TotalProcessedMegapixels += pixels / 1_000_000.0; } @@ -152,7 +156,7 @@ private string OutputPath(string inputPath, [CallerMemberName]string postfix = n public void SystemDrawingResize(string input) { using var image = SystemDrawingImage.FromFile(input, true); - this.IncreaseTotalMegapixels(image.Width, image.Height); + this.LogImageProcessed(image.Width, image.Height); (int Width, int Height) scaled = this.ScaledSize(image.Width, image.Height, this.ThumbnailSize); var resized = new Bitmap(scaled.Width, scaled.Height); @@ -178,7 +182,7 @@ public void ImageSharpResize(string input) // Resize it to fit a 150x150 square using var image = ImageSharpImage.Load(input); - this.IncreaseTotalMegapixels(image.Width, image.Height); + this.LogImageProcessed(image.Width, image.Height); image.Mutate(i => i.Resize(new ResizeOptions { @@ -196,7 +200,7 @@ public void ImageSharpResize(string input) public void MagickResize(string input) { using var image = new MagickImage(input); - this.IncreaseTotalMegapixels(image.Width, image.Height); + this.LogImageProcessed(image.Width, image.Height); // Resize it to fit a 150x150 square image.Resize(this.ThumbnailSize, this.ThumbnailSize); @@ -231,7 +235,7 @@ public void MagicScalerResize(string input) public void SkiaCanvasResize(string input) { using var original = SKBitmap.Decode(input); - this.IncreaseTotalMegapixels(original.Width, original.Height); + this.LogImageProcessed(original.Width, original.Height); (int Width, int Height) scaled = this.ScaledSize(original.Width, original.Height, this.ThumbnailSize); using var surface = SKSurface.Create(new SKImageInfo(scaled.Width, scaled.Height, original.ColorType, original.AlphaType)); using var paint = new SKPaint() { FilterQuality = SKFilterQuality.High }; @@ -249,7 +253,7 @@ public void SkiaCanvasResize(string input) public void SkiaBitmapResize(string input) { using var original = SKBitmap.Decode(input); - this.IncreaseTotalMegapixels(original.Width, original.Height); + this.LogImageProcessed(original.Width, original.Height); (int Width, int Height) scaled = this.ScaledSize(original.Width, original.Height, this.ThumbnailSize); using var resized = original.Resize(new SKImageInfo(scaled.Width, scaled.Height), SKFilterQuality.High); if (resized == null) @@ -268,7 +272,7 @@ public void SkiaBitmapDecodeToTargetSize(string input) using var codec = SKCodec.Create(input); SKImageInfo info = codec.Info; - this.IncreaseTotalMegapixels(info.Width, info.Height); + this.LogImageProcessed(info.Width, info.Height); (int Width, int Height) scaled = this.ScaledSize(info.Width, info.Height, this.ThumbnailSize); SKSizeI supportedScale = codec.GetScaledDimensions((float)scaled.Width / info.Width); diff --git a/tests/ImageSharp.Benchmarks/PixelBlenders/PorterDuffBulkVsPixel.cs b/tests/ImageSharp.Benchmarks/PixelBlenders/PorterDuffBulkVsPixel.cs index cf78078376..76d077c76f 100644 --- a/tests/ImageSharp.Benchmarks/PixelBlenders/PorterDuffBulkVsPixel.cs +++ b/tests/ImageSharp.Benchmarks/PixelBlenders/PorterDuffBulkVsPixel.cs @@ -70,7 +70,7 @@ public Size BulkVectorConvert() Buffer2D pixels = image.GetRootFramePixelBuffer(); for (int y = 0; y < image.Height; y++) { - Span span = pixels.GetRowSpan(y); + Span span = pixels.DangerousGetRowSpan(y); this.BulkVectorConvert(span, span, span, amounts.GetSpan()); } @@ -86,7 +86,7 @@ public Size BulkPixelConvert() Buffer2D pixels = image.GetRootFramePixelBuffer(); for (int y = 0; y < image.Height; y++) { - Span span = pixels.GetRowSpan(y); + Span span = pixels.DangerousGetRowSpan(y); this.BulkPixelConvert(span, span, span, amounts.GetSpan()); } diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj b/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj index 1a470fa31f..6ff5a4cc7f 100644 --- a/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj +++ b/tests/ImageSharp.Tests.ProfilingSandbox/ImageSharp.Tests.ProfilingSandbox.csproj @@ -48,6 +48,7 @@ + diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs b/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs index 7fe91fe5f6..c7484daa0d 100644 --- a/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs +++ b/tests/ImageSharp.Tests.ProfilingSandbox/LoadResizeSaveParallelMemoryStress.cs @@ -3,29 +3,133 @@ using System; using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Runtime.CompilerServices; using System.Text; +using System.Threading; +using CommandLine; +using CommandLine.Text; using SixLabors.ImageSharp.Benchmarks.LoadResizeSave; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Memory.Internals; namespace SixLabors.ImageSharp.Tests.ProfilingSandbox { // See ImageSharp.Benchmarks/LoadResizeSave/README.md internal class LoadResizeSaveParallelMemoryStress { - private readonly LoadResizeSaveStressRunner benchmarks; - private LoadResizeSaveParallelMemoryStress() { - this.benchmarks = new LoadResizeSaveStressRunner() + this.Benchmarks = new LoadResizeSaveStressRunner() { - // MaxDegreeOfParallelism = 10, Filter = JpegKind.Baseline, }; - this.benchmarks.Init(); + this.Benchmarks.Init(); + } + + private int gcFrequency; + + private int leakFrequency; + + private int imageCounter; + + public LoadResizeSaveStressRunner Benchmarks { get; } + + public static void Run(string[] args) + { + Console.WriteLine($"Running: {typeof(LoadResizeSaveParallelMemoryStress).Assembly.Location}"); + Console.WriteLine($"64 bit: {Environment.Is64BitProcess}"); + CommandLineOptions options = args.Length > 0 ? CommandLineOptions.Parse(args) : null; + + var lrs = new LoadResizeSaveParallelMemoryStress(); + if (options != null) + { + lrs.Benchmarks.MaxDegreeOfParallelism = options.MaxDegreeOfParallelism; + } + + Console.WriteLine($"\nEnvironment.ProcessorCount={Environment.ProcessorCount}"); + Stopwatch timer; + + if (options == null || !options.ImageSharp) + { + RunBenchmarkSwitcher(lrs, out timer); + } + else + { + Console.WriteLine("Running ImageSharp with options:"); + Console.WriteLine(options.ToString()); + + if (!options.KeepDefaultAllocator) + { + Configuration.Default.MemoryAllocator = options.CreateMemoryAllocator(); + } + + lrs.leakFrequency = options.LeakFrequency; + lrs.gcFrequency = options.GcFrequency; + + + timer = Stopwatch.StartNew(); + try + { + for (int i = 0; i < options.RepeatCount; i++) + { + lrs.ImageSharpBenchmarkParallel(); + } + } + catch (Exception ex) + { + Console.WriteLine(ex.Message); + } + + timer.Stop(); + + if (options.ReleaseRetainedResourcesAtEnd) + { + Configuration.Default.MemoryAllocator.ReleaseRetainedResources(); + } + + int finalGcCount = -Math.Min(0, options.GcFrequency); + + if (finalGcCount > 0) + { + Console.WriteLine($"TotalOutstandingHandles: {UnmanagedMemoryHandle.TotalOutstandingHandles}"); + Console.WriteLine($"GC x {finalGcCount}, with 3 seconds wait."); + for (int i = 0; i < finalGcCount; i++) + { + Thread.Sleep(3000); + GC.Collect(); + GC.WaitForPendingFinalizers(); + } + } + } + + var stats = new Stats(timer, lrs.Benchmarks.TotalProcessedMegapixels); + Console.WriteLine($"Total Megapixels: {stats.TotalMegapixels}, TotalOomRetries: {UnmanagedMemoryHandle.TotalOomRetries}, TotalOutstandingHandles: {UnmanagedMemoryHandle.TotalOutstandingHandles}, Total Gen2 GC count: {GC.CollectionCount(2)}"); + Console.WriteLine(stats.GetMarkdown()); + if (options?.FileOutput != null) + { + PrintFileOutput(options.FileOutput, stats); + } + + if (options != null && options.PauseAtEnd) + { + Console.WriteLine("Press ENTER"); + Console.ReadLine(); + } } - private double TotalProcessedMegapixels => this.benchmarks.TotalProcessedMegapixels; + private static void PrintFileOutput(string fileOutput, Stats stats) + { + string[] ss = fileOutput.Split(';'); + string fileName = ss[0]; + string content = ss[1] + .Replace("TotalSeconds", stats.TotalSeconds.ToString(CultureInfo.InvariantCulture)) + .Replace("EOL", Environment.NewLine); + File.AppendAllText(fileName, content); + } - public static void Run() + private static void RunBenchmarkSwitcher(LoadResizeSaveParallelMemoryStress lrs, out Stopwatch timer) { Console.WriteLine(@"Choose a library for image resizing stress test: @@ -33,57 +137,46 @@ 1. System.Drawing 2. ImageSharp 3. MagicScaler 4. SkiaSharp -5. NetVips -6. ImageMagick +5. SkiaSharp - Decode to target size +6. NetVips +7. ImageMagick "); ConsoleKey key = Console.ReadKey().Key; if (key < ConsoleKey.D1 || key > ConsoleKey.D6) { Console.WriteLine("Unrecognized command."); - return; + Environment.Exit(-1); } - try - { - var lrs = new LoadResizeSaveParallelMemoryStress(); - - Console.WriteLine($"\nEnvironment.ProcessorCount={Environment.ProcessorCount}"); - Console.WriteLine($"Running with MaxDegreeOfParallelism={lrs.benchmarks.MaxDegreeOfParallelism} ..."); - var timer = Stopwatch.StartNew(); - - switch (key) - { - case ConsoleKey.D1: - lrs.SystemDrawingBenchmarkParallel(); - break; - case ConsoleKey.D2: - Console.WriteLine($"Images: {lrs.benchmarks.Images.Length}"); - lrs.ImageSharpBenchmarkParallel(); - break; - case ConsoleKey.D3: - lrs.MagicScalerBenchmarkParallel(); - break; - case ConsoleKey.D4: - lrs.SkiaBitmapBenchmarkParallel(); - break; - case ConsoleKey.D5: - lrs.NetVipsBenchmarkParallel(); - break; - case ConsoleKey.D6: - lrs.MagickBenchmarkParallel(); - break; - } + timer = Stopwatch.StartNew(); - timer.Stop(); - var stats = new Stats(timer, lrs.TotalProcessedMegapixels); - Console.WriteLine("Done. TotalProcessedMegapixels: " + lrs.TotalProcessedMegapixels); - Console.WriteLine(stats.GetMarkdown()); - } - catch (Exception ex) + switch (key) { - Console.WriteLine(ex.ToString()); + case ConsoleKey.D1: + lrs.SystemDrawingBenchmarkParallel(); + break; + case ConsoleKey.D2: + lrs.ImageSharpBenchmarkParallel(); + break; + case ConsoleKey.D3: + lrs.MagicScalerBenchmarkParallel(); + break; + case ConsoleKey.D4: + lrs.SkiaBitmapBenchmarkParallel(); + break; + case ConsoleKey.D5: + lrs.SkiaBitmapDecodeToTargetSizeBenchmarkParallel(); + break; + case ConsoleKey.D6: + lrs.NetVipsBenchmarkParallel(); + break; + case ConsoleKey.D7: + lrs.MagickBenchmarkParallel(); + break; } + + timer.Stop(); } private struct Stats @@ -126,18 +219,124 @@ public string GetMarkdown() } } - private void ForEachImage(Action action) => this.benchmarks.ForEachImageParallel(action); + private class CommandLineOptions + { + [Option('i', "imagesharp", Required = false, Default = false, HelpText = "Test ImageSharp without benchmark switching")] + public bool ImageSharp { get; set; } + + [Option('d', "default-allocator", Required = false, Default = false, HelpText = "Keep default MemoryAllocator and ignore all settings")] + public bool KeepDefaultAllocator { get; set; } + + [Option('m', "max-contiguous", Required = false, Default = 4, HelpText = "Maximum size of contiguous pool buffers in MegaBytes")] + public int MaxContiguousPoolBufferMegaBytes { get; set; } = 4; + + [Option('s', "poolsize", Required = false, Default = 4096, HelpText = "The size of the pool in MegaBytes")] + public int MaxPoolSizeMegaBytes { get; set; } = 4096; + + [Option('u', "max-nonpool", Required = false, Default = 32, HelpText = "Maximum size of non-pooled contiguous blocks in MegaBytes")] + public int MaxCapacityOfNonPoolBuffersMegaBytes { get; set; } = 32; + + [Option('p', "parallelism", Required = false, Default = -1, HelpText = "Level of parallelism")] + public int MaxDegreeOfParallelism { get; set; } = -1; + + [Option('r', "repeat-count", Required = false, Default = 1, HelpText = "Times to run the whole benchmark")] + public int RepeatCount { get; set; } = 1; + + [Option('g', "gc-frequency", Required = false, Default = 0, HelpText = "Positive number: call GC every 'g'-th resize. Negative number: call GC '-g' times in the end.")] + public int GcFrequency { get; set; } + + [Option('e', "release-at-end", Required = false, Default = false, HelpText = "Specify to run ReleaseRetainedResources() after execution")] + public bool ReleaseRetainedResourcesAtEnd { get; set; } + + [Option('w', "pause", Required = false, Default = false, HelpText = "Specify to pause and wait for user input after the execution")] + public bool PauseAtEnd { get; set; } + + [Option('f', "file", Required = false, Default = null, HelpText = "Specify to print the execution time to a file. Format: ';' see the code for formatstr semantics.")] + public string FileOutput { get; set; } + + [Option('t', "trim-period", Required = false, Default = null, HelpText = "Trim period for the pool in seconds")] + public int? TrimTimeSeconds { get; set; } + + [Option('l', "leak-frequency", Required = false, Default = 0, HelpText = "Inject leaking memory allocations after every 'l'-th resize to stress test finalizer behavior.")] + public int LeakFrequency { get; set; } + + public static CommandLineOptions Parse(string[] args) + { + CommandLineOptions result = null; + ParserResult parserResult = Parser.Default.ParseArguments(args).WithParsed(o => + { + result = o; + }); + + if (result == null) + { + Console.WriteLine(HelpText.RenderUsageText(parserResult)); + } + + return result; + } + + public override string ToString() => + $"p({this.MaxDegreeOfParallelism})_i({this.ImageSharp})_d({this.KeepDefaultAllocator})_m({this.MaxContiguousPoolBufferMegaBytes})_s({this.MaxPoolSizeMegaBytes})_u({this.MaxCapacityOfNonPoolBuffersMegaBytes})_r({this.RepeatCount})_g({this.GcFrequency})_e({this.ReleaseRetainedResourcesAtEnd})_l({this.LeakFrequency})"; + + public MemoryAllocator CreateMemoryAllocator() + { + if (this.TrimTimeSeconds.HasValue) + { + return new UniformUnmanagedMemoryPoolMemoryAllocator( + 1024 * 1024, + (int)B(this.MaxContiguousPoolBufferMegaBytes), + B(this.MaxPoolSizeMegaBytes), + (int)B(this.MaxCapacityOfNonPoolBuffersMegaBytes), + new UniformUnmanagedMemoryPool.TrimSettings + { + TrimPeriodMilliseconds = this.TrimTimeSeconds.Value * 1000 + }); + } + else + { + return new UniformUnmanagedMemoryPoolMemoryAllocator( + 1024 * 1024, + (int)B(this.MaxContiguousPoolBufferMegaBytes), + B(this.MaxPoolSizeMegaBytes), + (int)B(this.MaxCapacityOfNonPoolBuffersMegaBytes)); + } + } + + private static long B(double megaBytes) => (long)(megaBytes * 1024 * 1024); + } + + private void ForEachImage(Action action) => this.Benchmarks.ForEachImageParallel(action); - private void SystemDrawingBenchmarkParallel() => this.ForEachImage(this.benchmarks.SystemDrawingResize); + private void SystemDrawingBenchmarkParallel() => this.ForEachImage(this.Benchmarks.SystemDrawingResize); - private void ImageSharpBenchmarkParallel() => this.ForEachImage(this.benchmarks.ImageSharpResize); + private void ImageSharpBenchmarkParallel() => + this.ForEachImage(f => + { + int cnt = Interlocked.Increment(ref this.imageCounter); + this.Benchmarks.ImageSharpResize(f); + if (this.leakFrequency > 0 && cnt % this.leakFrequency == 0) + { + _ = Configuration.Default.MemoryAllocator.Allocate(1 << 16); + Size size = this.Benchmarks.LastProcessedImageSize; + _ = new Image(size.Width, size.Height); + } + + if (this.gcFrequency > 0 && cnt % this.gcFrequency == 0) + { + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + } + }); - private void MagickBenchmarkParallel() => this.ForEachImage(this.benchmarks.MagickResize); + private void MagickBenchmarkParallel() => this.ForEachImage(this.Benchmarks.MagickResize); - private void MagicScalerBenchmarkParallel() => this.ForEachImage(this.benchmarks.MagicScalerResize); + private void MagicScalerBenchmarkParallel() => this.ForEachImage(this.Benchmarks.MagicScalerResize); - private void SkiaBitmapBenchmarkParallel() => this.ForEachImage(this.benchmarks.SkiaBitmapResize); + private void SkiaBitmapBenchmarkParallel() => this.ForEachImage(this.Benchmarks.SkiaBitmapResize); + private void SkiaBitmapDecodeToTargetSizeBenchmarkParallel() => this.ForEachImage(this.Benchmarks.SkiaBitmapDecodeToTargetSize); - private void NetVipsBenchmarkParallel() => this.ForEachImage(this.benchmarks.NetVipsResize); + private void NetVipsBenchmarkParallel() => this.ForEachImage(this.Benchmarks.NetVipsResize); } } diff --git a/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs b/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs index 079d24c478..bc0b40badd 100644 --- a/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs +++ b/tests/ImageSharp.Tests.ProfilingSandbox/Program.cs @@ -2,6 +2,9 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Reflection; +using System.Threading; +using SixLabors.ImageSharp.Memory.Internals; using SixLabors.ImageSharp.Tests.Formats.Jpg; using SixLabors.ImageSharp.Tests.PixelFormats.PixelOperations; using SixLabors.ImageSharp.Tests.ProfilingBenchmarks; @@ -31,7 +34,15 @@ private class ConsoleOutput : ITestOutputHelper /// public static void Main(string[] args) { - LoadResizeSaveParallelMemoryStress.Run(); + try + { + LoadResizeSaveParallelMemoryStress.Run(args); + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + // RunJpegEncoderProfilingTests(); // RunJpegColorProfilingTests(); // RunDecodeJpegProfilingTests(); @@ -41,6 +52,20 @@ public static void Main(string[] args) // Console.ReadLine(); } + private static Version GetNetCoreVersion() + { + Assembly assembly = typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly; + Console.WriteLine(assembly.Location); + string[] assemblyPath = assembly.Location.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); + int netCoreAppIndex = Array.IndexOf(assemblyPath, "Microsoft.NETCore.App"); + if (netCoreAppIndex > 0 && netCoreAppIndex < assemblyPath.Length - 2) + { + return Version.Parse(assemblyPath[netCoreAppIndex + 1]); + } + + return null; + } + private static void RunJpegEncoderProfilingTests() { var benchmarks = new JpegProfilingBenchmarks(new ConsoleOutput()); diff --git a/tests/ImageSharp.Tests/Advanced/AdvancedImageExtensionsTests.cs b/tests/ImageSharp.Tests/Advanced/AdvancedImageExtensionsTests.cs index 6031227bd6..2da0cbf83e 100644 --- a/tests/ImageSharp.Tests/Advanced/AdvancedImageExtensionsTests.cs +++ b/tests/ImageSharp.Tests/Advanced/AdvancedImageExtensionsTests.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; @@ -62,7 +63,7 @@ public void ConsumedMemory_PixelDataIsCorrect(TestImageProvider using Image image0 = provider.GetImage(); var targetBuffer = new TPixel[image0.Width * image0.Height]; - Assert.True(image0.TryGetSinglePixelSpan(out Span sourceBuffer)); + Assert.True(image0.DangerousTryGetSinglePixelMemory(out Memory sourceBuffer)); sourceBuffer.CopyTo(targetBuffer); @@ -106,7 +107,7 @@ private static void VerifyMemoryGroupDataMatchesTestPattern( [WithBasicTestPatternImages(1, 1, PixelTypes.Rgba32)] [WithBasicTestPatternImages(131, 127, PixelTypes.Rgba32)] [WithBasicTestPatternImages(333, 555, PixelTypes.Bgr24)] - public void GetPixelRowMemory_PixelDataIsCorrect(TestImageProvider provider) + public void DangerousGetPixelRowMemory_PixelDataIsCorrect(TestImageProvider provider) where TPixel : unmanaged, IPixel { provider.LimitAllocatorBufferCapacity().InPixelsSqrt(200); @@ -116,13 +117,18 @@ public void GetPixelRowMemory_PixelDataIsCorrect(TestImageProvider rowMemory = image.GetPixelRowMemory(y); - Span span = rowMemory.Span; + Memory rowMemoryFromImage = image.DangerousGetPixelRowMemory(y); + Memory rowMemoryFromFrame = image.Frames.RootFrame.DangerousGetPixelRowMemory(y); + Span spanFromImage = rowMemoryFromImage.Span; + Span spanFromFrame = rowMemoryFromFrame.Span; + + Assert.Equal(spanFromFrame.Length, spanFromImage.Length); + Assert.True(Unsafe.AreSame(ref spanFromFrame[0], ref spanFromImage[0])); // Assert: for (int x = 0; x < image.Width; x++) { - Assert.Equal(provider.GetExpectedBasicTestPatternPixelAt(x, y), span[x]); + Assert.Equal(provider.GetExpectedBasicTestPatternPixelAt(x, y), spanFromImage[x]); } } } @@ -134,30 +140,13 @@ public void GetPixelRowMemory_DestructiveMutate_ShouldInvalidateMemory(T { using Image image = provider.GetImage(); - Memory memory3 = image.GetPixelRowMemory(3); - Memory memory10 = image.GetPixelRowMemory(10); + Memory memory3 = image.DangerousGetPixelRowMemory(3); + Memory memory10 = image.DangerousGetPixelRowMemory(10); image.Mutate(c => c.Resize(8, 8)); Assert.ThrowsAny(() => _ = memory3.Span); Assert.ThrowsAny(() => _ = memory10.Span); } - - [Theory] - [WithBlankImages(1, 1, PixelTypes.Rgba32)] - [WithBlankImages(100, 111, PixelTypes.Rgba32)] - [WithBlankImages(400, 600, PixelTypes.Rgba32)] - public void GetPixelRowSpan_ShouldReferenceSpanOfMemory(TestImageProvider provider) - where TPixel : unmanaged, IPixel - { - provider.LimitAllocatorBufferCapacity().InPixelsSqrt(200); - - using Image image = provider.GetImage(); - - 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/ConfigurationTests.cs b/tests/ImageSharp.Tests/ConfigurationTests.cs index 803babdfa5..bc2bf36b54 100644 --- a/tests/ImageSharp.Tests/ConfigurationTests.cs +++ b/tests/ImageSharp.Tests/ConfigurationTests.cs @@ -3,10 +3,12 @@ using System; using System.Linq; +using Microsoft.DotNet.RemoteExecutor; using Moq; using SixLabors.ImageSharp.Formats.Bmp; using SixLabors.ImageSharp.IO; - +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Tests.Memory; using Xunit; // ReSharper disable InconsistentNaming @@ -146,5 +148,45 @@ public void StreamBufferSize_CannotGoBelowMinimum() Assert.Throws( () => config.StreamProcessingBufferSize = 0); } + + [Fact] + public void MemoryAllocator_Setter_Roundtrips() + { + MemoryAllocator customAllocator = new SimpleGcMemoryAllocator(); + var config = new Configuration() { MemoryAllocator = customAllocator }; + Assert.Same(customAllocator, config.MemoryAllocator); + } + + [Fact] + public void MemoryAllocator_SetNull_ThrowsArgumentNullException() + { + var config = new Configuration(); + Assert.Throws(() => config.MemoryAllocator = null); + } + + [Fact] + public void InheritsDefaultMemoryAllocatorInstance() + { + RemoteExecutor.Invoke(RunTest).Dispose(); + + static void RunTest() + { + var c1 = new Configuration(); + var c2 = new Configuration(new MockConfigurationModule()); + var c3 = Configuration.CreateDefaultInstance(); + + Assert.Same(MemoryAllocator.Default, Configuration.Default.MemoryAllocator); + Assert.Same(MemoryAllocator.Default, c1.MemoryAllocator); + Assert.Same(MemoryAllocator.Default, c2.MemoryAllocator); + Assert.Same(MemoryAllocator.Default, c3.MemoryAllocator); + } + } + + private class MockConfigurationModule : IConfigurationModule + { + public void Configure(Configuration configuration) + { + } + } } } diff --git a/tests/ImageSharp.Tests/Drawing/DrawImageTests.cs b/tests/ImageSharp.Tests/Drawing/DrawImageTests.cs index b426f44046..d10549b405 100644 --- a/tests/ImageSharp.Tests/Drawing/DrawImageTests.cs +++ b/tests/ImageSharp.Tests/Drawing/DrawImageTests.cs @@ -128,8 +128,8 @@ public void WorksWithDifferentLocations(TestImageProvider provider, int using (Image background = provider.GetImage()) using (var overlay = new Image(50, 50)) { - Assert.True(overlay.TryGetSinglePixelSpan(out Span overlaySpan)); - overlaySpan.Fill(Color.Black); + Assert.True(overlay.DangerousTryGetSinglePixelMemory(out Memory overlayMem)); + overlayMem.Span.Fill(Color.Black); background.Mutate(c => c.DrawImage(overlay, new Point(x, y), PixelColorBlendingMode.Normal, 1F)); diff --git a/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs b/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs index bf13a9097d..26eff26502 100644 --- a/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs +++ b/tests/ImageSharp.Tests/Formats/GeneralFormatTests.cs @@ -99,8 +99,6 @@ public void DecodeThenEncodeImageFromStreamShouldSucceed() public void QuantizeImageShouldPreserveMaximumColorPrecision(TestImageProvider provider, string quantizerName) where TPixel : unmanaged, IPixel { - provider.Configuration.MemoryAllocator = ArrayPoolMemoryAllocator.CreateWithModeratePooling(); - IQuantizer quantizer = GetQuantizer(quantizerName); using (Image image = provider.GetImage()) diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs index c0df1e400d..824ca535bf 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifDecoderTests.cs @@ -165,9 +165,9 @@ public void CanDecodeIntermingledImages() ImageFrame first = kumin1.Frames[i]; ImageFrame second = kumin2.Frames[i]; - Assert.True(second.TryGetSinglePixelSpan(out Span secondSpan)); + Assert.True(second.DangerousTryGetSinglePixelMemory(out Memory secondMemory)); - first.ComparePixelBufferTo(secondSpan); + first.ComparePixelBufferTo(secondMemory.Span); } } } diff --git a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs index cb0b1521d6..00d43b6ffe 100644 --- a/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Gif/GifEncoderTests.cs @@ -28,6 +28,15 @@ public class GifEncoderTests { TestImages.Gif.Ratio4x1, 4, 1, PixelResolutionUnit.AspectRatio } }; + public GifEncoderTests() + { + // Free the pool on 32 bit: + if (!TestEnvironment.Is64BitProcess) + { + Configuration.Default.MemoryAllocator.ReleaseRetainedResources(); + } + } + [Theory] [WithTestPatternImages(100, 100, TestPixelTypes, false)] [WithTestPatternImages(100, 100, TestPixelTypes, false)] diff --git a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs index 1785f3dec4..35113f14ff 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/SpectralJpegTests.cs @@ -122,7 +122,8 @@ private void VerifySpectralCorrectnessImpl( this.Output.WriteLine($"Component{i}: [total: {total} | average: {average}]"); averageDifference += average; totalDifference += total; - tolerance += libJpegComponent.SpectralBlocks.DangerousGetSingleSpan().Length; + Size s = libJpegComponent.SpectralBlocks.Size(); + tolerance += s.Width * s.Height; } averageDifference /= componentCount; @@ -182,7 +183,7 @@ public override void ConvertStrideBaseline() Buffer2D spectralBlocks = component.SpectralBlocks; for (int i = 0; i < spectralBlocks.Height; i++) { - spectralBlocks.GetRowSpan(i).Clear(); + spectralBlocks.DangerousGetRowSpan(i).Clear(); } } } diff --git a/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.ComponentData.cs b/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.ComponentData.cs index 5c00b39af8..a390212d15 100644 --- a/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.ComponentData.cs +++ b/tests/ImageSharp.Tests/Formats/Jpg/Utils/LibJpegTools.ComponentData.cs @@ -69,7 +69,7 @@ public void LoadSpectralStride(Buffer2D data, int strideIndex) for (int y = startIndex; y < endIndex; y++) { - Span blockRow = data.GetRowSpan(y - startIndex); + Span blockRow = data.DangerousGetRowSpan(y - startIndex); for (int x = 0; x < this.WidthInBlocks; x++) { this.MakeBlock(blockRow[x], y, x); @@ -82,7 +82,7 @@ public void LoadSpectral(JpegComponent c) Buffer2D data = c.SpectralBlocks; for (int y = 0; y < this.HeightInBlocks; y++) { - Span blockRow = data.GetRowSpan(y); + Span blockRow = data.DangerousGetRowSpan(y); for (int x = 0; x < this.WidthInBlocks; x++) { this.MakeBlock(blockRow[x], y, x); diff --git a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs index 50bacfba4d..9e99dded88 100644 --- a/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Png/PngEncoderTests.cs @@ -411,21 +411,24 @@ public void Encode_WithPngTransparentColorBehaviorClear_Works(PngColorType color ColorType = colorType }; Rgba32 rgba32 = Color.Blue; - for (int y = 0; y < image.Height; y++) + image.ProcessPixelRows(accessor => { - System.Span rowSpan = image.GetPixelRowSpan(y); - - // Half of the test image should be transparent. - if (y > 25) + for (int y = 0; y < image.Height; y++) { - rgba32.A = 0; - } + System.Span rowSpan = accessor.GetRowSpan(y); - for (int x = 0; x < image.Width; x++) - { - rowSpan[x].FromRgba32(rgba32); + // Half of the test image should be transparent. + if (y > 25) + { + rgba32.A = 0; + } + + for (int x = 0; x < image.Width; x++) + { + rowSpan[x].FromRgba32(rgba32); + } } - } + }); // act using var memStream = new MemoryStream(); @@ -441,20 +444,23 @@ public void Encode_WithPngTransparentColorBehaviorClear_Works(PngColorType color expectedColor = new Rgba32(luminance, luminance, luminance); } - for (int y = 0; y < actual.Height; y++) + actual.ProcessPixelRows(accessor => { - System.Span rowSpan = actual.GetPixelRowSpan(y); - - if (y > 25) + for (int y = 0; y < accessor.Height; y++) { - expectedColor = Color.Transparent; - } + System.Span rowSpan = accessor.GetRowSpan(y); - for (int x = 0; x < actual.Width; x++) - { - Assert.Equal(expectedColor, rowSpan[x]); + if (y > 25) + { + expectedColor = Color.Transparent; + } + + for (int x = 0; x < accessor.Width; x++) + { + Assert.Equal(expectedColor, rowSpan[x]); + } } - } + }); } [Theory] diff --git a/tests/ImageSharp.Tests/Formats/Tga/TgaTestUtils.cs b/tests/ImageSharp.Tests/Formats/Tga/TgaTestUtils.cs index c96777031b..4de1b9a19d 100644 --- a/tests/ImageSharp.Tests/Formats/Tga/TgaTestUtils.cs +++ b/tests/ImageSharp.Tests/Formats/Tga/TgaTestUtils.cs @@ -26,7 +26,7 @@ public static void CompareWithReferenceDecoder( } var testFile = TestFile.Create(path); - Image magickImage = DecodeWithMagick(Configuration.Default, new FileInfo(testFile.FullPath)); + Image magickImage = DecodeWithMagick(new FileInfo(testFile.FullPath)); if (useExactComparer) { ImageComparer.Exact.VerifySimilarity(magickImage, image); @@ -37,15 +37,17 @@ public static void CompareWithReferenceDecoder( } } - public static Image DecodeWithMagick(Configuration configuration, FileInfo fileInfo) + public static Image DecodeWithMagick(FileInfo fileInfo) where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel { + Configuration configuration = Configuration.Default.Clone(); + configuration.PreferContiguousImageBuffers = true; using (var magickImage = new MagickImage(fileInfo)) { magickImage.AutoOrient(); var result = new Image(configuration, magickImage.Width, magickImage.Height); - Assert.True(result.TryGetSinglePixelSpan(out Span resultPixels)); + Assert.True(result.DangerousTryGetSinglePixelMemory(out Memory resultPixels)); using (IUnsafePixelCollection pixels = magickImage.GetPixelsUnsafe()) { @@ -54,7 +56,7 @@ public static Image DecodeWithMagick(Configuration configuration PixelOperations.Instance.FromRgba32Bytes( configuration, data, - resultPixels, + resultPixels.Span, resultPixels.Length); } diff --git a/tests/ImageSharp.Tests/Formats/Tiff/Compression/PackBitsTiffCompressionTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/Compression/PackBitsTiffCompressionTests.cs index b56c1e7c92..bbca2610e8 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/Compression/PackBitsTiffCompressionTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/Compression/PackBitsTiffCompressionTests.cs @@ -30,7 +30,7 @@ public void Decompress_ReadsData(byte[] inputData, byte[] expectedResult) using var stream = new BufferedReadStream(Configuration.Default, memoryStream); byte[] buffer = new byte[expectedResult.Length]; - using var decompressor = new PackBitsTiffCompression(new ArrayPoolMemoryAllocator(), default, default); + using var decompressor = new PackBitsTiffCompression(MemoryAllocator.Create(), default, default); decompressor.Decompress(stream, 0, (uint)inputData.Length, 1, buffer); Assert.Equal(expectedResult, buffer); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs index d05e37e2a2..b68670f1f1 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffEncoderHeaderTests.cs @@ -13,7 +13,7 @@ namespace SixLabors.ImageSharp.Tests.Formats.Tiff [Trait("Format", "Tiff")] public class TiffEncoderHeaderTests { - private static readonly MemoryAllocator MemoryAllocator = new ArrayPoolMemoryAllocator(); + private static readonly MemoryAllocator MemoryAllocator = MemoryAllocator.Create(); private static readonly Configuration Configuration = Configuration.Default; private static readonly ITiffEncoderOptions Options = new TiffEncoder(); diff --git a/tests/ImageSharp.Tests/Formats/Tiff/TiffTestUtils.cs b/tests/ImageSharp.Tests/Formats/Tiff/TiffTestUtils.cs index eacadae2ba..d5ddd0a409 100644 --- a/tests/ImageSharp.Tests/Formats/Tiff/TiffTestUtils.cs +++ b/tests/ImageSharp.Tests/Formats/Tiff/TiffTestUtils.cs @@ -23,7 +23,7 @@ public static void CompareWithReferenceDecoder( where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel { var testFile = TestFile.Create(encodedImagePath); - Image magickImage = DecodeWithMagick(Configuration.Default, new FileInfo(testFile.FullPath)); + Image magickImage = DecodeWithMagick(new FileInfo(testFile.FullPath)); if (useExactComparer) { ImageComparer.Exact.VerifySimilarity(magickImage, image); @@ -34,14 +34,16 @@ public static void CompareWithReferenceDecoder( } } - public static Image DecodeWithMagick(Configuration configuration, FileInfo fileInfo) + public static Image DecodeWithMagick(FileInfo fileInfo) where TPixel : unmanaged, ImageSharp.PixelFormats.IPixel { + Configuration configuration = Configuration.Default.Clone(); + configuration.PreferContiguousImageBuffers = true; using var magickImage = new MagickImage(fileInfo); magickImage.AutoOrient(); var result = new Image(configuration, magickImage.Width, magickImage.Height); - Assert.True(result.TryGetSinglePixelSpan(out Span resultPixels)); + Assert.True(result.DangerousTryGetSinglePixelMemory(out Memory resultPixels)); using IUnsafePixelCollection pixels = magickImage.GetPixelsUnsafe(); byte[] data = pixels.ToByteArray(PixelMapping.RGBA); @@ -49,7 +51,7 @@ public static Image DecodeWithMagick(Configuration configuration PixelOperations.Instance.FromRgba32Bytes( configuration, data, - resultPixels, + resultPixels.Span, resultPixels.Length); return result; diff --git a/tests/ImageSharp.Tests/Formats/WebP/LosslessUtilsTests.cs b/tests/ImageSharp.Tests/Formats/WebP/LosslessUtilsTests.cs index 684d7791bf..9c7a2f7588 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/LosslessUtilsTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/LosslessUtilsTests.cs @@ -229,7 +229,6 @@ private static void RunPredictor13Test() public void TransformColorInverse_Works() => RunTransformColorInverseTest(); #if SUPPORTS_RUNTIME_INTRINSICS - [Fact] public void CombinedShannonEntropy_WithHardwareIntrinsics_Works() => FeatureTestRunner.RunWithHwIntrinsicsFeature(RunCombinedShannonEntropyTest, HwIntrinsics.AllowAll); diff --git a/tests/ImageSharp.Tests/Formats/WebP/PredictorEncoderTests.cs b/tests/ImageSharp.Tests/Formats/WebP/PredictorEncoderTests.cs index 98c144a90d..33d49a97dd 100644 --- a/tests/ImageSharp.Tests/Formats/WebP/PredictorEncoderTests.cs +++ b/tests/ImageSharp.Tests/Formats/WebP/PredictorEncoderTests.cs @@ -139,15 +139,18 @@ private static uint[] ToBgra(Image image) where TPixel : unmanaged, IPixel { uint[] bgra = new uint[image.Width * image.Height]; - int idx = 0; - for (int y = 0; y < image.Height; y++) + image.ProcessPixelRows(accessor => { - Span rowSpan = image.GetPixelRowSpan(y); - for (int x = 0; x < rowSpan.Length; x++) + int idx = 0; + for (int y = 0; y < accessor.Height; y++) { - bgra[idx++] = ToBgra32(rowSpan[x]).PackedValue; + Span rowSpan = accessor.GetRowSpan(y); + for (int x = 0; x < rowSpan.Length; x++) + { + bgra[idx++] = ToBgra32(rowSpan[x]).PackedValue; + } } - } + }); return bgra; } diff --git a/tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs b/tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs index 5cf5db523c..f46c9519ca 100644 --- a/tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs +++ b/tests/ImageSharp.Tests/Helpers/ParallelRowIteratorTests.cs @@ -360,7 +360,7 @@ void RowAction(RowInterval rows) in operation); // Assert: - TestImageExtensions.CompareBuffers(expected.DangerousGetSingleSpan(), actual.DangerousGetSingleSpan()); + TestImageExtensions.CompareBuffers(expected, actual); } } diff --git a/tests/ImageSharp.Tests/Image/ImageCloneTests.cs b/tests/ImageSharp.Tests/Image/ImageCloneTests.cs index 5efbe2cba1..f602643341 100644 --- a/tests/ImageSharp.Tests/Image/ImageCloneTests.cs +++ b/tests/ImageSharp.Tests/Image/ImageCloneTests.cs @@ -32,15 +32,17 @@ public void Clone_WhenDisposed_Throws() [WithTestPatternImages(9, 9, PixelTypes.Rgba32)] public void CloneAs_ToBgra32(TestImageProvider provider) { - using (Image image = provider.GetImage()) - using (Image clone = image.CloneAs()) + using Image image = provider.GetImage(); + using Image clone = image.CloneAs(); + + image.ProcessPixelRows(clone, static (imageAccessor, cloneAccessor) => { - for (int y = 0; y < image.Height; y++) + for (int y = 0; y < imageAccessor.Height; y++) { - Span row = image.GetPixelRowSpan(y); - Span rowClone = clone.GetPixelRowSpan(y); + Span row = imageAccessor.GetRowSpan(y); + Span rowClone = cloneAccessor.GetRowSpan(y); - for (int x = 0; x < image.Width; x++) + for (int x = 0; x < imageAccessor.Width; x++) { Rgba32 expected = row[x]; Bgra32 actual = rowClone[x]; @@ -51,22 +53,24 @@ public void CloneAs_ToBgra32(TestImageProvider provider) Assert.Equal(expected.A, actual.A); } } - } + }); } [Theory] [WithTestPatternImages(9, 9, PixelTypes.Rgba32)] public void CloneAs_ToBgr24(TestImageProvider provider) { - using (Image image = provider.GetImage()) - using (Image clone = image.CloneAs()) + using Image image = provider.GetImage(); + using Image clone = image.CloneAs(); + + image.ProcessPixelRows(clone, static (imageAccessor, cloneAccessor) => { - for (int y = 0; y < image.Height; y++) + for (int y = 0; y < imageAccessor.Height; y++) { - Span row = image.GetPixelRowSpan(y); - Span rowClone = clone.GetPixelRowSpan(y); + Span row = imageAccessor.GetRowSpan(y); + Span rowClone = cloneAccessor.GetRowSpan(y); - for (int x = 0; x < image.Width; x++) + for (int x = 0; x < cloneAccessor.Width; x++) { Rgba32 expected = row[x]; Bgr24 actual = rowClone[x]; @@ -76,22 +80,23 @@ public void CloneAs_ToBgr24(TestImageProvider provider) Assert.Equal(expected.B, actual.B); } } - } + }); } [Theory] [WithTestPatternImages(9, 9, PixelTypes.Rgba32)] public void CloneAs_ToArgb32(TestImageProvider provider) { - using (Image image = provider.GetImage()) - using (Image clone = image.CloneAs()) + using Image image = provider.GetImage(); + using Image clone = image.CloneAs(); + image.ProcessPixelRows(clone, static (imageAccessor, cloneAccessor) => { - for (int y = 0; y < image.Height; y++) + for (int y = 0; y < imageAccessor.Height; y++) { - Span row = image.GetPixelRowSpan(y); - Span rowClone = clone.GetPixelRowSpan(y); + Span row = imageAccessor.GetRowSpan(y); + Span rowClone = cloneAccessor.GetRowSpan(y); - for (int x = 0; x < image.Width; x++) + for (int x = 0; x < cloneAccessor.Width; x++) { Rgba32 expected = row[x]; Argb32 actual = rowClone[x]; @@ -102,22 +107,23 @@ public void CloneAs_ToArgb32(TestImageProvider provider) Assert.Equal(expected.A, actual.A); } } - } + }); } [Theory] [WithTestPatternImages(9, 9, PixelTypes.Rgba32)] public void CloneAs_ToRgb24(TestImageProvider provider) { - using (Image image = provider.GetImage()) - using (Image clone = image.CloneAs()) + using Image image = provider.GetImage(); + using Image clone = image.CloneAs(); + image.ProcessPixelRows(clone, static (imageAccessor, cloneAccessor) => { - for (int y = 0; y < image.Height; y++) + for (int y = 0; y < imageAccessor.Height; y++) { - Span row = image.GetPixelRowSpan(y); - Span rowClone = clone.GetPixelRowSpan(y); + Span row = imageAccessor.GetRowSpan(y); + Span rowClone = cloneAccessor.GetRowSpan(y); - for (int x = 0; x < image.Width; x++) + for (int x = 0; x < imageAccessor.Width; x++) { Rgba32 expected = row[x]; Rgb24 actual = rowClone[x]; @@ -127,7 +133,7 @@ public void CloneAs_ToRgb24(TestImageProvider provider) Assert.Equal(expected.B, actual.B); } } - } + }); } } } diff --git a/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.Generic.cs b/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.Generic.cs index dd8275ee8e..9ed276ebc9 100644 --- a/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.Generic.cs +++ b/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.Generic.cs @@ -2,10 +2,11 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Collections.Generic; using System.Linq; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.PixelFormats; - +using SixLabors.ImageSharp.Tests.Memory; using Xunit; namespace SixLabors.ImageSharp.Tests @@ -210,9 +211,9 @@ public void CloneFrame(TestImageProvider provider) using (Image cloned = img.Frames.CloneFrame(0)) { Assert.Equal(2, img.Frames.Count); - Assert.True(img.TryGetSinglePixelSpan(out Span imgSpan)); + Assert.True(img.DangerousTryGetSinglePixelMemory(out Memory imgMem)); - cloned.ComparePixelBufferTo(imgSpan); + cloned.ComparePixelBufferTo(imgMem); } } } @@ -224,15 +225,15 @@ public void ExtractFrame(TestImageProvider provider) { using (Image img = provider.GetImage()) { - Assert.True(img.TryGetSinglePixelSpan(out Span imgSpan)); - TPixel[] sourcePixelData = imgSpan.ToArray(); + Assert.True(img.DangerousTryGetSinglePixelMemory(out Memory imgMemory)); + TPixel[] sourcePixelData = imgMemory.ToArray(); using var imageFrame = new ImageFrame(Configuration.Default, 10, 10); using ImageFrame addedFrame = img.Frames.AddFrame(imageFrame); using (Image cloned = img.Frames.ExportFrame(0)) { Assert.Equal(1, img.Frames.Count); - cloned.ComparePixelBufferTo(sourcePixelData); + cloned.ComparePixelBufferTo(sourcePixelData.AsSpan()); } } } @@ -260,8 +261,8 @@ public void CreateFrame_CustomFillColor() [Fact] public void AddFrameFromPixelData() { - Assert.True(this.Image.Frames.RootFrame.TryGetSinglePixelSpan(out Span imgSpan)); - Rgba32[] pixelData = imgSpan.ToArray(); + Assert.True(this.Image.Frames.RootFrame.DangerousTryGetSinglePixelMemory(out Memory imgMem)); + Rgba32[] pixelData = imgMem.ToArray(); using ImageFrame addedFrame = this.Image.Frames.AddFrame(pixelData); Assert.Equal(2, this.Image.Frames.Count); } @@ -272,8 +273,8 @@ public void AddFrame_clones_sourceFrame() using var otherFrame = new ImageFrame(Configuration.Default, 10, 10); using ImageFrame addedFrame = this.Image.Frames.AddFrame(otherFrame); - Assert.True(otherFrame.TryGetSinglePixelSpan(out Span otherFrameSpan)); - addedFrame.ComparePixelBufferTo(otherFrameSpan); + Assert.True(otherFrame.DangerousTryGetSinglePixelMemory(out Memory otherFrameMem)); + addedFrame.ComparePixelBufferTo(otherFrameMem.Span); Assert.NotEqual(otherFrame, addedFrame); } @@ -283,8 +284,8 @@ public void InsertFrame_clones_sourceFrame() using var otherFrame = new ImageFrame(Configuration.Default, 10, 10); using ImageFrame addedFrame = this.Image.Frames.InsertFrame(0, otherFrame); - Assert.True(otherFrame.TryGetSinglePixelSpan(out Span otherFrameSpan)); - addedFrame.ComparePixelBufferTo(otherFrameSpan); + Assert.True(otherFrame.DangerousTryGetSinglePixelMemory(out Memory otherFrameMem)); + addedFrame.ComparePixelBufferTo(otherFrameMem.Span); Assert.NotEqual(otherFrame, addedFrame); } @@ -339,6 +340,26 @@ public void Contains_FalseIfNonMember() Assert.False(this.Image.Frames.Contains(frame)); } + [Fact] + public void PreferContiguousImageBuffers_True_AppliedToAllFrames() + { + Configuration configuration = Configuration.Default.Clone(); + configuration.MemoryAllocator = new TestMemoryAllocator { BufferCapacityInBytes = 1000 }; + configuration.PreferContiguousImageBuffers = true; + + using var image = new Image(configuration, 100, 100); + image.Frames.CreateFrame(); + image.Frames.InsertFrame(0, image.Frames[0]); + image.Frames.CreateFrame(Color.Red); + + Assert.Equal(4, image.Frames.Count); + IEnumerable> frames = image.Frames; + foreach (ImageFrame frame in frames) + { + Assert.True(frame.DangerousTryGetSinglePixelMemory(out Memory _)); + } + } + [Fact] public void DisposeCall_NoThrowIfCalledMultiple() { diff --git a/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs b/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs index b656151213..8435464391 100644 --- a/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs +++ b/tests/ImageSharp.Tests/Image/ImageFrameCollectionTests.NonGeneric.cs @@ -158,8 +158,8 @@ public void CloneFrame(TestImageProvider provider) var expectedClone = (Image)cloned; - Assert.True(img.TryGetSinglePixelSpan(out Span imgSpan)); - expectedClone.ComparePixelBufferTo(imgSpan); + Assert.True(img.DangerousTryGetSinglePixelMemory(out Memory imgMem)); + expectedClone.ComparePixelBufferTo(imgMem); } } } @@ -171,8 +171,8 @@ public void ExtractFrame(TestImageProvider provider) { using (Image img = provider.GetImage()) { - Assert.True(img.TryGetSinglePixelSpan(out Span imgSpan)); - var sourcePixelData = imgSpan.ToArray(); + Assert.True(img.DangerousTryGetSinglePixelMemory(out Memory imgMem)); + TPixel[] sourcePixelData = imgMem.ToArray(); ImageFrameCollection nonGenericFrameCollection = img.Frames; @@ -182,7 +182,7 @@ public void ExtractFrame(TestImageProvider provider) Assert.Equal(1, img.Frames.Count); var expectedClone = (Image)cloned; - expectedClone.ComparePixelBufferTo(sourcePixelData); + expectedClone.ComparePixelBufferTo(sourcePixelData.AsSpan()); } } } diff --git a/tests/ImageSharp.Tests/Image/ImageFrameTests.cs b/tests/ImageSharp.Tests/Image/ImageFrameTests.cs index 766bbd138a..4d01fd754b 100644 --- a/tests/ImageSharp.Tests/Image/ImageFrameTests.cs +++ b/tests/ImageSharp.Tests/Image/ImageFrameTests.cs @@ -2,8 +2,11 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Tests.Memory; using Xunit; namespace SixLabors.ImageSharp.Tests @@ -16,9 +19,9 @@ public class Indexer private void LimitBufferCapacity(int bufferCapacityInBytes) { - var allocator = ArrayPoolMemoryAllocator.CreateDefault(); - this.configuration.MemoryAllocator = allocator; + var allocator = new TestMemoryAllocator(); allocator.BufferCapacityInBytes = bufferCapacityInBytes; + this.configuration.MemoryAllocator = allocator; } [Theory] @@ -92,6 +95,99 @@ public void Set_OutOfRangeY(bool enforceDisco, int y) ArgumentOutOfRangeException ex = Assert.Throws(() => frame[3, y] = default); Assert.Equal("y", ex.ParamName); } + + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public void CopyPixelDataTo_Success(bool disco, bool byteSpan) + { + if (disco) + { + this.LimitBufferCapacity(20); + } + + using var image = new Image(this.configuration, 10, 10); + if (disco) + { + Assert.True(image.GetPixelMemoryGroup().Count > 1); + } + + byte[] expected = TestUtils.FillImageWithRandomBytes(image); + byte[] actual = new byte[expected.Length]; + if (byteSpan) + { + image.Frames.RootFrame.CopyPixelDataTo(actual); + } + else + { + Span destination = MemoryMarshal.Cast(actual); + image.Frames.RootFrame.CopyPixelDataTo(destination); + } + + Assert.True(expected.AsSpan().SequenceEqual(actual)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void CopyPixelDataTo_DestinationTooShort_Throws(bool byteSpan) + { + using var image = new Image(this.configuration, 10, 10); + + Assert.ThrowsAny(() => + { + if (byteSpan) + { + image.Frames.RootFrame.CopyPixelDataTo(new byte[199]); + } + else + { + image.Frames.RootFrame.CopyPixelDataTo(new La16[99]); + } + }); + } + } + + public class ProcessPixelRows : ProcessPixelRowsTestBase + { + protected override void ProcessPixelRowsImpl( + Image image, + PixelAccessorAction processPixels) => + image.Frames.RootFrame.ProcessPixelRows(processPixels); + + protected override void ProcessPixelRowsImpl( + Image image1, + Image image2, + PixelAccessorAction processPixels) => + image1.Frames.RootFrame.ProcessPixelRows(image2.Frames.RootFrame, processPixels); + + protected override void ProcessPixelRowsImpl( + Image image1, + Image image2, + Image image3, + PixelAccessorAction processPixels) => + image1.Frames.RootFrame.ProcessPixelRows( + image2.Frames.RootFrame, + image3.Frames.RootFrame, + processPixels); + + [Fact] + public void NullReference_Throws() + { + using var img = new Image(1, 1); + ImageFrame frame = img.Frames.RootFrame; + + Assert.Throws(() => frame.ProcessPixelRows(null)); + + Assert.Throws(() => frame.ProcessPixelRows((ImageFrame)null, (_, _) => { })); + Assert.Throws(() => frame.ProcessPixelRows(frame, frame, null)); + + Assert.Throws(() => frame.ProcessPixelRows((ImageFrame)null, frame, (_, _, _) => { })); + Assert.Throws(() => frame.ProcessPixelRows(frame, (ImageFrame)null, (_, _, _) => { })); + Assert.Throws(() => frame.ProcessPixelRows(frame, frame, null)); + } } } } diff --git a/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs b/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs index 7fae29a85f..ec9e3450f5 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.WrapMemory.cs @@ -5,12 +5,15 @@ using System.Buffers; using System.Drawing; using System.Drawing.Imaging; +using System.IO; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; using SixLabors.ImageSharp.Common.Helpers; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; using Xunit; // ReSharper disable InconsistentNaming @@ -71,7 +74,7 @@ public override unsafe Span GetSpan() public override unsafe MemoryHandle Pin(int elementIndex = 0) { void* ptr = (void*)this.bmpData.Scan0; - return new MemoryHandle(ptr); + return new MemoryHandle(ptr, pinnable: this); } public override void Unpin() @@ -132,8 +135,8 @@ public void WrapMemory_CreatedImageIsCorrect() using (var image = Image.WrapMemory(cfg, memory, 5, 5, metaData)) { - Assert.True(image.TryGetSinglePixelSpan(out Span imageSpan)); - ref Rgba32 pixel0 = ref imageSpan[0]; + Assert.True(image.DangerousTryGetSinglePixelMemory(out Memory imageMem)); + ref Rgba32 pixel0 = ref imageMem.Span[0]; Assert.True(Unsafe.AreSame(ref array[0], ref pixel0)); Assert.Equal(cfg, image.GetConfiguration()); @@ -160,17 +163,25 @@ public void WrapSystemDrawingBitmap_WhenObserved() using (var image = Image.WrapMemory(memory, bmp.Width, bmp.Height)) { Assert.Equal(memory, image.GetRootFramePixelBuffer().DangerousGetSingleMemory()); - Assert.True(image.TryGetSinglePixelSpan(out Span imageSpan)); - imageSpan.Fill(bg); - for (var i = 10; i < 20; i++) + image.GetPixelMemoryGroup().Fill(bg); + + image.ProcessPixelRows(accessor => { - image.GetPixelRowSpan(i).Slice(10, 10).Fill(fg); - } + for (var i = 10; i < 20; i++) + { + accessor.GetRowSpan(i).Slice(10, 10).Fill(fg); + } + }); } Assert.False(memoryManager.IsDisposed); } + if (!Directory.Exists(TestEnvironment.ActualOutputDirectoryFullPath)) + { + Directory.CreateDirectory(TestEnvironment.ActualOutputDirectoryFullPath); + } + string fn = System.IO.Path.Combine( TestEnvironment.ActualOutputDirectoryFullPath, $"{nameof(this.WrapSystemDrawingBitmap_WhenObserved)}.bmp"); @@ -196,12 +207,14 @@ public void WrapSystemDrawingBitmap_WhenOwned() using (var image = Image.WrapMemory(memoryManager, bmp.Width, bmp.Height)) { Assert.Equal(memoryManager.Memory, image.GetRootFramePixelBuffer().DangerousGetSingleMemory()); - Assert.True(image.TryGetSinglePixelSpan(out Span imageSpan)); - imageSpan.Fill(bg); - for (var i = 10; i < 20; i++) + image.GetPixelMemoryGroup().Fill(bg); + image.ProcessPixelRows(accessor => { - image.GetPixelRowSpan(i).Slice(10, 10).Fill(fg); - } + for (var i = 10; i < 20; i++) + { + accessor.GetRowSpan(i).Slice(10, 10).Fill(fg); + } + }); } Assert.True(memoryManager.IsDisposed); @@ -225,8 +238,8 @@ public void WrapMemory_FromBytes_CreatedImageIsCorrect() using (var image = Image.WrapMemory(cfg, memory, 5, 5, metaData)) { - Assert.True(image.TryGetSinglePixelSpan(out Span imageSpan)); - ref Rgba32 pixel0 = ref imageSpan[0]; + Assert.True(image.DangerousTryGetSinglePixelMemory(out Memory imageMem)); + ref Rgba32 pixel0 = ref imageMem.Span[0]; Assert.True(Unsafe.AreSame(ref Unsafe.As(ref array[0]), ref pixel0)); Assert.Equal(cfg, image.GetConfiguration()); @@ -262,12 +275,14 @@ public void WrapSystemDrawingBitmap_FromBytes_WhenObserved() Assert.Equal(pixelSpan.Length, imageSpan.Length); Assert.True(Unsafe.AreSame(ref pixelSpan.GetPinnableReference(), ref imageSpan.GetPinnableReference())); - Assert.True(image.TryGetSinglePixelSpan(out imageSpan)); - imageSpan.Fill(bg); - for (var i = 10; i < 20; i++) + image.GetPixelMemoryGroup().Fill(bg); + image.ProcessPixelRows(accessor => { - image.GetPixelRowSpan(i).Slice(10, 10).Fill(fg); - } + for (var i = 10; i < 20; i++) + { + accessor.GetRowSpan(i).Slice(10, 10).Fill(fg); + } + }); } Assert.False(memoryManager.IsDisposed); @@ -293,7 +308,8 @@ public unsafe void WrapMemory_FromPointer_CreatedImageIsCorrect() { using (var image = Image.WrapMemory(cfg, ptr, 5, 5, metaData)) { - Assert.True(image.TryGetSinglePixelSpan(out Span imageSpan)); + Assert.True(image.DangerousTryGetSinglePixelMemory(out Memory imageMem)); + Span imageSpan = imageMem.Span; ref Rgba32 pixel0 = ref imageSpan[0]; Assert.True(Unsafe.AreSame(ref array[0], ref pixel0)); ref Rgba32 pixel_1 = ref imageSpan[imageSpan.Length - 1]; @@ -331,12 +347,14 @@ public unsafe void WrapSystemDrawingBitmap_FromPointer() Assert.Equal(pixelSpan.Length, imageSpan.Length); Assert.True(Unsafe.AreSame(ref pixelSpan.GetPinnableReference(), ref imageSpan.GetPinnableReference())); - Assert.True(image.TryGetSinglePixelSpan(out imageSpan)); - imageSpan.Fill(bg); - for (var i = 10; i < 20; i++) + image.GetPixelMemoryGroup().Fill(bg); + image.ProcessPixelRows(accessor => { - image.GetPixelRowSpan(i).Slice(10, 10).Fill(fg); - } + for (var i = 10; i < 20; i++) + { + accessor.GetRowSpan(i).Slice(10, 10).Fill(fg); + } + }); } Assert.False(memoryManager.IsDisposed); @@ -414,16 +432,19 @@ public void WrapMemory_IMemoryOwnerOfT_ValidSize(int size, int height, int width Assert.Equal(width, img.Width); Assert.Equal(height, img.Height); - for (int i = 0; i < height; ++i) + img.ProcessPixelRows(accessor => { - var arrayIndex = width * i; + for (int i = 0; i < height; ++i) + { + var arrayIndex = width * i; - Span rowSpan = img.GetPixelRowSpan(i); - ref Rgba32 r0 = ref rowSpan[0]; - ref Rgba32 r1 = ref array[arrayIndex]; + Span rowSpan = accessor.GetRowSpan(i); + ref Rgba32 r0 = ref rowSpan[0]; + ref Rgba32 r1 = ref array[arrayIndex]; - Assert.True(Unsafe.AreSame(ref r0, ref r1)); - } + Assert.True(Unsafe.AreSame(ref r0, ref r1)); + } + }); } Assert.True(memory.Disposed); @@ -458,16 +479,19 @@ public void WrapMemory_IMemoryOwnerOfByte_ValidSize(int size, int height, int wi Assert.Equal(width, img.Width); Assert.Equal(height, img.Height); - for (int i = 0; i < height; ++i) + img.ProcessPixelRows(acccessor => { - var arrayIndex = pixelSize * width * i; + for (int i = 0; i < height; ++i) + { + var arrayIndex = pixelSize * width * i; - Span rowSpan = img.GetPixelRowSpan(i); - ref Rgba32 r0 = ref rowSpan[0]; - ref Rgba32 r1 = ref Unsafe.As(ref array[arrayIndex]); + Span rowSpan = acccessor.GetRowSpan(i); + ref Rgba32 r0 = ref rowSpan[0]; + ref Rgba32 r1 = ref Unsafe.As(ref array[arrayIndex]); - Assert.True(Unsafe.AreSame(ref r0, ref r1)); - } + Assert.True(Unsafe.AreSame(ref r0, ref r1)); + } + }); } Assert.True(memory.Disposed); diff --git a/tests/ImageSharp.Tests/Image/ImageTests.cs b/tests/ImageSharp.Tests/Image/ImageTests.cs index db04794844..0a9e2817a5 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.cs @@ -3,8 +3,11 @@ using System; using System.IO; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Jpeg; +using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.Metadata; using SixLabors.ImageSharp.PixelFormats; @@ -29,8 +32,8 @@ public void Width_Height() { Assert.Equal(11, image.Width); Assert.Equal(23, image.Height); - Assert.True(image.TryGetSinglePixelSpan(out Span imageSpan)); - Assert.Equal(11 * 23, imageSpan.Length); + Assert.True(image.DangerousTryGetSinglePixelMemory(out Memory imageMem)); + Assert.Equal(11 * 23, imageMem.Length); image.ComparePixelBufferTo(default(Rgba32)); Assert.Equal(Configuration.Default, image.GetConfiguration()); @@ -46,8 +49,8 @@ public void Configuration_Width_Height() { Assert.Equal(11, image.Width); Assert.Equal(23, image.Height); - Assert.True(image.TryGetSinglePixelSpan(out Span imageSpan)); - Assert.Equal(11 * 23, imageSpan.Length); + Assert.True(image.DangerousTryGetSinglePixelMemory(out Memory imageMem)); + Assert.Equal(11 * 23, imageMem.Length); image.ComparePixelBufferTo(default(Rgba32)); Assert.Equal(configuration, image.GetConfiguration()); @@ -64,8 +67,8 @@ public void Configuration_Width_Height_BackgroundColor() { Assert.Equal(11, image.Width); Assert.Equal(23, image.Height); - Assert.True(image.TryGetSinglePixelSpan(out Span imageSpan)); - Assert.Equal(11 * 23, imageSpan.Length); + Assert.True(image.DangerousTryGetSinglePixelMemory(out Memory imageMem)); + Assert.Equal(11 * 23, imageMem.Length); image.ComparePixelBufferTo(color); Assert.Equal(configuration, image.GetConfiguration()); @@ -97,12 +100,8 @@ public class Indexer { private readonly Configuration configuration = Configuration.CreateDefaultInstance(); - private void LimitBufferCapacity(int bufferCapacityInBytes) - { - var allocator = ArrayPoolMemoryAllocator.CreateDefault(); - this.configuration.MemoryAllocator = allocator; - allocator.BufferCapacityInBytes = bufferCapacityInBytes; - } + private void LimitBufferCapacity(int bufferCapacityInBytes) => + this.configuration.MemoryAllocator = new TestMemoryAllocator { BufferCapacityInBytes = bufferCapacityInBytes }; [Theory] [InlineData(false)] @@ -171,6 +170,95 @@ public void Set_OutOfRangeY(bool enforceDisco, int y) ArgumentOutOfRangeException ex = Assert.Throws(() => image[3, y] = default); Assert.Equal("y", ex.ParamName); } + + [Theory] + [InlineData(false, false)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(true, true)] + public void CopyPixelDataTo_Success(bool disco, bool byteSpan) + { + if (disco) + { + this.LimitBufferCapacity(20); + } + + using var image = new Image(this.configuration, 10, 10); + if (disco) + { + Assert.True(image.GetPixelMemoryGroup().Count > 1); + } + + byte[] expected = TestUtils.FillImageWithRandomBytes(image); + byte[] actual = new byte[expected.Length]; + if (byteSpan) + { + image.CopyPixelDataTo(actual); + } + else + { + Span destination = MemoryMarshal.Cast(actual); + image.CopyPixelDataTo(destination); + } + + Assert.True(expected.AsSpan().SequenceEqual(actual)); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void CopyPixelDataTo_DestinationTooShort_Throws(bool byteSpan) + { + using var image = new Image(this.configuration, 10, 10); + + Assert.ThrowsAny(() => + { + if (byteSpan) + { + image.CopyPixelDataTo(new byte[199]); + } + else + { + image.CopyPixelDataTo(new La16[99]); + } + }); + } + } + + public class ProcessPixelRows : ProcessPixelRowsTestBase + { + protected override void ProcessPixelRowsImpl( + Image image, + PixelAccessorAction processPixels) => + image.ProcessPixelRows(processPixels); + + protected override void ProcessPixelRowsImpl( + Image image1, + Image image2, + PixelAccessorAction processPixels) => + image1.ProcessPixelRows(image2, processPixels); + + protected override void ProcessPixelRowsImpl( + Image image1, + Image image2, + Image image3, + PixelAccessorAction processPixels) => + image1.ProcessPixelRows(image2, image3, processPixels); + + [Fact] + public void NullReference_Throws() + { + using var img = new Image(1, 1); + + Assert.Throws(() => img.ProcessPixelRows(null)); + + Assert.Throws(() => img.ProcessPixelRows((Image)null, (_, _) => { })); + Assert.Throws(() => img.ProcessPixelRows(img, img, null)); + + Assert.Throws(() => img.ProcessPixelRows((Image)null, img, (_, _, _) => { })); + Assert.Throws(() => img.ProcessPixelRows(img, (Image)null, (_, _, _) => { })); + Assert.Throws(() => img.ProcessPixelRows(img, img, null)); + } } public class Dispose @@ -232,12 +320,42 @@ public void NonPrivateMethods_ObjectDisposedException() // Image Assert.Throws(() => { var res = image.Clone(this.configuration); }); Assert.Throws(() => { var res = image.CloneAs(this.configuration); }); - Assert.Throws(() => { var res = image.GetPixelRowSpan(default); }); - Assert.Throws(() => { var res = image.TryGetSinglePixelSpan(out var _); }); + Assert.Throws(() => { var res = image.DangerousTryGetSinglePixelMemory(out Memory _); }); // Image Assert.Throws(() => { var res = genericImage.CloneAs(this.configuration); }); } } + + public class DetectEncoder + { + [Fact] + public void KnownExtension_ReturnsEncoder() + { + using var image = new Image(1, 1); + IImageEncoder encoder = image.DetectEncoder("dummy.png"); + Assert.NotNull(encoder); + Assert.IsType(encoder); + } + + [Fact] + public void UnknownExtension_ThrowsNotSupportedException() + { + using var image = new Image(1, 1); + Assert.Throws(() => image.DetectEncoder("dummy.yolo")); + } + + [Fact] + public void NoDetectorRegisteredForKnownExtension_ThrowsNotSupportedException() + { + var configuration = new Configuration(); + var format = new TestFormat(); + configuration.ImageFormatsManager.AddImageFormat(format); + configuration.ImageFormatsManager.AddImageFormatDetector(new MockImageFormatDetector(format)); + + using var image = new Image(configuration, 1, 1); + Assert.Throws(() => image.DetectEncoder($"dummy.{format.Extension}")); + } + } } } diff --git a/tests/ImageSharp.Tests/Image/LargeImageIntegrationTests.cs b/tests/ImageSharp.Tests/Image/LargeImageIntegrationTests.cs index afa217bbc9..b2ee9d673e 100644 --- a/tests/ImageSharp.Tests/Image/LargeImageIntegrationTests.cs +++ b/tests/ImageSharp.Tests/Image/LargeImageIntegrationTests.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. using System; +using Microsoft.DotNet.RemoteExecutor; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using Xunit; @@ -19,14 +21,31 @@ public void CreateAndResize(TestImageProvider provider) image.DebugSave(provider); } + [Fact] + public void PreferContiguousImageBuffers_CreateImage_BufferIsContiguous() + { + // Run remotely to avoid large allocation in the test process: + RemoteExecutor.Invoke(RunTest).Dispose(); + + static void RunTest() + { + Configuration configuration = Configuration.Default.Clone(); + configuration.PreferContiguousImageBuffers = true; + + using var image = new Image(configuration, 8192, 4096); + Assert.True(image.DangerousTryGetSinglePixelMemory(out Memory mem)); + Assert.Equal(8192 * 4096, mem.Length); + } + } + [Theory] [WithBasicTestPatternImages(width: 10, height: 10, PixelTypes.Rgba32)] - public void GetSingleSpan(TestImageProvider provider) + public void DangerousTryGetSinglePixelMemory_WhenImageTooLarge_ReturnsFalse(TestImageProvider provider) { provider.LimitAllocatorBufferCapacity().InPixels(10); using Image image = provider.GetImage(); - Assert.False(image.TryGetSinglePixelSpan(out Span imageSpan)); - Assert.False(image.Frames.RootFrame.TryGetSinglePixelSpan(out Span imageFrameSpan)); + Assert.False(image.DangerousTryGetSinglePixelMemory(out Memory mem)); + Assert.False(image.Frames.RootFrame.DangerousTryGetSinglePixelMemory(out Memory _)); } } } diff --git a/tests/ImageSharp.Tests/Image/ProcessPixelRowsTestBase.cs b/tests/ImageSharp.Tests/Image/ProcessPixelRowsTestBase.cs new file mode 100644 index 0000000000..fa0752e775 --- /dev/null +++ b/tests/ImageSharp.Tests/Image/ProcessPixelRowsTestBase.cs @@ -0,0 +1,323 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Collections.Generic; +using Microsoft.DotNet.RemoteExecutor; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Memory.Internals; +using SixLabors.ImageSharp.PixelFormats; +using Xunit; + +namespace SixLabors.ImageSharp.Tests +{ + public abstract class ProcessPixelRowsTestBase + { + protected abstract void ProcessPixelRowsImpl( + Image image, + PixelAccessorAction processPixels) + where TPixel : unmanaged, IPixel; + + protected abstract void ProcessPixelRowsImpl( + Image image1, + Image image2, + PixelAccessorAction processPixels) + where TPixel : unmanaged, IPixel; + + protected abstract void ProcessPixelRowsImpl( + Image image1, + Image image2, + Image image3, + PixelAccessorAction processPixels) + where TPixel : unmanaged, IPixel; + + [Fact] + public void PixelAccessorDimensionsAreCorrect() + { + using var image = new Image(123, 456); + this.ProcessPixelRowsImpl(image, accessor => + { + Assert.Equal(123, accessor.Width); + Assert.Equal(456, accessor.Height); + }); + } + + [Fact] + public void WriteImagePixels_SingleImage() + { + using var image = new Image(256, 256); + this.ProcessPixelRowsImpl(image, accessor => + { + for (int y = 0; y < accessor.Height; y++) + { + Span row = accessor.GetRowSpan(y); + for (int x = 0; x < row.Length; x++) + { + row[x] = new L16((ushort)(x * y)); + } + } + }); + + Buffer2D buffer = image.Frames.RootFrame.PixelBuffer; + for (int y = 0; y < 256; y++) + { + Span row = buffer.DangerousGetRowSpan(y); + for (int x = 0; x < 256; x++) + { + int actual = row[x].PackedValue; + Assert.Equal(x * y, actual); + } + } + } + + [Fact] + public void WriteImagePixels_MultiImage2() + { + using var img1 = new Image(256, 256); + Buffer2D buffer = img1.Frames.RootFrame.PixelBuffer; + for (int y = 0; y < 256; y++) + { + Span row = buffer.DangerousGetRowSpan(y); + for (int x = 0; x < 256; x++) + { + row[x] = new L16((ushort)(x * y)); + } + } + + using var img2 = new Image(256, 256); + + this.ProcessPixelRowsImpl(img1, img2, (accessor1, accessor2) => + { + for (int y = 0; y < accessor1.Height; y++) + { + Span row1 = accessor1.GetRowSpan(y); + Span row2 = accessor2.GetRowSpan(accessor2.Height - y - 1); + row1.CopyTo(row2); + } + }); + + buffer = img2.Frames.RootFrame.PixelBuffer; + for (int y = 0; y < 256; y++) + { + Span row = buffer.DangerousGetRowSpan(y); + for (int x = 0; x < 256; x++) + { + int actual = row[x].PackedValue; + Assert.Equal(x * (256 - y - 1), actual); + } + } + } + + [Fact] + public void WriteImagePixels_MultiImage3() + { + using var img1 = new Image(256, 256); + Buffer2D buffer2 = img1.Frames.RootFrame.PixelBuffer; + for (int y = 0; y < 256; y++) + { + Span row = buffer2.DangerousGetRowSpan(y); + for (int x = 0; x < 256; x++) + { + row[x] = new L16((ushort)(x * y)); + } + } + + using var img2 = new Image(256, 256); + using var img3 = new Image(256, 256); + + this.ProcessPixelRowsImpl(img1, img2, img3, (accessor1, accessor2, accessor3) => + { + for (int y = 0; y < accessor1.Height; y++) + { + Span row1 = accessor1.GetRowSpan(y); + Span row2 = accessor2.GetRowSpan(accessor2.Height - y - 1); + Span row3 = accessor3.GetRowSpan(y); + row1.CopyTo(row2); + row1.CopyTo(row3); + } + }); + + buffer2 = img2.Frames.RootFrame.PixelBuffer; + Buffer2D buffer3 = img3.Frames.RootFrame.PixelBuffer; + for (int y = 0; y < 256; y++) + { + Span row2 = buffer2.DangerousGetRowSpan(y); + Span row3 = buffer3.DangerousGetRowSpan(y); + for (int x = 0; x < 256; x++) + { + int actual2 = row2[x].PackedValue; + int actual3 = row3[x].PackedValue; + Assert.Equal(x * (256 - y - 1), actual2); + Assert.Equal(x * y, actual3); + } + } + } + + [Fact] + public void Disposed_ThrowsObjectDisposedException() + { + using var nonDisposed = new Image(1, 1); + var disposed = new Image(1, 1); + disposed.Dispose(); + + Assert.Throws(() => this.ProcessPixelRowsImpl(disposed, _ => { })); + + Assert.Throws(() => this.ProcessPixelRowsImpl(disposed, nonDisposed, (_, _) => { })); + Assert.Throws(() => this.ProcessPixelRowsImpl(nonDisposed, disposed, (_, _) => { })); + + Assert.Throws(() => this.ProcessPixelRowsImpl(disposed, nonDisposed, nonDisposed, (_, _, _) => { })); + Assert.Throws(() => this.ProcessPixelRowsImpl(nonDisposed, disposed, nonDisposed, (_, _, _) => { })); + Assert.Throws(() => this.ProcessPixelRowsImpl(nonDisposed, nonDisposed, disposed, (_, _, _) => { })); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void RetainsUnmangedBuffers1(bool throwException) + { + RemoteExecutor.Invoke(RunTest, this.GetType().FullName, throwException.ToString()).Dispose(); + + static void RunTest(string testTypeName, string throwExceptionStr) + { + bool throwExceptionInner = bool.Parse(throwExceptionStr); + var buffer = UnmanagedBuffer.Allocate(100); + var allocator = new MockUnmanagedMemoryAllocator(buffer); + Configuration.Default.MemoryAllocator = allocator; + + var image = new Image(10, 10); + + Assert.Equal(1, UnmanagedMemoryHandle.TotalOutstandingHandles); + try + { + GetTest(testTypeName).ProcessPixelRowsImpl(image, _ => + { + ((IDisposable)buffer).Dispose(); + Assert.Equal(1, UnmanagedMemoryHandle.TotalOutstandingHandles); + if (throwExceptionInner) + { + throw new NonFatalException(); + } + }); + } + catch (NonFatalException) + { + } + + Assert.Equal(0, UnmanagedMemoryHandle.TotalOutstandingHandles); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void RetainsUnmangedBuffers2(bool throwException) + { + RemoteExecutor.Invoke(RunTest, this.GetType().FullName, throwException.ToString()).Dispose(); + + static void RunTest(string testTypeName, string throwExceptionStr) + { + bool throwExceptionInner = bool.Parse(throwExceptionStr); + var buffer1 = UnmanagedBuffer.Allocate(100); + var buffer2 = UnmanagedBuffer.Allocate(100); + var allocator = new MockUnmanagedMemoryAllocator(buffer1, buffer2); + Configuration.Default.MemoryAllocator = allocator; + + var image1 = new Image(10, 10); + var image2 = new Image(10, 10); + + Assert.Equal(2, UnmanagedMemoryHandle.TotalOutstandingHandles); + try + { + GetTest(testTypeName).ProcessPixelRowsImpl(image1, image2, (_, _) => + { + ((IDisposable)buffer1).Dispose(); + ((IDisposable)buffer2).Dispose(); + Assert.Equal(2, UnmanagedMemoryHandle.TotalOutstandingHandles); + if (throwExceptionInner) + { + throw new NonFatalException(); + } + }); + } + catch (NonFatalException) + { + } + + Assert.Equal(0, UnmanagedMemoryHandle.TotalOutstandingHandles); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void RetainsUnmangedBuffers3(bool throwException) + { + RemoteExecutor.Invoke(RunTest, this.GetType().FullName, throwException.ToString()).Dispose(); + + static void RunTest(string testTypeName, string throwExceptionStr) + { + bool throwExceptionInner = bool.Parse(throwExceptionStr); + var buffer1 = UnmanagedBuffer.Allocate(100); + var buffer2 = UnmanagedBuffer.Allocate(100); + var buffer3 = UnmanagedBuffer.Allocate(100); + var allocator = new MockUnmanagedMemoryAllocator(buffer1, buffer2, buffer3); + Configuration.Default.MemoryAllocator = allocator; + + var image1 = new Image(10, 10); + var image2 = new Image(10, 10); + var image3 = new Image(10, 10); + + Assert.Equal(3, UnmanagedMemoryHandle.TotalOutstandingHandles); + try + { + GetTest(testTypeName).ProcessPixelRowsImpl(image1, image2, image3, (_, _, _) => + { + ((IDisposable)buffer1).Dispose(); + ((IDisposable)buffer2).Dispose(); + ((IDisposable)buffer3).Dispose(); + Assert.Equal(3, UnmanagedMemoryHandle.TotalOutstandingHandles); + if (throwExceptionInner) + { + throw new NonFatalException(); + } + }); + } + catch (NonFatalException) + { + } + + Assert.Equal(0, UnmanagedMemoryHandle.TotalOutstandingHandles); + } + } + + private static ProcessPixelRowsTestBase GetTest(string testTypeName) + { + Type type = typeof(ProcessPixelRowsTestBase).Assembly.GetType(testTypeName); + return (ProcessPixelRowsTestBase)Activator.CreateInstance(type); + } + + private class NonFatalException : Exception + { + } + + private class MockUnmanagedMemoryAllocator : MemoryAllocator + where T1 : struct + { + private Stack> buffers = new(); + + public MockUnmanagedMemoryAllocator(params UnmanagedBuffer[] buffers) + { + foreach (UnmanagedBuffer buffer in buffers) + { + this.buffers.Push(buffer); + } + } + + protected internal override int GetBufferCapacityInBytes() => int.MaxValue; + + public override IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None) => + this.buffers.Pop() as IMemoryOwner; + } + } +} diff --git a/tests/ImageSharp.Tests/Memory/Allocators/ArrayPoolMemoryAllocatorTests.cs b/tests/ImageSharp.Tests/Memory/Allocators/ArrayPoolMemoryAllocatorTests.cs deleted file mode 100644 index dd53b0b56f..0000000000 --- a/tests/ImageSharp.Tests/Memory/Allocators/ArrayPoolMemoryAllocatorTests.cs +++ /dev/null @@ -1,284 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Apache License, Version 2.0. - -using System; -using System.Buffers; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using Microsoft.DotNet.RemoteExecutor; -using SixLabors.ImageSharp.Memory; -using Xunit; - -namespace SixLabors.ImageSharp.Tests.Memory.Allocators -{ - [Collection("RunSerial")] - public class ArrayPoolMemoryAllocatorTests - { - private const int MaxPooledBufferSizeInBytes = 2048; - - private const int PoolSelectorThresholdInBytes = MaxPooledBufferSizeInBytes / 2; - - /// - /// Gets the SUT for in-process tests. - /// - private MemoryAllocatorFixture LocalFixture { get; } = new MemoryAllocatorFixture(); - - /// - /// Gets the SUT for tests executed by , - /// recreated in each external process. - /// - private static MemoryAllocatorFixture StaticFixture { get; } = new MemoryAllocatorFixture(); - - public class BufferTests : BufferTestSuite - { - public BufferTests() - : base(new ArrayPoolMemoryAllocator(MaxPooledBufferSizeInBytes, PoolSelectorThresholdInBytes)) - { - } - } - - public class Constructor - { - [Fact] - public void WhenBothParametersPassedByUser() - { - var mgr = new ArrayPoolMemoryAllocator(1111, 666); - Assert.Equal(1111, mgr.MaxPoolSizeInBytes); - Assert.Equal(666, mgr.PoolSelectorThresholdInBytes); - } - - [Fact] - public void WhenPassedOnly_MaxPooledBufferSizeInBytes_SmallerThresholdValueIsAutoCalculated() - { - var mgr = new ArrayPoolMemoryAllocator(5000); - Assert.Equal(5000, mgr.MaxPoolSizeInBytes); - Assert.True(mgr.PoolSelectorThresholdInBytes < mgr.MaxPoolSizeInBytes); - } - - [Fact] - public void When_PoolSelectorThresholdInBytes_IsGreaterThan_MaxPooledBufferSizeInBytes_ExceptionIsThrown() - => Assert.ThrowsAny(() => new ArrayPoolMemoryAllocator(100, 200)); - } - - [Theory] - [InlineData(32)] - [InlineData(512)] - [InlineData(MaxPooledBufferSizeInBytes - 1)] - public void SmallBuffersArePooled_OfByte(int size) => Assert.True(this.LocalFixture.CheckIsRentingPooledBuffer(size)); - - [Theory] - [InlineData(128 * 1024 * 1024)] - [InlineData(MaxPooledBufferSizeInBytes + 1)] - public void LargeBuffersAreNotPooled_OfByte(int size) - { - static void RunTest(string sizeStr) - { - int size = int.Parse(sizeStr); - StaticFixture.CheckIsRentingPooledBuffer(size); - } - - RemoteExecutor.Invoke(RunTest, size.ToString()).Dispose(); - } - - [Fact] - public unsafe void SmallBuffersArePooled_OfBigValueType() - { - int count = (MaxPooledBufferSizeInBytes / sizeof(LargeStruct)) - 1; - - Assert.True(this.LocalFixture.CheckIsRentingPooledBuffer(count)); - } - - [Fact] - public unsafe void LaregeBuffersAreNotPooled_OfBigValueType() - { - int count = (MaxPooledBufferSizeInBytes / sizeof(LargeStruct)) + 1; - - Assert.False(this.LocalFixture.CheckIsRentingPooledBuffer(count)); - } - - [Theory] - [InlineData(AllocationOptions.None)] - [InlineData(AllocationOptions.Clean)] - public void CleaningRequests_AreControlledByAllocationParameter_Clean(AllocationOptions options) - { - MemoryAllocator memoryAllocator = this.LocalFixture.MemoryAllocator; - using (IMemoryOwner firstAlloc = memoryAllocator.Allocate(42)) - { - BufferExtensions.GetSpan(firstAlloc).Fill(666); - } - - using (IMemoryOwner secondAlloc = memoryAllocator.Allocate(42, options)) - { - int expected = options == AllocationOptions.Clean ? 0 : 666; - Assert.Equal(expected, BufferExtensions.GetSpan(secondAlloc)[0]); - } - } - - [Fact] - public unsafe void Allocate_MemoryIsPinnableMultipleTimes() - { - ArrayPoolMemoryAllocator allocator = this.LocalFixture.MemoryAllocator; - using IMemoryOwner memoryOwner = allocator.Allocate(100); - - using (MemoryHandle pin = memoryOwner.Memory.Pin()) - { - Assert.NotEqual(IntPtr.Zero, (IntPtr)pin.Pointer); - } - - using (MemoryHandle pin = memoryOwner.Memory.Pin()) - { - Assert.NotEqual(IntPtr.Zero, (IntPtr)pin.Pointer); - } - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public void ReleaseRetainedResources_ReplacesInnerArrayPool(bool keepBufferAlive) - { - MemoryAllocator memoryAllocator = this.LocalFixture.MemoryAllocator; - IMemoryOwner buffer = memoryAllocator.Allocate(32); - ref int ptrToPrev0 = ref MemoryMarshal.GetReference(BufferExtensions.GetSpan(buffer)); - - if (!keepBufferAlive) - { - buffer.Dispose(); - } - - memoryAllocator.ReleaseRetainedResources(); - - buffer = memoryAllocator.Allocate(32); - - Assert.False(Unsafe.AreSame(ref ptrToPrev0, ref BufferExtensions.GetReference(buffer))); - } - - [Fact] - public void ReleaseRetainedResources_DisposingPreviouslyAllocatedBuffer_IsAllowed() - { - MemoryAllocator memoryAllocator = this.LocalFixture.MemoryAllocator; - IMemoryOwner buffer = memoryAllocator.Allocate(32); - memoryAllocator.ReleaseRetainedResources(); - buffer.Dispose(); - } - - [Fact] - public void AllocationOverLargeArrayThreshold_UsesDifferentPool() - { - static void RunTest() - { - const int ArrayLengthThreshold = PoolSelectorThresholdInBytes / sizeof(int); - - IMemoryOwner small = StaticFixture.MemoryAllocator.Allocate(ArrayLengthThreshold - 1); - ref int ptr2Small = ref BufferExtensions.GetReference(small); - small.Dispose(); - - IMemoryOwner large = StaticFixture.MemoryAllocator.Allocate(ArrayLengthThreshold + 1); - - Assert.False(Unsafe.AreSame(ref ptr2Small, ref BufferExtensions.GetReference(large))); - } - - RemoteExecutor.Invoke(RunTest).Dispose(); - } - - [Fact] - public void CreateWithAggressivePooling() - { - static void RunTest() - { - StaticFixture.MemoryAllocator = ArrayPoolMemoryAllocator.CreateWithAggressivePooling(); - Assert.True(StaticFixture.CheckIsRentingPooledBuffer(4096 * 4096)); - } - - RemoteExecutor.Invoke(RunTest).Dispose(); - } - - [Fact] - public void CreateDefault() - { - static void RunTest() - { - StaticFixture.MemoryAllocator = ArrayPoolMemoryAllocator.CreateDefault(); - - Assert.False(StaticFixture.CheckIsRentingPooledBuffer(2 * 4096 * 4096)); - Assert.True(StaticFixture.CheckIsRentingPooledBuffer(2048 * 2048)); - } - - RemoteExecutor.Invoke(RunTest).Dispose(); - } - - [Fact] - public void CreateWithModeratePooling() - { - static void RunTest() - { - StaticFixture.MemoryAllocator = ArrayPoolMemoryAllocator.CreateWithModeratePooling(); - Assert.False(StaticFixture.CheckIsRentingPooledBuffer(2048 * 2048)); - Assert.True(StaticFixture.CheckIsRentingPooledBuffer(1024 * 16)); - } - - RemoteExecutor.Invoke(RunTest).Dispose(); - } - - [Theory] - [InlineData(-1)] - [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(-1)] - public void AllocateManagedByteBuffer_IncorrectAmount_ThrowsCorrect_ArgumentOutOfRangeException(int length) - { - ArgumentOutOfRangeException ex = Assert.Throws(() => - this.LocalFixture.MemoryAllocator.AllocateManagedByteBuffer(length)); - Assert.Equal("length", ex.ParamName); - } - - private class MemoryAllocatorFixture - { - public ArrayPoolMemoryAllocator MemoryAllocator { get; set; } = - new ArrayPoolMemoryAllocator(MaxPooledBufferSizeInBytes, PoolSelectorThresholdInBytes); - - /// - /// Rent a buffer -> return it -> re-rent -> verify if it's span points to the previous location. - /// - public bool CheckIsRentingPooledBuffer(int length) - where T : struct - { - IMemoryOwner buffer = this.MemoryAllocator.Allocate(length); - ref T ptrToPrevPosition0 = ref BufferExtensions.GetReference(buffer); - buffer.Dispose(); - - buffer = this.MemoryAllocator.Allocate(length); - bool sameBuffers = Unsafe.AreSame(ref ptrToPrevPosition0, ref BufferExtensions.GetReference(buffer)); - buffer.Dispose(); - - return sameBuffers; - } - } - - [StructLayout(LayoutKind.Sequential)] - private struct SmallStruct - { - private readonly uint dummy; - } - - private const int SizeOfLargeStruct = MaxPooledBufferSizeInBytes / 5; - - [StructLayout(LayoutKind.Explicit, Size = SizeOfLargeStruct)] - private struct LargeStruct - { - } - } -} diff --git a/tests/ImageSharp.Tests/Memory/Allocators/BufferTestSuite.cs b/tests/ImageSharp.Tests/Memory/Allocators/BufferTestSuite.cs index 1cadf16536..c3c3d40b9d 100644 --- a/tests/ImageSharp.Tests/Memory/Allocators/BufferTestSuite.cs +++ b/tests/ImageSharp.Tests/Memory/Allocators/BufferTestSuite.cs @@ -96,8 +96,7 @@ private void TestHasCorrectLength(int desiredLength) [MemberData(nameof(LengthValues))] public void CanAllocateCleanBuffer_byte(int desiredLength) { - this.TestCanAllocateCleanBuffer(desiredLength, false); - this.TestCanAllocateCleanBuffer(desiredLength, true); + this.TestCanAllocateCleanBuffer(desiredLength); } [Theory] @@ -114,30 +113,14 @@ public void CanAllocateCleanBuffer_CustomStruct(int desiredLength) this.TestCanAllocateCleanBuffer(desiredLength); } - private IMemoryOwner Allocate(int desiredLength, AllocationOptions options, bool managedByteBuffer) - where T : struct - { - if (managedByteBuffer) - { - if (!(this.MemoryAllocator.AllocateManagedByteBuffer(desiredLength, options) is IMemoryOwner buffer)) - { - throw new InvalidOperationException("typeof(T) != typeof(byte)"); - } - - return buffer; - } - - return this.MemoryAllocator.Allocate(desiredLength, options); - } - - private void TestCanAllocateCleanBuffer(int desiredLength, bool testManagedByteBuffer = false) + private void TestCanAllocateCleanBuffer(int desiredLength) where T : struct, IEquatable { ReadOnlySpan expected = new T[desiredLength]; for (int i = 0; i < 10; i++) { - using (IMemoryOwner buffer = this.Allocate(desiredLength, AllocationOptions.Clean, testManagedByteBuffer)) + using (IMemoryOwner buffer = this.MemoryAllocator.Allocate(desiredLength, AllocationOptions.Clean)) { Assert.True(buffer.GetSpan().SequenceEqual(expected)); } @@ -155,14 +138,13 @@ public void SpanPropertyIsAlwaysTheSame_int(int desiredLength) [MemberData(nameof(LengthValues))] public void SpanPropertyIsAlwaysTheSame_byte(int desiredLength) { - this.TestSpanPropertyIsAlwaysTheSame(desiredLength, false); - this.TestSpanPropertyIsAlwaysTheSame(desiredLength, true); + this.TestSpanPropertyIsAlwaysTheSame(desiredLength); } - private void TestSpanPropertyIsAlwaysTheSame(int desiredLength, bool testManagedByteBuffer = false) + private void TestSpanPropertyIsAlwaysTheSame(int desiredLength) where T : struct { - using (IMemoryOwner buffer = this.Allocate(desiredLength, AllocationOptions.None, testManagedByteBuffer)) + using (IMemoryOwner buffer = this.MemoryAllocator.Allocate(desiredLength, AllocationOptions.None)) { ref T a = ref MemoryMarshal.GetReference(buffer.GetSpan()); ref T b = ref MemoryMarshal.GetReference(buffer.GetSpan()); @@ -184,14 +166,13 @@ public void WriteAndReadElements_float(int desiredLength) [MemberData(nameof(LengthValues))] public void WriteAndReadElements_byte(int desiredLength) { - this.TestWriteAndReadElements(desiredLength, x => (byte)(x + 1), false); - this.TestWriteAndReadElements(desiredLength, x => (byte)(x + 1), true); + this.TestWriteAndReadElements(desiredLength, x => (byte)(x + 1)); } - private void TestWriteAndReadElements(int desiredLength, Func getExpectedValue, bool testManagedByteBuffer = false) + private void TestWriteAndReadElements(int desiredLength, Func getExpectedValue) where T : struct { - using (IMemoryOwner buffer = this.Allocate(desiredLength, AllocationOptions.None, testManagedByteBuffer)) + using (IMemoryOwner buffer = this.MemoryAllocator.Allocate(desiredLength)) { var expectedVals = new T[buffer.Length()]; @@ -214,8 +195,7 @@ private void TestWriteAndReadElements(int desiredLength, Func getExpe [MemberData(nameof(LengthValues))] public void IndexingSpan_WhenOutOfRange_Throws_byte(int desiredLength) { - this.TestIndexOutOfRangeShouldThrow(desiredLength, false); - this.TestIndexOutOfRangeShouldThrow(desiredLength, true); + this.TestIndexOutOfRangeShouldThrow(desiredLength); } [Theory] @@ -232,12 +212,12 @@ public void IndexingSpan_WhenOutOfRange_Throws_CustomStruct(int desiredLength) this.TestIndexOutOfRangeShouldThrow(desiredLength); } - private T TestIndexOutOfRangeShouldThrow(int desiredLength, bool testManagedByteBuffer = false) + private T TestIndexOutOfRangeShouldThrow(int desiredLength) where T : struct, IEquatable { var dummy = default(T); - using (IMemoryOwner buffer = this.Allocate(desiredLength, AllocationOptions.None, testManagedByteBuffer)) + using (IMemoryOwner buffer = this.MemoryAllocator.Allocate(desiredLength)) { Assert.ThrowsAny( () => @@ -264,23 +244,6 @@ private T TestIndexOutOfRangeShouldThrow(int desiredLength, bool testManagedB return dummy; } - [Theory] - [InlineData(1)] - [InlineData(7)] - [InlineData(1024)] - [InlineData(6666)] - public void ManagedByteBuffer_ArrayIsCorrect(int desiredLength) - { - using (IManagedByteBuffer buffer = this.MemoryAllocator.AllocateManagedByteBuffer(desiredLength)) - { - ref byte array0 = ref buffer.Array[0]; - ref byte span0 = ref buffer.GetReference(); - - Assert.True(Unsafe.AreSame(ref span0, ref array0)); - Assert.True(buffer.Array.Length >= buffer.GetSpan().Length); - } - } - [Fact] public void GetMemory_ReturnsValidMemory() { diff --git a/tests/ImageSharp.Tests/Memory/Allocators/RefCountedLifetimeGuardTests.cs b/tests/ImageSharp.Tests/Memory/Allocators/RefCountedLifetimeGuardTests.cs new file mode 100644 index 0000000000..7fb3b7b7bb --- /dev/null +++ b/tests/ImageSharp.Tests/Memory/Allocators/RefCountedLifetimeGuardTests.cs @@ -0,0 +1,126 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Runtime.CompilerServices; +using Microsoft.DotNet.RemoteExecutor; +using SixLabors.ImageSharp.Memory.Internals; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Memory.Allocators +{ + public class RefCountedLifetimeGuardTests + { + [Theory] + [InlineData(1)] + [InlineData(3)] + public void Dispose_ResultsInSingleRelease(int disposeCount) + { + var guard = new MockLifetimeGuard(); + Assert.Equal(0, guard.ReleaseInvocationCount); + + for (int i = 0; i < disposeCount; i++) + { + guard.Dispose(); + } + + Assert.Equal(1, guard.ReleaseInvocationCount); + } + + [Fact] + public void Finalize_ResultsInSingleRelease() + { + RemoteExecutor.Invoke(RunTest).Dispose(); + + static void RunTest() + { + Assert.Equal(0, MockLifetimeGuard.GlobalReleaseInvocationCount); + LeakGuard(false); + GC.Collect(); + GC.WaitForPendingFinalizers(); + Assert.Equal(1, MockLifetimeGuard.GlobalReleaseInvocationCount); + } + } + + [Theory] + [InlineData(1)] + [InlineData(3)] + public void AddRef_PreventsReleaseOnDispose(int addRefCount) + { + var guard = new MockLifetimeGuard(); + + for (int i = 0; i < addRefCount; i++) + { + guard.AddRef(); + } + + guard.Dispose(); + + for (int i = 0; i < addRefCount; i++) + { + Assert.Equal(0, guard.ReleaseInvocationCount); + guard.ReleaseRef(); + } + + Assert.Equal(1, guard.ReleaseInvocationCount); + } + + [Fact] + public void AddRef_PreventsReleaseOnFinalize() + { + RemoteExecutor.Invoke(RunTest).Dispose(); + + static void RunTest() + { + LeakGuard(true); + GC.Collect(); + GC.WaitForPendingFinalizers(); + Assert.Equal(0, MockLifetimeGuard.GlobalReleaseInvocationCount); + } + } + + [Fact] + public void AddRefReleaseRefMisuse_DoesntLeadToMultipleReleases() + { + var guard = new MockLifetimeGuard(); + guard.Dispose(); + guard.AddRef(); + guard.ReleaseRef(); + + Assert.Equal(1, guard.ReleaseInvocationCount); + } + + [Fact] + public void UnmanagedBufferLifetimeGuard_Handle_IsReturnedByRef() + { + var h = UnmanagedMemoryHandle.Allocate(10); + using var guard = new UnmanagedBufferLifetimeGuard.FreeHandle(h); + Assert.True(guard.Handle.IsValid); + guard.Handle.Free(); + Assert.False(guard.Handle.IsValid); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void LeakGuard(bool addRef) + { + var guard = new MockLifetimeGuard(); + if (addRef) + { + guard.AddRef(); + } + } + + private class MockLifetimeGuard : RefCountedLifetimeGuard + { + public int ReleaseInvocationCount { get; private set; } + + public static int GlobalReleaseInvocationCount { get; private set; } + + protected override void Release() + { + this.ReleaseInvocationCount++; + GlobalReleaseInvocationCount++; + } + } + } +} diff --git a/tests/ImageSharp.Tests/Memory/Allocators/SharedArrayPoolBufferTests.cs b/tests/ImageSharp.Tests/Memory/Allocators/SharedArrayPoolBufferTests.cs new file mode 100644 index 0000000000..fab520e190 --- /dev/null +++ b/tests/ImageSharp.Tests/Memory/Allocators/SharedArrayPoolBufferTests.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Linq; +using Microsoft.DotNet.RemoteExecutor; +using SixLabors.ImageSharp.Memory.Internals; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Memory.Allocators +{ + public class SharedArrayPoolBufferTests + { + [Fact] + public void AllocatesArrayPoolArray() + { + RemoteExecutor.Invoke(RunTest).Dispose(); + + static void RunTest() + { + using (var buffer = new SharedArrayPoolBuffer(900)) + { + Assert.Equal(900, buffer.GetSpan().Length); + buffer.GetSpan().Fill(42); + } + + byte[] array = ArrayPool.Shared.Rent(900); + byte[] expected = Enumerable.Repeat((byte)42, 900).ToArray(); + + Assert.True(expected.AsSpan().SequenceEqual(array.AsSpan(0, 900))); + } + } + + [Fact] + public void OutstandingReferences_RetainArrays() + { + RemoteExecutor.Invoke(RunTest).Dispose(); + + static void RunTest() + { + var buffer = new SharedArrayPoolBuffer(900); + Span span = buffer.GetSpan(); + + buffer.AddRef(); + ((IDisposable)buffer).Dispose(); + span.Fill(42); + byte[] array = ArrayPool.Shared.Rent(900); + Assert.NotEqual(42, array[0]); + ArrayPool.Shared.Return(array); + + buffer.ReleaseRef(); + array = ArrayPool.Shared.Rent(900); + byte[] expected = Enumerable.Repeat((byte)42, 900).ToArray(); + Assert.True(expected.AsSpan().SequenceEqual(array.AsSpan(0, 900))); + ArrayPool.Shared.Return(array); + } + } + } +} diff --git a/tests/ImageSharp.Tests/Memory/Allocators/SimpleGcMemoryAllocatorTests.cs b/tests/ImageSharp.Tests/Memory/Allocators/SimpleGcMemoryAllocatorTests.cs index 0e1f997254..3ab45be82d 100644 --- a/tests/ImageSharp.Tests/Memory/Allocators/SimpleGcMemoryAllocatorTests.cs +++ b/tests/ImageSharp.Tests/Memory/Allocators/SimpleGcMemoryAllocatorTests.cs @@ -29,14 +29,6 @@ public void Allocate_IncorrectAmount_ThrowsCorrect_ArgumentOutOfRangeException(i Assert.Equal("length", ex.ParamName); } - [Theory] - [InlineData(-1)] - public void AllocateManagedByteBuffer_IncorrectAmount_ThrowsCorrect_ArgumentOutOfRangeException(int length) - { - ArgumentOutOfRangeException ex = Assert.Throws(() => this.MemoryAllocator.AllocateManagedByteBuffer(length)); - Assert.Equal("length", ex.ParamName); - } - [Fact] public unsafe void Allocate_MemoryIsPinnableMultipleTimes() { diff --git a/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedMemoryPoolTests.Trim.cs b/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedMemoryPoolTests.Trim.cs new file mode 100644 index 0000000000..f748b7feb4 --- /dev/null +++ b/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedMemoryPoolTests.Trim.cs @@ -0,0 +1,169 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading; +using Microsoft.DotNet.RemoteExecutor; +using SixLabors.ImageSharp.Memory.Internals; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Memory.Allocators +{ + public partial class UniformUnmanagedMemoryPoolTests + { + [Collection(nameof(NonParallelTests))] + public class Trim + { + [CollectionDefinition(nameof(NonParallelTests), DisableParallelization = true)] + public class NonParallelTests + { + } + + [Fact] + public void TrimPeriodElapsed_TrimsHalfOfUnusedArrays() + { + RemoteExecutor.Invoke(RunTest).Dispose(); + static void RunTest() + { + var trimSettings = new UniformUnmanagedMemoryPool.TrimSettings { TrimPeriodMilliseconds = 5_000 }; + var pool = new UniformUnmanagedMemoryPool(128, 256, trimSettings); + + UnmanagedMemoryHandle[] a = pool.Rent(64); + UnmanagedMemoryHandle[] b = pool.Rent(64); + pool.Return(a); + Assert.Equal(128, UnmanagedMemoryHandle.TotalOutstandingHandles); + Thread.Sleep(15_000); + + // We expect at least 2 Trim actions, first trim 32, then 16 arrays. + // 128 - 32 - 16 = 80 + Assert.True( + UnmanagedMemoryHandle.TotalOutstandingHandles <= 80, + $"UnmanagedMemoryHandle.TotalOutstandingHandles={UnmanagedMemoryHandle.TotalOutstandingHandles} > 80"); + pool.Return(b); + } + } + + // Complicated Xunit ceremony to disable parallel execution of an individual test, + // MultiplePoolInstances_TrimPeriodElapsed_AllAreTrimmed, + // which is strongly timing-sensitive, and might be flaky under high load. + [CollectionDefinition(nameof(NonParallelCollection), DisableParallelization = true)] + public class NonParallelCollection + { + } + + [Collection(nameof(NonParallelCollection))] + public class NonParallel + { + public static readonly bool IsNotMacOs = !TestEnvironment.IsOSX; + + // TODO: Investigate failures on MacOS. All handles are released after GC. + // (It seems to happen more consistently on .NET 6.) + [ConditionalFact(nameof(IsNotMacOs))] + public void MultiplePoolInstances_TrimPeriodElapsed_AllAreTrimmed() + { + if (!TestEnvironment.RunsOnCI) + { + // This may fail in local runs resulting in high memory load. + // Remove the condition for local debugging! + return; + } + + RemoteExecutor.Invoke(RunTest).Dispose(); + + static void RunTest() + { + var trimSettings1 = new UniformUnmanagedMemoryPool.TrimSettings { TrimPeriodMilliseconds = 6_000 }; + var pool1 = new UniformUnmanagedMemoryPool(128, 256, trimSettings1); + Thread.Sleep(8_000); // Let some callbacks fire already + var trimSettings2 = new UniformUnmanagedMemoryPool.TrimSettings { TrimPeriodMilliseconds = 3_000 }; + var pool2 = new UniformUnmanagedMemoryPool(128, 256, trimSettings2); + + pool1.Return(pool1.Rent(64)); + pool2.Return(pool2.Rent(64)); + Assert.Equal(128, UnmanagedMemoryHandle.TotalOutstandingHandles); + + // This exercises pool weak reference list trimming: + LeakPoolInstance(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + Assert.Equal(128, UnmanagedMemoryHandle.TotalOutstandingHandles); + + Thread.Sleep(15_000); + Assert.True( + UnmanagedMemoryHandle.TotalOutstandingHandles <= 64, + $"UnmanagedMemoryHandle.TotalOutstandingHandles={UnmanagedMemoryHandle.TotalOutstandingHandles} > 64"); + GC.KeepAlive(pool1); + GC.KeepAlive(pool2); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void LeakPoolInstance() + { + var trimSettings = new UniformUnmanagedMemoryPool.TrimSettings { TrimPeriodMilliseconds = 4_000 }; + _ = new UniformUnmanagedMemoryPool(128, 256, trimSettings); + } + } + } + +#if NETCOREAPP3_1_OR_GREATER + public static readonly bool Is32BitProcess = !Environment.Is64BitProcess; + private static readonly List PressureArrays = new(); + + [ConditionalFact(nameof(Is32BitProcess))] + public static void GC_Collect_OnHighLoad_TrimsEntirePool() + { + RemoteExecutor.Invoke(RunTest).Dispose(); + + static void RunTest() + { + Assert.False(Environment.Is64BitProcess); + const int OneMb = 1 << 20; + + var trimSettings = new UniformUnmanagedMemoryPool.TrimSettings { HighPressureThresholdRate = 0.2f }; + + GCMemoryInfo memInfo = GC.GetGCMemoryInfo(); + int highLoadThreshold = (int)(memInfo.HighMemoryLoadThresholdBytes / OneMb); + highLoadThreshold = (int)(trimSettings.HighPressureThresholdRate * highLoadThreshold); + + var pool = new UniformUnmanagedMemoryPool(OneMb, 16, trimSettings); + pool.Return(pool.Rent(16)); + Assert.Equal(16, UnmanagedMemoryHandle.TotalOutstandingHandles); + + for (int i = 0; i < highLoadThreshold; i++) + { + byte[] array = new byte[OneMb]; + TouchPage(array); + PressureArrays.Add(array); + } + + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + GC.WaitForPendingFinalizers(); // The pool should be fully trimmed after this point + + Assert.Equal(0, UnmanagedMemoryHandle.TotalOutstandingHandles); + + // Prevent eager collection of the pool: + GC.KeepAlive(pool); + + static void TouchPage(byte[] b) + { + uint size = (uint)b.Length; + const uint pageSize = 4096; + uint numPages = size / pageSize; + + for (uint i = 0; i < numPages; i++) + { + b[i * pageSize] = (byte)(i % 256); + } + } + } + } +#endif + } + } +} diff --git a/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedMemoryPoolTests.cs b/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedMemoryPoolTests.cs new file mode 100644 index 0000000000..00acce64eb --- /dev/null +++ b/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedMemoryPoolTests.cs @@ -0,0 +1,379 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.DotNet.RemoteExecutor; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Memory.Internals; +using Xunit; +using Xunit.Abstractions; + +namespace SixLabors.ImageSharp.Tests.Memory.Allocators +{ + public partial class UniformUnmanagedMemoryPoolTests + { + private readonly ITestOutputHelper output; + + public UniformUnmanagedMemoryPoolTests(ITestOutputHelper output) => this.output = output; + + private class CleanupUtil : IDisposable + { + private readonly UniformUnmanagedMemoryPool pool; + private readonly List handlesToDestroy = new(); + private readonly List ptrsToDestroy = new(); + + public CleanupUtil(UniformUnmanagedMemoryPool pool) + { + this.pool = pool; + } + + public void Register(UnmanagedMemoryHandle handle) => this.handlesToDestroy.Add(handle); + + public void Register(IEnumerable handles) => this.handlesToDestroy.AddRange(handles); + + public void Register(IntPtr memoryPtr) => this.ptrsToDestroy.Add(memoryPtr); + + public void Register(IEnumerable memoryPtrs) => this.ptrsToDestroy.AddRange(memoryPtrs); + + public void Dispose() + { + foreach (UnmanagedMemoryHandle handle in this.handlesToDestroy) + { + handle.Free(); + } + + this.pool.Release(); + + foreach (IntPtr ptr in this.ptrsToDestroy) + { + Marshal.FreeHGlobal(ptr); + } + } + } + + [Theory] + [InlineData(3, 11)] + [InlineData(7, 4)] + public void Constructor_InitializesProperties(int arrayLength, int capacity) + { + var pool = new UniformUnmanagedMemoryPool(arrayLength, capacity); + Assert.Equal(arrayLength, pool.BufferLength); + Assert.Equal(capacity, pool.Capacity); + } + + [Theory] + [InlineData(1, 3)] + [InlineData(8, 10)] + public void Rent_SingleBuffer_ReturnsCorrectBuffer(int length, int capacity) + { + var pool = new UniformUnmanagedMemoryPool(length, capacity); + using var cleanup = new CleanupUtil(pool); + + for (int i = 0; i < capacity; i++) + { + UnmanagedMemoryHandle h = pool.Rent(); + CheckBuffer(length, pool, h); + cleanup.Register(h); + } + } + + [Fact] + public void Return_DoesNotDeallocateMemory() + { + RemoteExecutor.Invoke(RunTest).Dispose(); + + static void RunTest() + { + var pool = new UniformUnmanagedMemoryPool(16, 16); + UnmanagedMemoryHandle a = pool.Rent(); + UnmanagedMemoryHandle[] b = pool.Rent(2); + + Assert.Equal(3, UnmanagedMemoryHandle.TotalOutstandingHandles); + pool.Return(a); + pool.Return(b); + Assert.Equal(3, UnmanagedMemoryHandle.TotalOutstandingHandles); + } + } + + private static void CheckBuffer(int length, UniformUnmanagedMemoryPool pool, UnmanagedMemoryHandle h) + { + Assert.False(h.IsInvalid); + Span span = GetSpan(h, pool.BufferLength); + span.Fill(123); + + byte[] expected = new byte[length]; + expected.AsSpan().Fill(123); + Assert.True(span.SequenceEqual(expected)); + } + + private static unsafe Span GetSpan(UnmanagedMemoryHandle h, int length) => new Span(h.Pointer, length); + + [Theory] + [InlineData(1, 1)] + [InlineData(1, 5)] + [InlineData(42, 7)] + [InlineData(5, 10)] + public void Rent_MultiBuffer_ReturnsCorrectBuffers(int length, int bufferCount) + { + var pool = new UniformUnmanagedMemoryPool(length, 10); + using var cleanup = new CleanupUtil(pool); + UnmanagedMemoryHandle[] handles = pool.Rent(bufferCount); + cleanup.Register(handles); + + Assert.NotNull(handles); + Assert.Equal(bufferCount, handles.Length); + + foreach (UnmanagedMemoryHandle h in handles) + { + CheckBuffer(length, pool, h); + } + } + + [Fact] + public void Rent_MultipleTimesWithoutReturn_ReturnsDifferentHandles() + { + var pool = new UniformUnmanagedMemoryPool(128, 10); + using var cleanup = new CleanupUtil(pool); + UnmanagedMemoryHandle[] a = pool.Rent(2); + cleanup.Register(a); + UnmanagedMemoryHandle b = pool.Rent(); + cleanup.Register(b); + + Assert.NotEqual(a[0].Handle, a[1].Handle); + Assert.NotEqual(a[0].Handle, b.Handle); + Assert.NotEqual(a[1].Handle, b.Handle); + } + + [Theory] + [InlineData(4, 2, 10)] + [InlineData(5, 1, 6)] + [InlineData(12, 4, 12)] + public void RentReturnRent_SameBuffers(int totalCount, int rentUnit, int capacity) + { + var pool = new UniformUnmanagedMemoryPool(128, capacity); + using var cleanup = new CleanupUtil(pool); + var allHandles = new HashSet(); + var handleUnits = new List(); + + UnmanagedMemoryHandle[] handles; + for (int i = 0; i < totalCount; i += rentUnit) + { + handles = pool.Rent(rentUnit); + Assert.NotNull(handles); + handleUnits.Add(handles); + foreach (UnmanagedMemoryHandle array in handles) + { + allHandles.Add(array); + } + + // Allocate some memory, so potential new pool allocation wouldn't allocated the same memory: + cleanup.Register(Marshal.AllocHGlobal(128)); + } + + foreach (UnmanagedMemoryHandle[] arrayUnit in handleUnits) + { + if (arrayUnit.Length == 1) + { + // Test single-array return: + pool.Return(arrayUnit.Single()); + } + else + { + pool.Return(arrayUnit); + } + } + + handles = pool.Rent(totalCount); + + Assert.NotNull(handles); + + foreach (UnmanagedMemoryHandle array in handles) + { + Assert.Contains(array, allHandles); + } + + cleanup.Register(allHandles); + } + + [Fact] + public void Rent_SingleBuffer_OverCapacity_ReturnsInvalidBuffer() + { + var pool = new UniformUnmanagedMemoryPool(7, 1000); + using var cleanup = new CleanupUtil(pool); + UnmanagedMemoryHandle[] initial = pool.Rent(1000); + Assert.NotNull(initial); + cleanup.Register(initial); + UnmanagedMemoryHandle b1 = pool.Rent(); + Assert.True(b1.IsInvalid); + } + + [Theory] + [InlineData(0, 6, 5)] + [InlineData(5, 1, 5)] + [InlineData(4, 7, 10)] + public void Rent_MultiBuffer_OverCapacity_ReturnsNull(int initialRent, int attempt, int capacity) + { + var pool = new UniformUnmanagedMemoryPool(128, capacity); + using var cleanup = new CleanupUtil(pool); + UnmanagedMemoryHandle[] initial = pool.Rent(initialRent); + Assert.NotNull(initial); + cleanup.Register(initial); + UnmanagedMemoryHandle[] b1 = pool.Rent(attempt); + Assert.Null(b1); + } + + [Theory] + [InlineData(0, 5, 5)] + [InlineData(5, 1, 6)] + [InlineData(4, 7, 11)] + [InlineData(3, 3, 7)] + public void Rent_MultiBuff_BelowCapacity_Succeeds(int initialRent, int attempt, int capacity) + { + var pool = new UniformUnmanagedMemoryPool(128, capacity); + using var cleanup = new CleanupUtil(pool); + UnmanagedMemoryHandle[] b0 = pool.Rent(initialRent); + Assert.NotNull(b0); + cleanup.Register(b0); + UnmanagedMemoryHandle[] b1 = pool.Rent(attempt); + Assert.NotNull(b1); + cleanup.Register(b1); + } + + public static readonly bool IsNotMacOS = !TestEnvironment.IsOSX; + + // TODO: Investigate MacOS failures + [ConditionalTheory(nameof(IsNotMacOS))] + [InlineData(false)] + [InlineData(true)] + public void RentReturnRelease_SubsequentRentReturnsDifferentHandles(bool multiple) + { + var pool = new UniformUnmanagedMemoryPool(16, 16); + using var cleanup = new CleanupUtil(pool); + UnmanagedMemoryHandle b0 = pool.Rent(); + IntPtr h0 = b0.Handle; + UnmanagedMemoryHandle b1 = pool.Rent(); + IntPtr h1 = b1.Handle; + pool.Return(b0); + pool.Return(b1); + pool.Release(); + + // Do some unmanaged allocations to make sure new pool buffers are different: + IntPtr[] dummy = Enumerable.Range(0, 100).Select(_ => Marshal.AllocHGlobal(16)).ToArray(); + cleanup.Register(dummy); + + if (multiple) + { + UnmanagedMemoryHandle b = pool.Rent(); + cleanup.Register(b); + Assert.NotEqual(h0, b.Handle); + Assert.NotEqual(h1, b.Handle); + } + else + { + UnmanagedMemoryHandle[] b = pool.Rent(2); + cleanup.Register(b); + Assert.NotEqual(h0, b[0].Handle); + Assert.NotEqual(h1, b[0].Handle); + Assert.NotEqual(h0, b[1].Handle); + Assert.NotEqual(h1, b[1].Handle); + } + } + + [Fact] + public void Release_ShouldFreeRetainedMemory() + { + RemoteExecutor.Invoke(RunTest).Dispose(); + + static void RunTest() + { + var pool = new UniformUnmanagedMemoryPool(16, 16); + UnmanagedMemoryHandle a = pool.Rent(); + UnmanagedMemoryHandle[] b = pool.Rent(2); + pool.Return(a); + pool.Return(b); + + Assert.Equal(3, UnmanagedMemoryHandle.TotalOutstandingHandles); + pool.Release(); + Assert.Equal(0, UnmanagedMemoryHandle.TotalOutstandingHandles); + } + } + + [Fact] + public void RentReturn_IsThreadSafe() + { + int count = Environment.ProcessorCount * 200; + var pool = new UniformUnmanagedMemoryPool(8, count); + using var cleanup = new CleanupUtil(pool); + var rnd = new Random(0); + + Parallel.For(0, Environment.ProcessorCount, (int i) => + { + var allHandles = new List(); + int pauseAt = rnd.Next(100); + for (int j = 0; j < 100; j++) + { + UnmanagedMemoryHandle[] data = pool.Rent(2); + + GetSpan(data[0], pool.BufferLength).Fill((byte)i); + GetSpan(data[1], pool.BufferLength).Fill((byte)i); + allHandles.Add(data[0]); + allHandles.Add(data[1]); + + if (j == pauseAt) + { + Thread.Sleep(15); + } + } + + Span expected = new byte[8]; + expected.Fill((byte)i); + + foreach (UnmanagedMemoryHandle h in allHandles) + { + Assert.True(expected.SequenceEqual(GetSpan(h, pool.BufferLength))); + pool.Return(new[] { h }); + } + }); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void LeakPool_FinalizerShouldFreeRetainedHandles(bool withGuardedBuffers) + { + RemoteExecutor.Invoke(RunTest, withGuardedBuffers.ToString()).Dispose(); + + static void RunTest(string withGuardedBuffersInner) + { + LeakPoolInstance(bool.Parse(withGuardedBuffersInner)); + Assert.Equal(20, UnmanagedMemoryHandle.TotalOutstandingHandles); + GC.Collect(); + GC.WaitForPendingFinalizers(); + Assert.Equal(0, UnmanagedMemoryHandle.TotalOutstandingHandles); + } + + [MethodImpl(MethodImplOptions.NoInlining)] + static void LeakPoolInstance(bool withGuardedBuffers) + { + var pool = new UniformUnmanagedMemoryPool(16, 128); + if (withGuardedBuffers) + { + UnmanagedMemoryHandle h = pool.Rent(); + _ = pool.CreateGuardedBuffer(h, 10, false); + UnmanagedMemoryHandle[] g = pool.Rent(19); + _ = pool.CreateGroupLifetimeGuard(g); + } + else + { + pool.Return(pool.Rent(20)); + } + } + } + } +} diff --git a/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs b/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs new file mode 100644 index 0000000000..80c8cc6a06 --- /dev/null +++ b/tests/ImageSharp.Tests/Memory/Allocators/UniformUnmanagedPoolMemoryAllocatorTests.cs @@ -0,0 +1,371 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Microsoft.DotNet.RemoteExecutor; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Memory.Internals; +using SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Memory.Allocators +{ + public class UniformUnmanagedPoolMemoryAllocatorTests + { + public class BufferTests1 : BufferTestSuite + { + private static MemoryAllocator CreateMemoryAllocator() => + new UniformUnmanagedMemoryPoolMemoryAllocator( + sharedArrayPoolThresholdInBytes: 1024, + poolBufferSizeInBytes: 2048, + maxPoolSizeInBytes: 2048 * 4, + unmanagedBufferSizeInBytes: 4096); + + public BufferTests1() + : base(CreateMemoryAllocator()) + { + } + } + + public class BufferTests2 : BufferTestSuite + { + private static MemoryAllocator CreateMemoryAllocator() => + new UniformUnmanagedMemoryPoolMemoryAllocator( + sharedArrayPoolThresholdInBytes: 512, + poolBufferSizeInBytes: 1024, + maxPoolSizeInBytes: 1024 * 4, + unmanagedBufferSizeInBytes: 2048); + + public BufferTests2() + : base(CreateMemoryAllocator()) + { + } + } + + public static TheoryData AllocateData = + new TheoryData() + { + { default(S4), 16, 256, 256, 1024, 64, 64, 1, -1, 64 }, + { default(S4), 16, 256, 256, 1024, 256, 256, 1, -1, 256 }, + { default(S4), 16, 256, 256, 1024, 250, 256, 1, -1, 250 }, + { default(S4), 16, 256, 256, 1024, 248, 250, 1, -1, 248 }, + { default(S4), 16, 1024, 2048, 4096, 512, 256, 2, 256, 256 }, + { default(S4), 16, 1024, 1024, 1024, 511, 256, 2, 256, 255 }, + }; + + [Theory] + [MemberData(nameof(AllocateData))] + public void AllocateGroup_BufferSizesAreCorrect( + T dummy, + int sharedArrayPoolThresholdInBytes, + int maxContiguousPoolBufferInBytes, + int maxPoolSizeInBytes, + int maxCapacityOfUnmanagedBuffers, + long allocationLengthInElements, + int bufferAlignmentInElements, + int expectedNumberOfBuffers, + int expectedBufferSize, + int expectedSizeOfLastBuffer) + where T : struct + { + var allocator = new UniformUnmanagedMemoryPoolMemoryAllocator( + sharedArrayPoolThresholdInBytes, + maxContiguousPoolBufferInBytes, + maxPoolSizeInBytes, + maxCapacityOfUnmanagedBuffers); + + using MemoryGroup g = allocator.AllocateGroup(allocationLengthInElements, bufferAlignmentInElements); + MemoryGroupTests.Allocate.ValidateAllocateMemoryGroup( + expectedNumberOfBuffers, + expectedBufferSize, + expectedSizeOfLastBuffer, + g); + } + + [Fact] + public void AllocateGroup_MultipleTimes_ExceedPoolLimit() + { + var allocator = new UniformUnmanagedMemoryPoolMemoryAllocator( + 64, + 128, + 1024, + 1024); + + var groups = new List>(); + for (int i = 0; i < 16; i++) + { + int lengthInElements = 128 / Unsafe.SizeOf(); + MemoryGroup g = allocator.AllocateGroup(lengthInElements, 32); + MemoryGroupTests.Allocate.ValidateAllocateMemoryGroup(1, -1, lengthInElements, g); + groups.Add(g); + } + + foreach (MemoryGroup g in groups) + { + g.Dispose(); + } + } + + [Fact] + public unsafe void Allocate_MemoryIsPinnableMultipleTimes() + { + var allocator = new UniformUnmanagedMemoryPoolMemoryAllocator(null); + using IMemoryOwner memoryOwner = allocator.Allocate(100); + + using (MemoryHandle pin = memoryOwner.Memory.Pin()) + { + Assert.NotEqual(IntPtr.Zero, (IntPtr)pin.Pointer); + } + + using (MemoryHandle pin = memoryOwner.Memory.Pin()) + { + Assert.NotEqual(IntPtr.Zero, (IntPtr)pin.Pointer); + } + } + + [Fact] + public void MemoryAllocator_Create_WithoutSettings_AllocatesDiscontiguousMemory() + { + RemoteExecutor.Invoke(RunTest).Dispose(); + + static void RunTest() + { + var allocator = MemoryAllocator.Create(); + long sixteenMegabytes = 16 * (1 << 20); + + // Should allocate 4 times 4MB discontiguos blocks: + MemoryGroup g = allocator.AllocateGroup(sixteenMegabytes, 1024); + Assert.Equal(4, g.Count); + } + } + + [Fact] + public void MemoryAllocator_Create_LimitPoolSize() + { + RemoteExecutor.Invoke(RunTest).Dispose(); + + static void RunTest() + { + var allocator = MemoryAllocator.Create(new MemoryAllocatorOptions() + { + MaximumPoolSizeMegabytes = 8 + }); + + MemoryGroup g0 = allocator.AllocateGroup(B(8), 1024); + MemoryGroup g1 = allocator.AllocateGroup(B(8), 1024); + ref byte r0 = ref MemoryMarshal.GetReference(g0[0].Span); + ref byte r1 = ref MemoryMarshal.GetReference(g1[0].Span); + g0.Dispose(); + g1.Dispose(); + + // Do some unmanaged allocations to make sure new non-pooled unmanaged allocations will grab different memory: + IntPtr dummy1 = Marshal.AllocHGlobal((IntPtr)B(8)); + IntPtr dummy2 = Marshal.AllocHGlobal((IntPtr)B(8)); + + using MemoryGroup g2 = allocator.AllocateGroup(B(8), 1024); + using MemoryGroup g3 = allocator.AllocateGroup(B(8), 1024); + ref byte r2 = ref MemoryMarshal.GetReference(g2[0].Span); + ref byte r3 = ref MemoryMarshal.GetReference(g3[0].Span); + + Assert.True(Unsafe.AreSame(ref r0, ref r2)); + Assert.False(Unsafe.AreSame(ref r1, ref r3)); + + Marshal.FreeHGlobal(dummy1); + Marshal.FreeHGlobal(dummy2); + } + + static long B(int value) => value << 20; + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void BufferDisposal_ReturnsToPool(bool shared) + { + RemoteExecutor.Invoke(RunTest, shared.ToString()).Dispose(); + + static void RunTest(string sharedStr) + { + var allocator = new UniformUnmanagedMemoryPoolMemoryAllocator(512, 1024, 16 * 1024, 1024); + IMemoryOwner buffer0 = allocator.Allocate(bool.Parse(sharedStr) ? 300 : 600); + buffer0.GetSpan()[0] = 42; + buffer0.Dispose(); + using IMemoryOwner buffer1 = allocator.Allocate(bool.Parse(sharedStr) ? 300 : 600); + Assert.Equal(42, buffer1.GetSpan()[0]); + } + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void MemoryGroupDisposal_ReturnsToPool(bool shared) + { + RemoteExecutor.Invoke(RunTest, shared.ToString()).Dispose(); + + static void RunTest(string sharedStr) + { + var allocator = new UniformUnmanagedMemoryPoolMemoryAllocator(512, 1024, 16 * 1024, 1024); + MemoryGroup g0 = allocator.AllocateGroup(bool.Parse(sharedStr) ? 300 : 600, 100); + g0.Single().Span[0] = 42; + g0.Dispose(); + using MemoryGroup g1 = allocator.AllocateGroup(bool.Parse(sharedStr) ? 300 : 600, 100); + Assert.Equal(42, g1.Single().Span[0]); + } + } + + [Fact] + public void ReleaseRetainedResources_ShouldFreePooledMemory() + { + RemoteExecutor.Invoke(RunTest).Dispose(); + static void RunTest() + { + var allocator = new UniformUnmanagedMemoryPoolMemoryAllocator(128, 512, 16 * 512, 1024); + MemoryGroup g = allocator.AllocateGroup(2048, 128); + g.Dispose(); + Assert.Equal(4, UnmanagedMemoryHandle.TotalOutstandingHandles); + allocator.ReleaseRetainedResources(); + Assert.Equal(0, UnmanagedMemoryHandle.TotalOutstandingHandles); + } + } + + [Fact] + public void ReleaseRetainedResources_DoesNotFreeOutstandingBuffers() + { + RemoteExecutor.Invoke(RunTest).Dispose(); + static void RunTest() + { + var allocator = new UniformUnmanagedMemoryPoolMemoryAllocator(128, 512, 16 * 512, 1024); + IMemoryOwner b = allocator.Allocate(256); + MemoryGroup g = allocator.AllocateGroup(2048, 128); + Assert.Equal(5, UnmanagedMemoryHandle.TotalOutstandingHandles); + allocator.ReleaseRetainedResources(); + Assert.Equal(5, UnmanagedMemoryHandle.TotalOutstandingHandles); + b.Dispose(); + g.Dispose(); + Assert.Equal(5, UnmanagedMemoryHandle.TotalOutstandingHandles); + allocator.ReleaseRetainedResources(); + Assert.Equal(0, UnmanagedMemoryHandle.TotalOutstandingHandles); + } + } + + [Theory] + [InlineData(300)] // Group of single SharedArrayPoolBuffer + [InlineData(600)] // Group of single UniformUnmanagedMemoryPool buffer + [InlineData(1200)] // Group of two UniformUnmanagedMemoryPool buffers + public void AllocateMemoryGroup_Finalization_ReturnsToPool(int length) + { + if (!TestEnvironment.RunsOnCI) + { + // This may fail in local runs resulting in high memory load. + // Remove the condition for local debugging! + return; + } + + // RunTest(length.ToString()); + RemoteExecutor.Invoke(RunTest, length.ToString()).Dispose(); + + static void RunTest(string lengthStr) + { + var allocator = new UniformUnmanagedMemoryPoolMemoryAllocator(512, 1024, 16 * 1024, 1024); + int lengthInner = int.Parse(lengthStr); + + AllocateGroupAndForget(allocator, lengthInner); + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + + AllocateGroupAndForget(allocator, lengthInner, true); + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + + using MemoryGroup g = allocator.AllocateGroup(lengthInner, 100); + Assert.Equal(42, g.First().Span[0]); + } + } + + private static void AllocateGroupAndForget(UniformUnmanagedMemoryPoolMemoryAllocator allocator, int length, bool check = false) + { + MemoryGroup g = allocator.AllocateGroup(length, 100); + if (check) + { + Assert.Equal(42, g.First().Span[0]); + } + + g.First().Span[0] = 42; + + if (length < 512) + { + // For ArrayPool.Shared, first array will be returned to the TLS storage of the finalizer thread, + // repeat rental to make sure per-core buckets are also utilized. + MemoryGroup g1 = allocator.AllocateGroup(length, 100); + g1.First().Span[0] = 42; + } + } + + [Theory] + [InlineData(300)] // Group of single SharedArrayPoolBuffer + [InlineData(600)] // Group of single UniformUnmanagedMemoryPool buffer + public void AllocateSingleMemoryOwner_Finalization_ReturnsToPool(int length) + { + if (!TestEnvironment.RunsOnCI) + { + // This may fail in local runs resulting in high memory load. + // Remove the condition for local debugging! + return; + } + + // RunTest(length.ToString()); + RemoteExecutor.Invoke(RunTest, length.ToString()).Dispose(); + + static void RunTest(string lengthStr) + { + var allocator = new UniformUnmanagedMemoryPoolMemoryAllocator(512, 1024, 16 * 1024, 1024); + int lengthInner = int.Parse(lengthStr); + + AllocateSingleAndForget(allocator, lengthInner); + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + + AllocateSingleAndForget(allocator, lengthInner, true); + GC.Collect(); + GC.WaitForPendingFinalizers(); + GC.Collect(); + GC.WaitForPendingFinalizers(); + + using IMemoryOwner g = allocator.Allocate(lengthInner); + Assert.Equal(42, g.GetSpan()[0]); + GC.KeepAlive(allocator); + } + } + + [MethodImpl(MethodImplOptions.NoInlining)] + private static void AllocateSingleAndForget(UniformUnmanagedMemoryPoolMemoryAllocator allocator, int length, bool check = false) + { + IMemoryOwner g = allocator.Allocate(length); + if (check) + { + Assert.Equal(42, g.GetSpan()[0]); + } + + g.GetSpan()[0] = 42; + + if (length < 512) + { + // For ArrayPool.Shared, first array will be returned to the TLS storage of the finalizer thread, + // repeat rental to make sure per-core buckets are also utilized. + IMemoryOwner g1 = allocator.Allocate(length); + g1.GetSpan()[0] = 42; + } + } + } +} diff --git a/tests/ImageSharp.Tests/Memory/Allocators/UnmanagedBufferTests.cs b/tests/ImageSharp.Tests/Memory/Allocators/UnmanagedBufferTests.cs new file mode 100644 index 0000000000..68251be861 --- /dev/null +++ b/tests/ImageSharp.Tests/Memory/Allocators/UnmanagedBufferTests.cs @@ -0,0 +1,93 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Buffers; +using System.Collections.Generic; +using Microsoft.DotNet.RemoteExecutor; +using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Memory.Internals; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Memory.Allocators +{ + public class UnmanagedBufferTests + { + public class AllocatorBufferTests : BufferTestSuite + { + public AllocatorBufferTests() + : base(new UnmanagedMemoryAllocator(1024 * 64)) + { + } + } + + [Fact] + public void Allocate_CreatesValidBuffer() + { + using var buffer = UnmanagedBuffer.Allocate(10); + Span span = buffer.GetSpan(); + Assert.Equal(10, span.Length); + span[9] = 123; + Assert.Equal(123, span[9]); + } + + [Fact] + public unsafe void Dispose_DoesNotReleaseOutstandingReferences() + { + RemoteExecutor.Invoke(RunTest).Dispose(); + + static void RunTest() + { + var buffer = UnmanagedBuffer.Allocate(10); + Assert.Equal(1, UnmanagedMemoryHandle.TotalOutstandingHandles); + Span span = buffer.GetSpan(); + + // Pin should AddRef + using (MemoryHandle h = buffer.Pin()) + { + int* ptr = (int*)h.Pointer; + ((IDisposable)buffer).Dispose(); + Assert.Equal(1, UnmanagedMemoryHandle.TotalOutstandingHandles); + ptr[3] = 13; + Assert.Equal(13, span[3]); + } // Unpin should ReleaseRef + + Assert.Equal(0, UnmanagedMemoryHandle.TotalOutstandingHandles); + } + } + + [Theory] + [InlineData(2)] + [InlineData(12)] + public void BufferFinalization_TracksAllocations(int count) + { + RemoteExecutor.Invoke(RunTest, count.ToString()).Dispose(); + + static void RunTest(string countStr) + { + int countInner = int.Parse(countStr); + List> l = FillList(countInner); + + l.RemoveRange(0, l.Count / 2); + + GC.Collect(); + GC.WaitForPendingFinalizers(); + + Assert.Equal(countInner / 2, l.Count); // This is here to prevent eager finalization of the list's elements + Assert.Equal(countInner / 2, UnmanagedMemoryHandle.TotalOutstandingHandles); + } + + static List> FillList(int countInner) + { + var l = new List>(); + for (int i = 0; i < countInner; i++) + { + var h = UnmanagedBuffer.Allocate(42); + l.Add(h); + } + + return l; + } + } + } +} diff --git a/tests/ImageSharp.Tests/Memory/Allocators/UnmanagedMemoryHandleTests.cs b/tests/ImageSharp.Tests/Memory/Allocators/UnmanagedMemoryHandleTests.cs new file mode 100644 index 0000000000..a3f827355f --- /dev/null +++ b/tests/ImageSharp.Tests/Memory/Allocators/UnmanagedMemoryHandleTests.cs @@ -0,0 +1,100 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +using System; +using System.Collections.Generic; +using Microsoft.DotNet.RemoteExecutor; +using SixLabors.ImageSharp.Memory.Internals; +using Xunit; + +namespace SixLabors.ImageSharp.Tests.Memory.Allocators +{ + public class UnmanagedMemoryHandleTests + { + [Fact] + public unsafe void Allocate_AllocatesReadWriteMemory() + { + var h = UnmanagedMemoryHandle.Allocate(128); + Assert.False(h.IsInvalid); + Assert.True(h.IsValid); + byte* ptr = (byte*)h.Handle; + for (int i = 0; i < 128; i++) + { + ptr[i] = (byte)i; + } + + for (int i = 0; i < 128; i++) + { + Assert.Equal((byte)i, ptr[i]); + } + + h.Free(); + } + + [Fact] + public void Free_ClosesHandle() + { + var h = UnmanagedMemoryHandle.Allocate(128); + h.Free(); + Assert.True(h.IsInvalid); + Assert.Equal(IntPtr.Zero, h.Handle); + } + + [Theory] + [InlineData(1)] + [InlineData(13)] + public void Create_Free_AllocationsAreTracked(int count) + { + RemoteExecutor.Invoke(RunTest, count.ToString()).Dispose(); + + static void RunTest(string countStr) + { + int countInner = int.Parse(countStr); + var l = new List(); + for (int i = 0; i < countInner; i++) + { + Assert.Equal(i, UnmanagedMemoryHandle.TotalOutstandingHandles); + var h = UnmanagedMemoryHandle.Allocate(42); + Assert.Equal(i + 1, UnmanagedMemoryHandle.TotalOutstandingHandles); + l.Add(h); + } + + for (int i = 0; i < countInner; i++) + { + Assert.Equal(countInner - i, UnmanagedMemoryHandle.TotalOutstandingHandles); + l[i].Free(); + Assert.Equal(countInner - i - 1, UnmanagedMemoryHandle.TotalOutstandingHandles); + } + } + } + + [Fact] + public void Equality_WhenTrue() + { + var h1 = UnmanagedMemoryHandle.Allocate(10); + UnmanagedMemoryHandle h2 = h1; + + Assert.True(h1.Equals(h2)); + Assert.True(h2.Equals(h1)); + Assert.True(h1 == h2); + Assert.False(h1 != h2); + Assert.True(h1.GetHashCode() == h2.GetHashCode()); + h1.Free(); + } + + [Fact] + public void Equality_WhenFalse() + { + var h1 = UnmanagedMemoryHandle.Allocate(10); + var h2 = UnmanagedMemoryHandle.Allocate(10); + + Assert.False(h1.Equals(h2)); + Assert.False(h2.Equals(h1)); + Assert.False(h1 == h2); + Assert.True(h1 != h2); + + h1.Free(); + h2.Free(); + } + } +} diff --git a/tests/ImageSharp.Tests/Memory/Buffer2DTests.cs b/tests/ImageSharp.Tests/Memory/Buffer2DTests.cs index 9092cbb08c..73a0f4d60e 100644 --- a/tests/ImageSharp.Tests/Memory/Buffer2DTests.cs +++ b/tests/ImageSharp.Tests/Memory/Buffer2DTests.cs @@ -68,6 +68,25 @@ public unsafe void Construct_Empty(int bufferCapacity, int width, int height) } } + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Construct_PreferContiguousImageBuffers_AllocatesContiguousRegardlessOfCapacity(bool useSizeOverload) + { + this.MemoryAllocator.BufferCapacityInBytes = 10_000; + + using Buffer2D buffer = useSizeOverload ? + this.MemoryAllocator.Allocate2D( + new Size(200, 200), + preferContiguosImageBuffers: true) : + this.MemoryAllocator.Allocate2D( + 200, + 200, + preferContiguosImageBuffers: true); + Assert.Equal(1, buffer.FastMemoryGroup.Count); + Assert.Equal(200 * 200, buffer.FastMemoryGroup.TotalLength); + } + [Theory] [InlineData(50, 10, 20, 4)] public void Allocate2DOveraligned(int bufferCapacity, int width, int height, int alignmentMultiplier) @@ -108,7 +127,7 @@ public unsafe void GetRowSpanY(int bufferCapacity, int width, int height, int y, using (Buffer2D buffer = this.MemoryAllocator.Allocate2D(width, height)) { - Span span = buffer.GetRowSpan(y); + Span span = buffer.DangerousGetRowSpan(y); Assert.Equal(width, span.Length); @@ -158,7 +177,7 @@ public void GetRowSpan_OutOfRange(int bufferCapacity, int width, int height, int this.MemoryAllocator.BufferCapacityInBytes = bufferCapacity; using Buffer2D buffer = this.MemoryAllocator.Allocate2D(width, height); - Exception ex = Assert.ThrowsAny(() => buffer.GetRowSpan(y)); + Exception ex = Assert.ThrowsAny(() => buffer.DangerousGetRowSpan(y)); Assert.True(ex is ArgumentOutOfRangeException || ex is IndexOutOfRangeException); } @@ -249,16 +268,14 @@ public void SwapOrCopyContent_WhenDestinationIsOwned_ShouldNotSwapInDisposedSour Buffer2D.SwapOrCopyContent(dest, source); } - int actual1 = dest.GetRowSpan(0)[0]; - int actual2 = dest.GetRowSpan(0)[0]; + int actual1 = dest.DangerousGetRowSpan(0)[0]; + int actual2 = dest.DangerousGetRowSpan(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); } @@ -280,7 +297,7 @@ public void CopyColumns(int width, int height, int startIndex, int destIndex, in for (int y = 0; y < b.Height; y++) { - Span row = b.GetRowSpan(y); + Span row = b.DangerousGetRowSpan(y); Span s = row.Slice(startIndex, columnCount); Span d = row.Slice(destIndex, columnCount); @@ -303,7 +320,7 @@ public void CopyColumns_InvokeMultipleTimes() for (int y = 0; y < b.Height; y++) { - Span row = b.GetRowSpan(y); + Span row = b.DangerousGetRowSpan(y); Span s = row.Slice(0, 22); Span d = row.Slice(50, 22); diff --git a/tests/ImageSharp.Tests/Memory/BufferAreaTests.cs b/tests/ImageSharp.Tests/Memory/BufferAreaTests.cs index 0dfc5f36b4..76e55aa3a1 100644 --- a/tests/ImageSharp.Tests/Memory/BufferAreaTests.cs +++ b/tests/ImageSharp.Tests/Memory/BufferAreaTests.cs @@ -127,7 +127,7 @@ public void Clear_FullArea(int bufferCapacity) for (int y = 0; y < 13; y++) { - Span row = buffer.GetRowSpan(y); + Span row = buffer.DangerousGetRowSpan(y); Assert.True(row.SequenceEqual(emptyRow)); } } @@ -151,7 +151,7 @@ public void Clear_SubArea(int bufferCapacity) for (int y = region.Rectangle.Y; y < region.Rectangle.Bottom; y++) { - Span span = buffer.GetRowSpan(y).Slice(region.Rectangle.X, region.Width); + Span span = buffer.DangerousGetRowSpan(y).Slice(region.Rectangle.X, region.Width); Assert.True(span.SequenceEqual(new int[region.Width])); } } diff --git a/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.Allocate.cs b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.Allocate.cs index e9094fcca4..adfafcb890 100644 --- a/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.Allocate.cs +++ b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.Allocate.cs @@ -2,10 +2,15 @@ // Licensed under the Apache License, Version 2.0. using System; +using System.Buffers; using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using SixLabors.ImageSharp.Memory; +using SixLabors.ImageSharp.Memory.Internals; using Xunit; +using Xunit.Abstractions; namespace SixLabors.ImageSharp.Tests.Memory.DiscontiguousBuffers { @@ -14,8 +19,7 @@ public partial class MemoryGroupTests public class Allocate : MemoryGroupTestsBase { #pragma warning disable SA1509 - public static TheoryData AllocateData = - new TheoryData() + public static TheoryData AllocateData = new() { { default(S5), 22, 4, 4, 1, 4, 4 }, { default(S5), 22, 4, 7, 2, 4, 3 }, @@ -39,6 +43,7 @@ public class Allocate : MemoryGroupTestsBase { 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(S4), 1024, 20, 800, 4, 240, 80 }, { default(short), 200, 50, 49, 1, 49, 49 }, { default(short), 200, 50, 1, 1, 1, 1 }, @@ -47,7 +52,7 @@ public class Allocate : MemoryGroupTestsBase [Theory] [MemberData(nameof(AllocateData))] - public void BufferSizesAreCorrect( + public void Allocate_FromMemoryAllocator_BufferSizesAreCorrect( T dummy, int bufferCapacity, int bufferAlignment, @@ -63,6 +68,96 @@ public void BufferSizesAreCorrect( using var g = MemoryGroup.Allocate(this.MemoryAllocator, totalLength, bufferAlignment); // Assert: + ValidateAllocateMemoryGroup(expectedNumberOfBuffers, expectedBufferSize, expectedSizeOfLastBuffer, g); + } + + [Theory] + [MemberData(nameof(AllocateData))] + public void Allocate_FromPool_BufferSizesAreCorrect( + T dummy, + int bufferCapacity, + int bufferAlignment, + long totalLength, + int expectedNumberOfBuffers, + int expectedBufferSize, + int expectedSizeOfLastBuffer) + where T : struct + { + if (totalLength == 0) + { + // Invalid case for UniformByteArrayPool allocations + return; + } + + var pool = new UniformUnmanagedMemoryPool(bufferCapacity, expectedNumberOfBuffers); + + // Act: + Assert.True(MemoryGroup.TryAllocate(pool, totalLength, bufferAlignment, AllocationOptions.None, out MemoryGroup g)); + + // Assert: + ValidateAllocateMemoryGroup(expectedNumberOfBuffers, expectedBufferSize, expectedSizeOfLastBuffer, g); + g.Dispose(); + } + + [Theory] + [InlineData(AllocationOptions.None)] + [InlineData(AllocationOptions.Clean)] + public unsafe void Allocate_FromPool_AllocationOptionsAreApplied(AllocationOptions options) + { + var pool = new UniformUnmanagedMemoryPool(10, 5); + UnmanagedMemoryHandle[] buffers = pool.Rent(5); + foreach (UnmanagedMemoryHandle b in buffers) + { + new Span(b.Pointer, pool.BufferLength).Fill(42); + } + + pool.Return(buffers); + + Assert.True(MemoryGroup.TryAllocate(pool, 50, 10, options, out MemoryGroup g)); + Span expected = stackalloc byte[10]; + expected.Fill((byte)(options == AllocationOptions.Clean ? 0 : 42)); + foreach (Memory memory in g) + { + Assert.True(expected.SequenceEqual(memory.Span)); + } + + g.Dispose(); + } + + [Theory] + [InlineData(64, 4, 60, 240, true)] + [InlineData(64, 4, 60, 244, false)] + public void Allocate_FromPool_AroundLimit( + int bufferCapacityBytes, + int poolCapacity, + int alignmentBytes, + int requestBytes, + bool shouldSucceed) + { + var pool = new UniformUnmanagedMemoryPool(bufferCapacityBytes, poolCapacity); + int alignmentElements = alignmentBytes / Unsafe.SizeOf(); + int requestElements = requestBytes / Unsafe.SizeOf(); + + Assert.Equal(shouldSucceed, MemoryGroup.TryAllocate(pool, requestElements, alignmentElements, AllocationOptions.None, out MemoryGroup g)); + if (shouldSucceed) + { + Assert.NotNull(g); + } + else + { + Assert.Null(g); + } + + g?.Dispose(); + } + + internal static void ValidateAllocateMemoryGroup( + int expectedNumberOfBuffers, + int expectedBufferSize, + int expectedSizeOfLastBuffer, + MemoryGroup g) + where T : struct + { Assert.Equal(expectedNumberOfBuffers, g.Count); if (expectedBufferSize >= 0) @@ -100,6 +195,7 @@ public void WhenBlockAlignmentIsOverCapacity_Throws_InvalidMemoryOperationExcept public void MemoryAllocatorIsUtilizedCorrectly(AllocationOptions allocationOptions) { this.MemoryAllocator.BufferCapacityInBytes = 200; + this.MemoryAllocator.EnableNonThreadSafeLogging(); HashSet bufferHashes; @@ -125,4 +221,16 @@ public void MemoryAllocatorIsUtilizedCorrectly(AllocationOptions allocationOptio } } } + + [StructLayout(LayoutKind.Sequential, Size = 5)] + internal struct S5 + { + public override string ToString() => "S5"; + } + + [StructLayout(LayoutKind.Sequential, Size = 4)] + internal struct S4 + { + public override string ToString() => "S4"; + } } diff --git a/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.cs b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.cs index 3ab5797ddb..13e47bdee2 100644 --- a/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.cs +++ b/tests/ImageSharp.Tests/Memory/DiscontiguousBuffers/MemoryGroupTests.cs @@ -119,6 +119,14 @@ public void Wrap() Assert.True(group[0].Span.SequenceEqual(data0)); Assert.True(group[1].Span.SequenceEqual(data1)); Assert.True(group[2].Span.SequenceEqual(data2)); + + int cnt = 0; + int[][] allData = { data0, data1, data2 }; + foreach (Memory memory in group) + { + Assert.True(memory.Span.SequenceEqual(allData[cnt])); + cnt++; + } } public static TheoryData GetBoundedSlice_SuccessData = new TheoryData() @@ -229,17 +237,5 @@ private static void MultiplyAllBy2(ReadOnlySpan source, Span target) 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/Processing/Processors/Dithering/DitherTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs index 0465cae940..7ae85c1974 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Dithering/DitherTests.cs @@ -61,7 +61,7 @@ public static readonly TheoryData DefaultInstanceDitherers /// but it is very different because of floating point inaccuracies. /// private static readonly bool SkipAllDitherTests = - !TestEnvironment.Is64BitProcess && string.IsNullOrEmpty(TestEnvironment.NetCoreVersion); + !TestEnvironment.Is64BitProcess && TestEnvironment.NetCoreVersion == null; [Theory] [WithFile(TestImages.Png.CalliphoraPartial, PixelTypes.Rgba32)] diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/AffineTransformTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/AffineTransformTests.cs index d2d2fcc1f7..3cd8cec154 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/AffineTransformTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/AffineTransformTests.cs @@ -239,9 +239,9 @@ private static IResampler GetResampler(string name) private static void VerifyAllPixelsAreWhiteOrTransparent(Image image) where TPixel : unmanaged, IPixel { - Assert.True(image.Frames.RootFrame.TryGetSinglePixelSpan(out Span data)); + Assert.True(image.Frames.RootFrame.DangerousTryGetSinglePixelMemory(out Memory data)); var white = new Rgb24(255, 255, 255); - foreach (TPixel pixel in data) + foreach (TPixel pixel in data.Span) { Rgba32 rgba = default; pixel.ToRgba32(ref rgba); diff --git a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs index 3c0faf4991..022bb224c4 100644 --- a/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs +++ b/tests/ImageSharp.Tests/Processing/Processors/Transforms/ResizeTests.cs @@ -117,6 +117,7 @@ public void WorkingBufferSizeHintInBytes_IsAppliedCorrectly( int workingBufferSizeHintInBytes = workingBufferLimitInRows * destSize.Width * SizeOfVector4; var allocator = new TestMemoryAllocator(); + allocator.EnableNonThreadSafeLogging(); configuration.MemoryAllocator = allocator; configuration.WorkingBufferSizeHintInBytes = workingBufferSizeHintInBytes; @@ -269,8 +270,8 @@ public void Resize_ThrowsForWrappedMemoryImage(TestImageProvider { using (Image image0 = provider.GetImage()) { - Assert.True(image0.TryGetSinglePixelSpan(out Span imageSpan)); - var mmg = TestMemoryManager.CreateAsCopyOf(imageSpan); + Assert.True(image0.DangerousTryGetSinglePixelMemory(out Memory imageMem)); + var mmg = TestMemoryManager.CreateAsCopyOf(imageMem.Span); using (var image1 = Image.WrapMemory(mmg.Memory, image0.Width, image0.Height)) { @@ -340,7 +341,7 @@ public void Resize_WorksWithAllResamplers( // Resize_WorksWithAllResamplers_TestPattern301x1180_NearestNeighbor-300x480.png // TODO: Should we investigate this? bool allowHigherInaccuracy = !TestEnvironment.Is64BitProcess - && string.IsNullOrEmpty(TestEnvironment.NetCoreVersion) + && TestEnvironment.NetCoreVersion == null && sampler is NearestNeighborResampler; var comparer = ImageComparer.TolerantPercentage(allowHigherInaccuracy ? 0.3f : 0.017f); diff --git a/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs b/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs index c9e5d3aa79..77b114fd2d 100644 --- a/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs +++ b/tests/ImageSharp.Tests/Quantization/QuantizedImageTests.cs @@ -73,7 +73,7 @@ public void OctreeQuantizerYieldsCorrectTransparentPixel( using (IndexedImageFrame quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds())) { int index = this.GetTransparentIndex(quantized); - Assert.Equal(index, quantized.GetPixelRowSpan(0)[0]); + Assert.Equal(index, quantized.DangerousGetRowSpan(0)[0]); } } } @@ -103,7 +103,7 @@ public void WuQuantizerYieldsCorrectTransparentPixel(TestImageProvider quantized = frameQuantizer.BuildPaletteAndQuantizeFrame(frame, frame.Bounds())) { int index = this.GetTransparentIndex(quantized); - Assert.Equal(index, quantized.GetPixelRowSpan(0)[0]); + Assert.Equal(index, quantized.DangerousGetRowSpan(0)[0]); } } } diff --git a/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs b/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs index 71a8702c70..b835aa63e2 100644 --- a/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs +++ b/tests/ImageSharp.Tests/Quantization/WuQuantizerTests.cs @@ -28,7 +28,7 @@ public void SinglePixelOpaque() Assert.Equal(1, result.Height); Assert.Equal(Color.Black, (Color)result.Palette.Span[0]); - Assert.Equal(0, result.GetPixelRowSpan(0)[0]); + Assert.Equal(0, result.DangerousGetRowSpan(0)[0]); } [Fact] @@ -48,7 +48,7 @@ public void SinglePixelTransparent() Assert.Equal(1, result.Height); Assert.Equal(default, result.Palette.Span[0]); - Assert.Equal(0, result.GetPixelRowSpan(0)[0]); + Assert.Equal(0, result.DangerousGetRowSpan(0)[0]); } [Fact] @@ -93,25 +93,31 @@ public void Palette256() Assert.Equal(1, result.Width); Assert.Equal(256, result.Height); - var actualImage = new Image(1, 256); + using var actualImage = new Image(1, 256); - ReadOnlySpan paletteSpan = result.Palette.Span; - int paletteCount = paletteSpan.Length - 1; - for (int y = 0; y < actualImage.Height; y++) + actualImage.ProcessPixelRows(accessor => { - Span row = actualImage.GetPixelRowSpan(y); - ReadOnlySpan quantizedPixelSpan = result.GetPixelRowSpan(y); - - for (int x = 0; x < actualImage.Width; x++) + ReadOnlySpan paletteSpan = result.Palette.Span; + int paletteCount = paletteSpan.Length - 1; + for (int y = 0; y < accessor.Height; y++) { - row[x] = paletteSpan[Math.Min(paletteCount, quantizedPixelSpan[x])]; + Span row = accessor.GetRowSpan(y); + ReadOnlySpan quantizedPixelSpan = result.DangerousGetRowSpan(y); + + for (int x = 0; x < accessor.Width; x++) + { + row[x] = paletteSpan[Math.Min(paletteCount, quantizedPixelSpan[x])]; + } } - } + }); - for (int y = 0; y < image.Height; y++) + image.ProcessPixelRows(actualImage, static (imageAccessor, actualImageAccessor) => { - Assert.True(image.GetPixelRowSpan(y).SequenceEqual(actualImage.GetPixelRowSpan(y))); - } + for (int y = 0; y < imageAccessor.Height; y++) + { + Assert.True(imageAccessor.GetRowSpan(y).SequenceEqual(actualImageAccessor.GetRowSpan(y))); + } + }); } [Theory] @@ -162,24 +168,30 @@ private static void TestScale(Func pixelBuilder) Assert.Equal(1, result.Width); Assert.Equal(256, result.Height); - ReadOnlySpan paletteSpan = result.Palette.Span; - int paletteCount = paletteSpan.Length - 1; - for (int y = 0; y < actualImage.Height; y++) + actualImage.ProcessPixelRows(accessor => { - Span row = actualImage.GetPixelRowSpan(y); - ReadOnlySpan quantizedPixelSpan = result.GetPixelRowSpan(y); - - for (int x = 0; x < actualImage.Width; x++) + ReadOnlySpan paletteSpan = result.Palette.Span; + int paletteCount = paletteSpan.Length - 1; + for (int y = 0; y < accessor.Height; y++) { - row[x] = paletteSpan[Math.Min(paletteCount, quantizedPixelSpan[x])]; + Span row = accessor.GetRowSpan(y); + ReadOnlySpan quantizedPixelSpan = result.DangerousGetRowSpan(y); + + for (int x = 0; x < accessor.Width; x++) + { + row[x] = paletteSpan[Math.Min(paletteCount, quantizedPixelSpan[x])]; + } } - } + }); } - for (int y = 0; y < expectedImage.Height; y++) + expectedImage.ProcessPixelRows(actualImage, static (expectedAccessor, actualAccessor) => { - Assert.True(expectedImage.GetPixelRowSpan(y).SequenceEqual(actualImage.GetPixelRowSpan(y))); - } + for (int y = 0; y < expectedAccessor.Height; y++) + { + Assert.True(expectedAccessor.GetRowSpan(y).SequenceEqual(actualAccessor.GetRowSpan(y))); + } + }); } } } diff --git a/tests/ImageSharp.Tests/RunTestsInLoop.ps1 b/tests/ImageSharp.Tests/RunTestsInLoop.ps1 new file mode 100644 index 0000000000..c7c5c9ac51 --- /dev/null +++ b/tests/ImageSharp.Tests/RunTestsInLoop.ps1 @@ -0,0 +1,22 @@ +# This script can be used to collect logs from sporadic bugs +Param( + [int]$TestRunCount=10, + [string]$TargetFramework="netcoreapp3.1", + [string]$Configuration="Release" +) + +$runId = Get-Random -Minimum 0 -Maximum 9999 + +dotnet build -c $Configuration -f $TargetFramework +for ($i = 0; $i -lt $TestRunCount; $i++) { + $logFile = ".\_testlog-" + $runId.ToString("d4") + "-run-" + $i.ToString("d3") + ".log" + Write-Host "Test run $i ..." + & dotnet test --no-build -c $Configuration -f $TargetFramework 3>&1 2>&1 > $logFile + if ($LastExitCode -eq 0) { + Write-Host "Success!" + Remove-Item $logFile + } + else { + Write-Host "Failed: $logFile" + } +} diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ExactImageComparer.cs b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ExactImageComparer.cs index c6bcef4617..5c53922605 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ExactImageComparer.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/ExactImageComparer.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison @@ -29,11 +30,13 @@ public override ImageSimilarityReport CompareImagesOrFrames(); Configuration configuration = expected.GetConfiguration(); + Buffer2D expectedBuffer = expected.PixelBuffer; + Buffer2D actualBuffer = actual.PixelBuffer; for (int y = 0; y < actual.Height; y++) { - Span aSpan = expected.GetPixelRowSpan(y); - Span bSpan = actual.GetPixelRowSpan(y); + Span aSpan = expectedBuffer.DangerousGetRowSpan(y); + Span bSpan = actualBuffer.DangerousGetRowSpan(y); PixelOperations.Instance.ToRgba64(configuration, aSpan, aBuffer); PixelOperations.Instance.ToRgba64(configuration, bSpan, bBuffer); diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs index 38fb4026df..771d4baf9c 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageComparison/TolerantImageComparer.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Runtime.CompilerServices; using SixLabors.ImageSharp.Advanced; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; namespace SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison @@ -74,11 +75,13 @@ public override ImageSimilarityReport CompareImagesOrFrames(); Configuration configuration = expected.GetConfiguration(); + Buffer2D expectedBuffer = expected.PixelBuffer; + Buffer2D actualBuffer = actual.PixelBuffer; for (int y = 0; y < actual.Height; y++) { - Span aSpan = expected.GetPixelRowSpan(y); - Span bSpan = actual.GetPixelRowSpan(y); + Span aSpan = expectedBuffer.DangerousGetRowSpan(y); + Span bSpan = actualBuffer.DangerousGetRowSpan(y); PixelOperations.Instance.ToRgba64(configuration, aSpan, aBuffer); PixelOperations.Instance.ToRgba64(configuration, bSpan, bBuffer); diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs index f5e1f238e6..2d1c6e2241 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/BasicTestPatternProvider.cs @@ -39,25 +39,27 @@ public BasicTestPatternProvider() public override Image GetImage() { var result = new Image(this.Configuration, this.Width, this.Height); - - int midY = this.Height / 2; - int midX = this.Width / 2; - - for (int y = 0; y < midY; y++) + result.ProcessPixelRows(accessor => { - Span row = result.GetPixelRowSpan(y); + int midY = this.Height / 2; + int midX = this.Width / 2; - row.Slice(0, midX).Fill(TopLeftColor); - row.Slice(midX, this.Width - midX).Fill(TopRightColor); - } + for (int y = 0; y < midY; y++) + { + Span row = accessor.GetRowSpan(y); - for (int y = midY; y < this.Height; y++) - { - Span row = result.GetPixelRowSpan(y); + row.Slice(0, midX).Fill(TopLeftColor); + row.Slice(midX, this.Width - midX).Fill(TopRightColor); + } - row.Slice(0, midX).Fill(BottomLeftColor); - row.Slice(midX, this.Width - midX).Fill(BottomRightColor); - } + for (int y = midY; y < this.Height; y++) + { + Span row = accessor.GetRowSpan(y); + + row.Slice(0, midX).Fill(BottomLeftColor); + row.Slice(midX, this.Width - midX).Fill(BottomRightColor); + } + }); return result; } diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs index f57c19f12a..e2e7d73bc6 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/FileProvider.cs @@ -8,6 +8,7 @@ using System.Reflection; using System.Threading.Tasks; using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Memory; using SixLabors.ImageSharp.PixelFormats; using Xunit.Abstractions; @@ -23,18 +24,17 @@ internal class FileProvider : TestImageProvider, IXunitSerializable // are shared between PixelTypes.Color & PixelTypes.Rgba32 private class Key : IEquatable { - private readonly Tuple commonValues; + private readonly Tuple commonValues; private readonly Dictionary decoderParameters; - public Key(PixelTypes pixelType, string filePath, int allocatorBufferCapacity, IImageDecoder customDecoder) + public Key(PixelTypes pixelType, string filePath, IImageDecoder customDecoder) { Type customType = customDecoder?.GetType(); - this.commonValues = new Tuple( + this.commonValues = new Tuple( pixelType, filePath, - customType, - allocatorBufferCapacity); + customType); this.decoderParameters = GetDecoderParameters(customDecoder); } @@ -152,13 +152,13 @@ public override Image GetImage(IImageDecoder decoder) { Guard.NotNull(decoder, nameof(decoder)); - if (!TestEnvironment.Is64BitProcess) + // Do not cache with 64 bits or if image has been created with non-default MemoryAllocator + if (!TestEnvironment.Is64BitProcess || this.Configuration.MemoryAllocator != MemoryAllocator.Default) { return this.LoadImage(decoder); } - int bufferCapacity = this.Configuration.MemoryAllocator.GetBufferCapacityInBytes(); - var key = new Key(this.PixelType, this.FilePath, bufferCapacity, decoder); + var key = new Key(this.PixelType, this.FilePath, decoder); Image cachedImage = Cache.GetOrAdd(key, _ => this.LoadImage(decoder)); diff --git a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs index 4860524b36..700c40b726 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ImageProviders/TestImageProvider.cs @@ -21,20 +21,11 @@ namespace SixLabors.ImageSharp.Tests public abstract partial class TestImageProvider : ITestImageProvider, IXunitSerializable where TPixel : unmanaged, IPixel { - // Create a Configuration with Configuration.CreateDefaultInstance(), - // but use the shared MemoryAllocator from Configuration.Default.MemoryAllocator - private static Configuration CreateDefaultConfiguration() - { - var configuration = Configuration.CreateDefaultInstance(); - configuration.MemoryAllocator = ImageSharp.Configuration.Default.MemoryAllocator; - return configuration; - } - public PixelTypes PixelType { get; private set; } = typeof(TPixel).GetPixelType(); public virtual string SourceFileOrDescription => string.Empty; - public Configuration Configuration { get; set; } = CreateDefaultConfiguration(); + public Configuration Configuration { get; set; } = Configuration.CreateDefaultInstance(); /// /// Gets the utility instance to provide information about the test image & manage input/output. diff --git a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingBridge.cs b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingBridge.cs index 157748bdd1..28f0dba06a 100644 --- a/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingBridge.cs +++ b/tests/ImageSharp.Tests/TestUtilities/ReferenceCodecs/SystemDrawingBridge.cs @@ -47,14 +47,14 @@ internal static unsafe Image From32bppArgbSystemDrawingBitmap(Bi long destRowByteCount = w * sizeof(Bgra32); Configuration configuration = image.GetConfiguration(); - - using (IMemoryOwner workBuffer = Configuration.Default.MemoryAllocator.Allocate(w)) + image.ProcessPixelRows(accessor => { + using IMemoryOwner workBuffer = Configuration.Default.MemoryAllocator.Allocate(w); fixed (Bgra32* destPtr = &workBuffer.GetReference()) { for (int y = 0; y < h; y++) { - Span row = image.Frames.RootFrame.GetPixelRowSpan(y); + Span row = accessor.GetRowSpan(y); byte* sourcePtr = sourcePtrBase + (data.Stride * y); @@ -65,7 +65,7 @@ internal static unsafe Image From32bppArgbSystemDrawingBitmap(Bi row); } } - } + }); } finally { @@ -106,6 +106,7 @@ internal static unsafe Image From24bppRgbSystemDrawingBitmap(Bit long destRowByteCount = w * sizeof(Bgr24); Configuration configuration = image.GetConfiguration(); + Buffer2D imageBuffer = image.Frames.RootFrame.PixelBuffer; using (IMemoryOwner workBuffer = Configuration.Default.MemoryAllocator.Allocate(w)) { @@ -113,7 +114,7 @@ internal static unsafe Image From24bppRgbSystemDrawingBitmap(Bit { for (int y = 0; y < h; y++) { - Span row = image.Frames.RootFrame.GetPixelRowSpan(y); + Span row = imageBuffer.DangerousGetRowSpan(y); byte* sourcePtr = sourcePtrBase + (data.Stride * y); @@ -144,24 +145,23 @@ internal static unsafe Bitmap To32bppArgbSystemDrawingBitmap(Image workBuffer = image.GetConfiguration().MemoryAllocator.Allocate(w)) + image.ProcessPixelRows(accessor => { + using IMemoryOwner workBuffer = image.GetConfiguration().MemoryAllocator.Allocate(w); fixed (Bgra32* sourcePtr = &workBuffer.GetReference()) { for (int y = 0; y < h; y++) { - Span row = image.Frames.RootFrame.GetPixelRowSpan(y); + Span row = accessor.GetRowSpan(y); PixelOperations.Instance.ToBgra32(configuration, row, workBuffer.GetSpan()); byte* destPtr = destPtrBase + (data.Stride * y); Buffer.MemoryCopy(sourcePtr, destPtr, destRowByteCount, sourceRowByteCount); } } - } + }); } finally { diff --git a/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.cs b/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.cs index b14c2bf782..3ccaf2ba37 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestEnvironment.cs @@ -24,14 +24,14 @@ public static partial class TestEnvironment private static readonly Lazy SolutionDirectoryFullPathLazy = new Lazy(GetSolutionDirectoryFullPathImpl); - private static readonly Lazy NetCoreVersionLazy = new Lazy(GetNetCoreVersion); + private static readonly Lazy NetCoreVersionLazy = new Lazy(GetNetCoreVersion); static TestEnvironment() => PrepareRemoteExecutor(); /// /// Gets the .NET Core version, if running on .NET Core, otherwise returns an empty string. /// - internal static string NetCoreVersion => NetCoreVersionLazy.Value; + internal static Version NetCoreVersion => NetCoreVersionLazy.Value; // ReSharper disable once InconsistentNaming @@ -118,7 +118,7 @@ internal static string GetReferenceOutputFileName(string actualOutputFileName) = internal static bool Is64BitProcess => IntPtr.Size == 8; - internal static bool IsFramework => string.IsNullOrEmpty(NetCoreVersion); + internal static bool IsFramework => NetCoreVersion == null; /// /// A dummy operation to enforce the execution of the static constructor. @@ -159,7 +159,7 @@ internal static string CreateOutputDirectory(string path, params string[] pathPa /// private static void PrepareRemoteExecutor() { - if (!IsFramework) + if (!IsFramework || !Environment.Is64BitProcess) { return; } @@ -262,17 +262,24 @@ static FileInfo Find(DirectoryInfo root, string name) /// Solution borrowed from: /// https://github.com/dotnet/BenchmarkDotNet/issues/448#issuecomment-308424100 /// - private static string GetNetCoreVersion() + private static Version GetNetCoreVersion() { Assembly assembly = typeof(System.Runtime.GCSettings).GetTypeInfo().Assembly; string[] assemblyPath = assembly.Location.Split(new[] { '/', '\\' }, StringSplitOptions.RemoveEmptyEntries); int netCoreAppIndex = Array.IndexOf(assemblyPath, "Microsoft.NETCore.App"); if (netCoreAppIndex > 0 && netCoreAppIndex < assemblyPath.Length - 2) { - return assemblyPath[netCoreAppIndex + 1]; + string runtimeFolderStr = assemblyPath[netCoreAppIndex + 1]; + int previewSuffix = runtimeFolderStr.IndexOf('-'); + if (previewSuffix > 0) + { + runtimeFolderStr = runtimeFolderStr.Substring(0, previewSuffix); + } + + return Version.Parse(runtimeFolderStr); } - return string.Empty; + return null; } } } diff --git a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs index 3f41281d01..719e529466 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestImageExtensions.cs @@ -12,6 +12,7 @@ using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors; +using SixLabors.ImageSharp.Tests.Memory; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using Xunit; @@ -396,12 +397,18 @@ public static Image ComparePixelBufferTo( Span expectedPixels) where TPixel : unmanaged, IPixel { - Assert.True(image.TryGetSinglePixelSpan(out Span actualPixels)); - CompareBuffers(expectedPixels, actualPixels); + Assert.True(image.DangerousTryGetSinglePixelMemory(out Memory actualPixels)); + CompareBuffers(expectedPixels, actualPixels.Span); return image; } + public static Image ComparePixelBufferTo( + this Image image, + Memory expectedPixels) + where TPixel : unmanaged, IPixel => + ComparePixelBufferTo(image, expectedPixels.Span); + public static void CompareBuffers(Span expected, Span actual) where T : struct, IEquatable { @@ -416,6 +423,27 @@ public static void CompareBuffers(Span expected, Span actual) } } + public static void CompareBuffers(Buffer2D expected, Buffer2D actual) + where T : struct, IEquatable + { + Assert.True(expected.Size() == actual.Size(), "Buffer sizes are not equal!"); + + for (int y = 0; y < expected.Height; y++) + { + Span expectedRow = expected.DangerousGetRowSpan(y); + Span actualRow = actual.DangerousGetRowSpan(y); + for (int x = 0; x < expectedRow.Length; x++) + { + T expectedVal = expectedRow[x]; + T actualVal = actualRow[x]; + + Assert.True( + expectedVal.Equals(actualVal), + $"Buffers differ at position ({x},{y})! Expected: {expectedVal} | Actual: {actualVal}"); + } + } + } + /// /// All pixels in all frames should be exactly equal to 'expectedPixel'. /// @@ -456,7 +484,8 @@ public static Image ComparePixelBufferTo(this Image imag public static ImageFrame ComparePixelBufferTo(this ImageFrame imageFrame, TPixel expectedPixel) where TPixel : unmanaged, IPixel { - Assert.True(imageFrame.TryGetSinglePixelSpan(out Span actualPixels)); + Assert.True(imageFrame.DangerousTryGetSinglePixelMemory(out Memory actualPixelMem)); + Span actualPixels = actualPixelMem.Span; for (int i = 0; i < actualPixels.Length; i++) { @@ -471,7 +500,8 @@ public static ImageFrame ComparePixelBufferTo( Span expectedPixels) where TPixel : unmanaged, IPixel { - Assert.True(image.TryGetSinglePixelSpan(out Span actual)); + Assert.True(image.DangerousTryGetSinglePixelMemory(out Memory actualMem)); + Span actual = actualMem.Span; Assert.True(expectedPixels.Length == actual.Length, "Buffer sizes are not equal!"); for (int i = 0; i < expectedPixels.Length; i++) @@ -663,7 +693,7 @@ internal static AllocatorBufferCapacityConfigurator LimitAllocatorBufferCapacity this TestImageProvider provider) where TPixel : unmanaged, IPixel { - var allocator = ArrayPoolMemoryAllocator.CreateDefault(); + var allocator = new TestMemoryAllocator(); provider.Configuration.MemoryAllocator = allocator; return new AllocatorBufferCapacityConfigurator(allocator, Unsafe.SizeOf()); } @@ -672,7 +702,8 @@ internal static Image ToGrayscaleImage(this Buffer2D buffer, floa { var image = new Image(buffer.Width, buffer.Height); - Assert.True(image.Frames.RootFrame.TryGetSinglePixelSpan(out Span pixels)); + Assert.True(image.Frames.RootFrame.DangerousTryGetSinglePixelMemory(out Memory pixelMem)); + Span pixels = pixelMem.Span; Span bufferSpan = buffer.DangerousGetSingleSpan(); for (int i = 0; i < bufferSpan.Length; i++) @@ -705,7 +736,7 @@ protected override void OnFrameApply(ImageFrame source) Rectangle sourceRectangle = this.SourceRectangle; Configuration configuration = this.Configuration; - var operation = new RowOperation(configuration, sourceRectangle, source); + var operation = new RowOperation(configuration, sourceRectangle, source.PixelBuffer); ParallelRowIterator.IterateRowIntervals( configuration, @@ -717,9 +748,9 @@ protected override void OnFrameApply(ImageFrame source) { private readonly Configuration configuration; private readonly Rectangle bounds; - private readonly ImageFrame source; + private readonly Buffer2D source; - public RowOperation(Configuration configuration, Rectangle bounds, ImageFrame source) + public RowOperation(Configuration configuration, Rectangle bounds, Buffer2D source) { this.configuration = configuration; this.bounds = bounds; @@ -730,7 +761,7 @@ public void Invoke(in RowInterval rows, Span span) { for (int y = rows.Min; y < rows.Max; y++) { - Span rowSpan = this.source.GetPixelRowSpan(y).Slice(this.bounds.Left, this.bounds.Width); + Span rowSpan = this.source.DangerousGetRowSpan(y).Slice(this.bounds.Left, this.bounds.Width); PixelOperations.Instance.ToVector4(this.configuration, rowSpan, span, PixelConversionModifiers.Scale); for (int i = 0; i < span.Length; i++) { @@ -747,14 +778,16 @@ public void Invoke(in RowInterval rows, Span span) internal class AllocatorBufferCapacityConfigurator { - private readonly ArrayPoolMemoryAllocator allocator; +#pragma warning disable CS0618 // 'ArrayPoolMemoryAllocator' is obsolete + private readonly TestMemoryAllocator allocator; private readonly int pixelSizeInBytes; - public AllocatorBufferCapacityConfigurator(ArrayPoolMemoryAllocator allocator, int pixelSizeInBytes) + public AllocatorBufferCapacityConfigurator(TestMemoryAllocator allocator, int pixelSizeInBytes) { this.allocator = allocator; this.pixelSizeInBytes = pixelSizeInBytes; } +#pragma warning restore CS0618 public void InBytes(int totalBytes) => this.allocator.BufferCapacityInBytes = totalBytes; diff --git a/tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs b/tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs index eab0d57765..5fb6d873a7 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestMemoryAllocator.cs @@ -12,8 +12,8 @@ namespace SixLabors.ImageSharp.Tests.Memory { internal class TestMemoryAllocator : MemoryAllocator { - private readonly List allocationLog = new List(); - private readonly List returnLog = new List(); + private List allocationLog; + private List returnLog; public TestMemoryAllocator(byte dirtyValue = 42) { @@ -27,29 +27,29 @@ public TestMemoryAllocator(byte dirtyValue = 42) public int BufferCapacityInBytes { get; set; } = int.MaxValue; - public IReadOnlyList AllocationLog => this.allocationLog; + public IReadOnlyList AllocationLog => this.allocationLog ?? throw new InvalidOperationException("Call TestMemoryAllocator.EnableLogging() first!"); - public IReadOnlyList ReturnLog => this.returnLog; + public IReadOnlyList ReturnLog => this.returnLog ?? throw new InvalidOperationException("Call TestMemoryAllocator.EnableLogging() first!"); protected internal override int GetBufferCapacityInBytes() => this.BufferCapacityInBytes; - public override IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None) + public void EnableNonThreadSafeLogging() { - T[] array = this.AllocateArray(length, options); - return new BasicArrayBuffer(array, length, this); + this.allocationLog = new List(); + this.returnLog = new List(); } - public override IManagedByteBuffer AllocateManagedByteBuffer(int length, AllocationOptions options = AllocationOptions.None) + public override IMemoryOwner Allocate(int length, AllocationOptions options = AllocationOptions.None) { - byte[] array = this.AllocateArray(length, options); - return new ManagedByteBuffer(array, this); + T[] array = this.AllocateArray(length, options); + return new BasicArrayBuffer(array, length, this); } private T[] AllocateArray(int length, AllocationOptions options) where T : struct { var array = new T[length + 42]; - this.allocationLog.Add(AllocationRequest.Create(options, length, array)); + this.allocationLog?.Add(AllocationRequest.Create(options, length, array)); if (options == AllocationOptions.None) { @@ -63,7 +63,7 @@ private T[] AllocateArray(int length, AllocationOptions options) private void Return(BasicArrayBuffer buffer) where T : struct { - this.returnLog.Add(new ReturnRequest(buffer.Array.GetHashCode())); + this.returnLog?.Add(new ReturnRequest(buffer.Array.GetHashCode())); } public struct AllocationRequest @@ -152,12 +152,12 @@ public override unsafe MemoryHandle Pin(int elementIndex = 0) } void* ptr = (void*)this.pinHandle.AddrOfPinnedObject(); - return new MemoryHandle(ptr, this.pinHandle); + return new MemoryHandle(ptr, pinnable: this); } public override void Unpin() { - throw new NotImplementedException(); + this.pinHandle.Free(); } /// @@ -170,7 +170,7 @@ protected override void Dispose(bool disposing) } } - private class ManagedByteBuffer : BasicArrayBuffer, IManagedByteBuffer + private class ManagedByteBuffer : BasicArrayBuffer, IMemoryOwner { 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 32b5eaf182..3c27b60fe4 100644 --- a/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs +++ b/tests/ImageSharp.Tests/TestUtilities/TestUtils.cs @@ -12,6 +12,7 @@ using SixLabors.ImageSharp.Processing; using SixLabors.ImageSharp.Processing.Processors.Dithering; using SixLabors.ImageSharp.Processing.Processors.Transforms; +using SixLabors.ImageSharp.Tests.Memory; using SixLabors.ImageSharp.Tests.TestUtilities.ImageComparison; using Xunit; @@ -53,6 +54,32 @@ static TestUtils() public static bool HasFlag(this PixelTypes pixelTypes, PixelTypes flag) => (pixelTypes & flag) == flag; + public static byte[] GetRandomBytes(int length, int seed = 42) + { + var rnd = new Random(42); + byte[] bytes = new byte[length]; + rnd.NextBytes(bytes); + return bytes; + } + + internal static byte[] FillImageWithRandomBytes(Image image) + { + byte[] expected = TestUtils.GetRandomBytes(image.Width * image.Height * 2); + image.ProcessPixelRows(accessor => + { + int cnt = 0; + for (int y = 0; y < accessor.Height; y++) + { + Span row = accessor.GetRowSpan(y); + for (int x = 0; x < row.Length; x++) + { + row[x] = new La16(expected[cnt++], expected[cnt++]); + } + } + }); + return expected; + } + public static bool IsEquivalentTo(this Image a, Image b, bool compareAlpha = true) where TPixel : unmanaged, IPixel { @@ -165,7 +192,7 @@ internal static void RunBufferCapacityLimitProcessorTest( int width = expected.Width; expected.Mutate(process); - var allocator = ArrayPoolMemoryAllocator.CreateDefault(); + var allocator = new TestMemoryAllocator(); provider.Configuration.MemoryAllocator = allocator; allocator.BufferCapacityInBytes = bufferCapacityInPixelRows * width * Unsafe.SizeOf(); @@ -277,8 +304,8 @@ public static void RunValidatingProcessorTestOnWrappedMemoryImage( using (Image image0 = provider.GetImage()) { - Assert.True(image0.TryGetSinglePixelSpan(out Span imageSpan)); - var mmg = TestMemoryManager.CreateAsCopyOf(imageSpan); + Assert.True(image0.DangerousTryGetSinglePixelMemory(out Memory imageMem)); + var mmg = TestMemoryManager.CreateAsCopyOf(imageMem.Span); using (var image1 = Image.WrapMemory(mmg.Memory, image0.Width, image0.Height)) {