// 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 = new UnmanagedBuffer(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, _ => { buffer.BufferHandle.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 = new UnmanagedBuffer(100); var buffer2 = new UnmanagedBuffer(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, (_, _) => { buffer1.BufferHandle.Dispose(); buffer2.BufferHandle.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 = new UnmanagedBuffer(100); var buffer2 = new UnmanagedBuffer(100); var buffer3 = new UnmanagedBuffer(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, (_, _, _) => { buffer1.BufferHandle.Dispose(); buffer2.BufferHandle.Dispose(); buffer3.BufferHandle.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) => (IMemoryOwner)this.buffers.Pop(); } } }