From a1cb6592eb4d109a4f9ccc50239dfcb48a6c8ed7 Mon Sep 17 00:00:00 2001 From: dennisarfan Date: Thu, 31 Jul 2025 06:19:32 +0700 Subject: [PATCH 1/4] Upgrade to .net9 --- App/Controllers/ImageController.cs | 4 +- App/StitchATon2.App.csproj | 3 +- App/Utils.cs | 7 + Domain/ImageCreators/DangerousImageCreator.cs | 172 ++++++++++++++++++ Domain/StitchATon2.Domain.csproj | 2 +- Domain/Utils.cs | 30 ++- Infra/Buffers/ImmovableMemory.cs | 37 ++++ Infra/Buffers/MemoryAllocator.cs | 3 + Infra/Buffers/UnmanagedMemory.cs | 19 +- Infra/Encoders/PngPipeEncoder.cs | 135 ++++++++++++++ Infra/ImageIntegral.cs | 4 +- Infra/StitchATon2.Infra.csproj | 4 +- Infra/Synchronization/TaskHelper.cs | 10 +- Infra/Utils.cs | 17 +- global.json | 2 +- 15 files changed, 395 insertions(+), 54 deletions(-) create mode 100644 Domain/ImageCreators/DangerousImageCreator.cs create mode 100644 Infra/Buffers/ImmovableMemory.cs create mode 100644 Infra/Encoders/PngPipeEncoder.cs diff --git a/App/Controllers/ImageController.cs b/App/Controllers/ImageController.cs index 2b5386f..d2fda76 100644 --- a/App/Controllers/ImageController.cs +++ b/App/Controllers/ImageController.cs @@ -24,7 +24,7 @@ public static class ImageController await tileManager .CreateSection(dto) - .WriteToStream(response.Body, dto.OutputScale); + .DangerousWriteToPipe(response.BodyWriter, dto.OutputScale); await response.CompleteAsync(); } @@ -47,7 +47,7 @@ public static class ImageController var scale = float.Clamp(480f / int.Max(section.Width, section.Height), 0.01f, 1f); Console.WriteLine($"Generate random image for {coordinatePair} scale: {scale}"); - await section.WriteToStream(response.Body, scale); + await section.DangerousWriteToPipe(response.BodyWriter, scale); await response.CompleteAsync(); } } \ No newline at end of file diff --git a/App/StitchATon2.App.csproj b/App/StitchATon2.App.csproj index ff5d1d5..aa50324 100644 --- a/App/StitchATon2.App.csproj +++ b/App/StitchATon2.App.csproj @@ -1,11 +1,12 @@ - net8.0 + net9.0 enable enable true StitchATon2.App + true diff --git a/App/Utils.cs b/App/Utils.cs index bf13bbd..2b96e01 100644 --- a/App/Utils.cs +++ b/App/Utils.cs @@ -1,3 +1,4 @@ +using System.IO.Pipelines; using StitchATon2.App.Models; using StitchATon2.Domain; using StitchATon2.Domain.ImageCreators; @@ -19,4 +20,10 @@ public static class Utils var imageCreator = new ImageCreator(section); await imageCreator.WriteToStream(stream, scale!.Value); } + + public static async Task DangerousWriteToPipe(this GridSection section, PipeWriter pipeWriter, float? scale) + { + var imageCreator = new DangerousImageCreator(section); + await imageCreator.WriteToPipe(pipeWriter, scale!.Value); + } } \ No newline at end of file diff --git a/Domain/ImageCreators/DangerousImageCreator.cs b/Domain/ImageCreators/DangerousImageCreator.cs new file mode 100644 index 0000000..2346479 --- /dev/null +++ b/Domain/ImageCreators/DangerousImageCreator.cs @@ -0,0 +1,172 @@ +using System.IO.Pipelines; +using System.Runtime.CompilerServices; +using SixLabors.ImageSharp.PixelFormats; +using StitchATon2.Infra; +using StitchATon2.Infra.Buffers; +using StitchATon2.Infra.Encoders; +using StitchATon2.Infra.Synchronization; + +namespace StitchATon2.Domain.ImageCreators; + +public sealed class DangerousImageCreator : IDisposable +{ + private readonly GridSection _section; + private readonly IBuffer _mmfReadBuffer; + + private int FullWidth => _section.TileManager.Configuration.FullWidth; + private int FullHeight => _section.TileManager.Configuration.FullHeight; + + private int OffsetX => _section.OffsetX; + private int OffsetY => _section.OffsetY; + + private int Width => _section.Width; + private int Height => _section.Height; + + private int TileWidth => _section.TileManager.Configuration.Width; + private int TileHeight => _section.TileManager.Configuration.Height; + private Tile TileOrigin => _section.Origin; + + private int RightmostPixelIndex => _section.TileManager.Configuration.RightTileIndex; + private int BottomPixelIndex => _section.TileManager.Configuration.BottomTileIndex; + + private TileManager TileManager => _section.TileManager; + + public DangerousImageCreator(GridSection section) + { + _section = section; + _mmfReadBuffer = MemoryAllocator.Allocate(TileWidth); + } + + ~DangerousImageCreator() => Dispose(); + + public async Task WriteToPipe(PipeWriter outputPipe, float scale, CancellationToken cancellationToken = default) + { + var scaleFactor = MathF.ReciprocalEstimate(scale); + var targetWidth = (int)(Width / scaleFactor); + var targetHeight = (int)(Height / scaleFactor); + + var encoder = new PngPipeEncoder(outputPipe, targetWidth, targetHeight); + encoder.WriteHeader(); + + var outputBufferSize = targetWidth * Unsafe.SizeOf(); + + using var xLookup = Utils.BoundsMatrix(scaleFactor, targetWidth, FullWidth, OffsetX); + using var yLookup = Utils.BoundsMatrix(scaleFactor, targetHeight, FullHeight, OffsetY); + + using var yStartMap = MemoryAllocator.Allocate(targetWidth); + using var yEndMap = MemoryAllocator.Allocate(targetWidth); + + var yStart = OffsetY; + + var outputTaskQueue = TaskHelper.SynchronizedTaskFactory.StartNew(() => { }, cancellationToken); + for (var y = 0; y < targetHeight; y++) + { + var yEnd = yLookup[y]; + + var (localRow0, localOffsetY0) = int.DivRem(yStart, TileHeight); + MapRow(localRow0, localOffsetY0, xLookup, targetWidth, yStartMap); + + var (localRow1, localOffsetY1) = int.DivRem(yEnd, TileHeight); + MapRow(localRow1, localOffsetY1, xLookup, targetWidth, yEndMap); + + if (localRow0 != localRow1) + { + MapRow(localRow0, BottomPixelIndex, xLookup, targetWidth, yEndMap, true); + } + + int xStart = OffsetX, x0 = 0; + + var pxInt32 = Int32Pixel.Zero; + ref var px = ref pxInt32; + ref var rChannel = ref Unsafe.As(ref px); + ref var gChannel = ref Unsafe.Add(ref rChannel, 4); + ref var bChannel = ref Unsafe.Add(ref rChannel, 8); + + var outputBuffer = MemoryAllocator.Allocate(outputBufferSize); + ref var outputChannel = ref outputBuffer.Span[0]; + for (int x1 = 0; x1 < targetWidth; x1++) + { + var xEnd = xLookup[x1]; + + px = yEndMap[x1]; + px += yStartMap[x0]; + px -= yEndMap[x0]; + px -= yStartMap[x1]; + px /= Math.Max(1, (xEnd - xStart) * (yEnd - yStart)); + + outputChannel = rChannel; + outputChannel = ref Unsafe.Add(ref outputChannel, 1); + + outputChannel = gChannel; + outputChannel = ref Unsafe.Add(ref outputChannel, 1); + + outputChannel = bChannel; + outputChannel = ref Unsafe.Add(ref outputChannel, 1); + + xStart = xEnd; + x0 = x1; + } + + outputTaskQueue = outputTaskQueue + .ContinueWith(_ => encoder.WriteData(outputBuffer, cancellationToken: cancellationToken), cancellationToken); + + yStart = yEnd; + } + + await outputTaskQueue; + encoder.WriteEndOfFile(cancellationToken); + } + + private void MapRow( + int rowOffset, + int yOffset, + IBuffer boundsMatrix, + int count, + IBuffer destination, + bool appendMode = false) + { + var sourceMap = boundsMatrix.Span[..count]; + var currentTile = TileManager.GetAdjacent(TileOrigin, 0, rowOffset); + var xAdder = Int32Pixel.Zero; + var xOffset = 0; + var written = 0; + var destinationSpan = destination.Span; + var readBufferSpan = _mmfReadBuffer.Span; + while (true) + { + currentTile.Integral.Acquire(yOffset, readBufferSpan); + int localX; + if (appendMode) + { + while (written < sourceMap.Length && (localX = sourceMap[written] - xOffset) < TileWidth) + { + destinationSpan[written] += readBufferSpan[localX]; + destinationSpan[written] += xAdder; + written++; + } + } + else + { + while (written < sourceMap.Length && (localX = sourceMap[written] - xOffset) < TileWidth) + { + destinationSpan[written] = readBufferSpan[localX]; + destinationSpan[written] += xAdder; + written++; + } + } + + if (written >= sourceMap.Length) + break; + + xAdder += readBufferSpan[RightmostPixelIndex]; + xOffset += TileWidth; + currentTile = TileManager.GetAdjacent(currentTile, 1, 0); + } + } + + public void Dispose() + { + _mmfReadBuffer.Dispose(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/Domain/StitchATon2.Domain.csproj b/Domain/StitchATon2.Domain.csproj index 6e68f4d..99a234b 100644 --- a/Domain/StitchATon2.Domain.csproj +++ b/Domain/StitchATon2.Domain.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 enable enable diff --git a/Domain/Utils.cs b/Domain/Utils.cs index b3bd3d0..4cde46b 100644 --- a/Domain/Utils.cs +++ b/Domain/Utils.cs @@ -20,6 +20,7 @@ public static class Utils return (a + b - 1) / b; } + [Pure] public static (int Column, int Row) GetSBSCoordinate(string coordinate) { var column = coordinate[^1] - '0'; @@ -33,7 +34,13 @@ public static class Utils return (column, row); } - + /// + /// Performs a SIMD-accelerated calculation that generates a buffer of bounded, scaled indices. + /// + /// The amount by which to scale the sequence values. + /// The total number of scalar values to generate. + /// Upper limit (exclusive) for clamping values. + /// The offset to apply before clamping. public static IBuffer BoundsMatrix(float scaleFactor, int length, int max, int offset) { var vectorSize = DivCeil(length, Vector.Count); @@ -45,14 +52,13 @@ public static class Utils var vectorMax = new Vector(max - 1); var vectorScale = new Vector(scaleFactor); - var vectorSequence = SequenceVector(0f, 1f); + var vectorSequence = Vector.CreateSequence(0f, 1f); var seq = 0f; for (var i = 0; i < vectorSize; i++, seq += Vector.Count) { var sequence = new Vector(seq) + vectorSequence; - span[i] = Vector.Multiply(sequence, vectorScale); - span[i] = Vector.Add(span[i], vectorScale); + span[i] = Vector.FusedMultiplyAdd(sequence, vectorScale, vectorScale); span[i] = Vector.Ceiling(span[i]); } @@ -62,23 +68,9 @@ public static class Utils { resultSpan[i] = Vector.ConvertToInt32(span[i]); resultSpan[i] = Vector.Add(resultSpan[i], vectorOffset); - resultSpan[i] = Vector.Min(resultSpan[i], vectorMax); - resultSpan[i] = Vector.Max(resultSpan[i], vectorMin); + resultSpan[i] = Vector.ClampNative(resultSpan[i], vectorMin, vectorMax); } return result; } - - private static Vector SequenceVector(float start, float step) - { - var vector = Vector.Zero; - ref var reference = ref Unsafe.As, float>(ref vector); - for (var i = 0; i < Vector.Count; i++) - { - ref var current = ref Unsafe.Add(ref reference, i); - current = start + step * i; - } - - return vector; - } } \ No newline at end of file diff --git a/Infra/Buffers/ImmovableMemory.cs b/Infra/Buffers/ImmovableMemory.cs new file mode 100644 index 0000000..4400c9a --- /dev/null +++ b/Infra/Buffers/ImmovableMemory.cs @@ -0,0 +1,37 @@ +using System.Buffers; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace StitchATon2.Infra.Buffers; + +internal sealed unsafe class ImmovableMemory : MemoryManager where T : unmanaged +{ + private readonly T* _pointer; + private readonly int _length; + private bool _disposed; + + public ImmovableMemory(int count) + { + _pointer = (T*)NativeMemory.Alloc((nuint)count, (nuint)Unsafe.SizeOf()); + _length = count; + } + + protected override void Dispose(bool disposing) + { + if (!_disposed) + { + NativeMemory.Free(_pointer); + _disposed = true; + } + } + + public override Span GetSpan() + => new(_pointer, _length); + + public override MemoryHandle Pin(int elementIndex = 0) + => new(_pointer + elementIndex); + + public override void Unpin() + { + } +} \ No newline at end of file diff --git a/Infra/Buffers/MemoryAllocator.cs b/Infra/Buffers/MemoryAllocator.cs index 43c1590..75787f0 100644 --- a/Infra/Buffers/MemoryAllocator.cs +++ b/Infra/Buffers/MemoryAllocator.cs @@ -12,4 +12,7 @@ public static class MemoryAllocator public static ArrayOwner AllocateArray(int count) where T : unmanaged => new(ArrayPool.Shared, count); + + public static MemoryManager AllocateImmovable(int count) where T : unmanaged + => new ImmovableMemory(count); } \ No newline at end of file diff --git a/Infra/Buffers/UnmanagedMemory.cs b/Infra/Buffers/UnmanagedMemory.cs index 4bf6064..7f61b3d 100644 --- a/Infra/Buffers/UnmanagedMemory.cs +++ b/Infra/Buffers/UnmanagedMemory.cs @@ -3,18 +3,23 @@ using System.Runtime.InteropServices; namespace StitchATon2.Infra.Buffers; +/// +/// Provide non-thread safe anti GC contiguous memory. +/// +/// internal sealed unsafe class UnmanagedMemory : IBuffer where T : unmanaged { - private readonly void* _pointer; + private readonly T* _pointer; private readonly int _count; + private bool _disposed; - public ref T this[int index] => ref Unsafe.AsRef((T*)_pointer + index); // *((T*)_pointer + index); + public ref T this[int index] => ref Unsafe.AsRef(_pointer + index); public Span Span => new(_pointer, _count); public UnmanagedMemory(int count) { - _pointer = NativeMemory.Alloc((nuint)count, (nuint)Unsafe.SizeOf()); + _pointer = (T*)NativeMemory.Alloc((nuint)count, (nuint)Unsafe.SizeOf()); _count = count; } @@ -22,7 +27,11 @@ internal sealed unsafe class UnmanagedMemory : IBuffer where T : unmanaged public void Dispose() { - NativeMemory.Free(_pointer); - GC.SuppressFinalize(this); + if (!_disposed) + { + NativeMemory.Free(_pointer); + GC.SuppressFinalize(this); + _disposed = true; + } } } \ No newline at end of file diff --git a/Infra/Encoders/PngPipeEncoder.cs b/Infra/Encoders/PngPipeEncoder.cs new file mode 100644 index 0000000..8aef896 --- /dev/null +++ b/Infra/Encoders/PngPipeEncoder.cs @@ -0,0 +1,135 @@ +using System.Buffers; +using System.Buffers.Binary; +using System.IO.Compression; +using System.IO.Pipelines; +using StitchATon2.Infra.Buffers; + +namespace StitchATon2.Infra.Encoders; + +public class PngPipeEncoder : IDisposable +{ + private const int BufferSize = 8 * 1024; + private const int FlushThreshold = 1024; + + private readonly PipeWriter _outputPipe; + private readonly MemoryStream _memoryStream; + private readonly int _width; + private readonly int _height; + + private readonly ZLibStream _zlibStream; + private bool _disposed; + private bool _shouldFlush; + + public PngPipeEncoder(PipeWriter outputPipe, int width, int height) + { + _outputPipe = outputPipe; + _width = width; + _height = height; + _memoryStream = new MemoryStream(BufferSize * 2); + _zlibStream = new ZLibStream(_memoryStream, CompressionLevel.Optimal, leaveOpen: true); + _memoryStream.SetLength(8); + _memoryStream.Position = 8; + } + + ~PngPipeEncoder() => Dispose(); + + public void WriteHeader() + { + Span headerBytes = [ + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG Signature + 0x00, 0x00, 0x00, 0x0D, // Length + + // IHDR chunk + 0x49, 0x48, 0x44, 0x52, // IHDR + 0x00, 0x00, 0x00, 0x00, // Reserve to write Width + 0x00, 0x00, 0x00, 0x00, // Reserve to write Height + 0x08, // Bit depth + 0x02, // Color type + 0x00, // Compression method + 0x00, // Filter method + 0x00, // Interlace method + 0x00, 0x00, 0x00, 0x00, // Reserve to write CRC-32 + ]; + + BinaryPrimitives.WriteInt32BigEndian(headerBytes[16..], _width); + BinaryPrimitives.WriteInt32BigEndian(headerBytes[20..], _height); + var crc = Crc32.Compute(headerBytes.Slice(12, 17)); + + BinaryPrimitives.WriteUInt32BigEndian(headerBytes[29..], crc); + + _outputPipe.Write(headerBytes); + } + + public void WriteData(IBuffer buffer, bool disposeBuffer = true, CancellationToken cancellationToken = default) + { + _zlibStream.Write([0]); + + var dataSlice = buffer.Span; + while (dataSlice.Length > FlushThreshold) + { + _zlibStream.Write(dataSlice[..FlushThreshold]); + _zlibStream.Flush(); + dataSlice = dataSlice[FlushThreshold..]; + if(_memoryStream.Length >= BufferSize) + Flush(cancellationToken); + } + + if (dataSlice.Length > 0) + { + _zlibStream.Write(dataSlice); + _zlibStream.Flush(); + _shouldFlush = true; + } + + if(disposeBuffer) buffer.Dispose(); + } + + private void Flush(CancellationToken cancellationToken) + { + _zlibStream.Flush(); + var dataSize = (int)(_memoryStream.Length - 8); + + _memoryStream.Write("\0\0\0\0"u8); + + _memoryStream.Position = 4; + _memoryStream.Write("IDAT"u8); + + var buffer = _memoryStream.GetBuffer(); + BinaryPrimitives.WriteInt32BigEndian(buffer, dataSize); + + // write Crc + var crc = Crc32.Compute(buffer.AsSpan(4, dataSize + 4)); + BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(dataSize + 8), crc); + + _outputPipe.Write(buffer.AsSpan(0, dataSize + 12)); + _memoryStream.SetLength(8); + _memoryStream.Position = 8; + _shouldFlush = false; + } + + public void WriteEndOfFile(CancellationToken cancellationToken = default) + { + if(_shouldFlush) + Flush(cancellationToken); + + Span endChunk = [ + 0x00, 0x00, 0x00, 0x00, // Length + 0x49, 0x45, 0x4E, 0x44, // IEND + 0xAE, 0x42, 0x60, 0x82, // Crc + ]; + + _outputPipe.Write(endChunk); + Dispose(); + } + + public void Dispose() + { + if (!_disposed) + { + _zlibStream.Dispose(); + _memoryStream.Dispose(); + _disposed = true; + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/Infra/ImageIntegral.cs b/Infra/ImageIntegral.cs index 49a8d49..f3d20e4 100644 --- a/Infra/ImageIntegral.cs +++ b/Infra/ImageIntegral.cs @@ -20,7 +20,7 @@ public class ImageIntegral : IDisposable private IMemoryOwner? _rowLocks; private MemoryMappedFile? _memoryMappedFile; - private readonly object _lock = new(); + private readonly Lock _lock = new(); private readonly ManualResetEventSlim _queueLock = new(true); private readonly ManualResetEventSlim _initializationLock = new(false); @@ -249,7 +249,7 @@ public class ImageIntegral : IDisposable view.DangerousReadSpan(0, buffer, 0, _width); } - private void ReadRow(int row, IBuffer buffer) + private void ReadRow(int row, ArrayOwner buffer) { using var view = AcquireView(row, MemoryMappedFileAccess.Read); view.DangerousReadSpan(0, buffer.Span, 0, _width); diff --git a/Infra/StitchATon2.Infra.csproj b/Infra/StitchATon2.Infra.csproj index 6e6dca5..8a8bbb2 100644 --- a/Infra/StitchATon2.Infra.csproj +++ b/Infra/StitchATon2.Infra.csproj @@ -1,14 +1,14 @@  - net8.0 + net9.0 enable enable true - + diff --git a/Infra/Synchronization/TaskHelper.cs b/Infra/Synchronization/TaskHelper.cs index aa10870..594c9c0 100644 --- a/Infra/Synchronization/TaskHelper.cs +++ b/Infra/Synchronization/TaskHelper.cs @@ -2,11 +2,7 @@ namespace StitchATon2.Infra.Synchronization; public static class TaskHelper { - public static TaskFactory CreateTaskFactory() - { - return new TaskFactory( - TaskCreationOptions.AttachedToParent, - TaskContinuationOptions.ExecuteSynchronously - ); - } + public static readonly TaskFactory SynchronizedTaskFactory = new( + TaskCreationOptions.LongRunning, + TaskContinuationOptions.ExecuteSynchronously); } \ No newline at end of file diff --git a/Infra/Utils.cs b/Infra/Utils.cs index a67462c..4ca34c3 100644 --- a/Infra/Utils.cs +++ b/Infra/Utils.cs @@ -7,19 +7,13 @@ namespace StitchATon2.Infra; public static class Utils { - [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_buffer")] - private static extern ref SafeBuffer GetSafeBuffer(this UnmanagedMemoryAccessor view); - - [UnsafeAccessor(UnsafeAccessorKind.Field, Name = "_offset")] - private static extern ref long GetOffset(this UnmanagedMemoryAccessor view); - private static unsafe uint AlignedSizeOf() where T : unmanaged { uint size = (uint)sizeof(T); return size is 1 or 2 ? size : (uint)((size + 3) & (~3)); } - public static void DangerousReadSpan(this UnmanagedMemoryAccessor view, long position, Span span, int offset, int count) + internal static void DangerousReadSpan(this MemoryMappedViewAccessor view, long position, Span span, int offset, int count) where T : unmanaged { uint sizeOfT = AlignedSizeOf(); @@ -38,8 +32,8 @@ public static class Utils } } - var byteOffset = (ulong)(view.GetOffset() + position); - view.GetSafeBuffer().ReadSpan(byteOffset, span.Slice(offset, n)); + var byteOffset = (ulong)(view.PointerOffset + position); + view.SafeMemoryMappedViewHandle.ReadSpan(byteOffset, span.Slice(offset, n)); } public static ArrayOwner Clone(this ArrayOwner arrayOwner, int length) where T : unmanaged @@ -48,9 +42,4 @@ public static class Utils Array.Copy(arrayOwner.Array, 0, newArrayOwner.Array, 0, length); return newArrayOwner; } - - public static void CopyTo(this ArrayOwner arrayOwner, ArrayOwner target, int length) where T : unmanaged - { - Array.Copy(arrayOwner.Array, 0, target.Array, 0, length); - } } \ No newline at end of file diff --git a/global.json b/global.json index 2ddda36..93681ff 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.0", + "version": "9.0.0", "rollForward": "latestMinor", "allowPrerelease": false } -- 2.49.0 From 741d34a5e00a88c9dfc381d773d4c1b7f7068ac2 Mon Sep 17 00:00:00 2001 From: dennisarfan Date: Thu, 31 Jul 2025 07:40:28 +0700 Subject: [PATCH 2/4] dangerous release (possibly memory leak and deadlock) --- App/Controllers/ImageController.cs | 17 ++- App/Utils.cs | 8 +- Domain/ImageCreators/DangerousImageCreator.cs | 22 +-- Infra/Buffers/ArrayOwner.cs | 7 +- Infra/Buffers/IBuffer.cs | 2 + Infra/Buffers/MemoryAllocator.cs | 26 ++++ Infra/Buffers/UnmanagedMemory.cs | 17 +-- Infra/Encoders/PngPipeEncoder.cs | 33 +++-- Infra/ImageIntegral.cs | 132 +++++++++--------- Infra/Utils.cs | 15 +- 10 files changed, 164 insertions(+), 115 deletions(-) diff --git a/App/Controllers/ImageController.cs b/App/Controllers/ImageController.cs index d2fda76..e04cfb0 100644 --- a/App/Controllers/ImageController.cs +++ b/App/Controllers/ImageController.cs @@ -6,7 +6,11 @@ namespace StitchATon2.App.Controllers; public static class ImageController { - public static async Task GenerateImage(HttpResponse response, GenerateImageDto dto, TileManager tileManager) + public static async Task GenerateImage( + HttpResponse response, + GenerateImageDto dto, + TileManager tileManager, + CancellationToken cancellationToken) { if (dto.GetErrors() is { Count: > 0 } errors) { @@ -14,7 +18,7 @@ public static class ImageController response.ContentType = "text/json"; var errorBody = JsonSerializer.Serialize(errors, AppJsonSerializerContext.Default.DictionaryStringListString); response.ContentLength = errorBody.Length; - await response.WriteAsync(errorBody); + await response.WriteAsync(errorBody, cancellationToken: cancellationToken); await response.CompleteAsync(); return; } @@ -24,12 +28,15 @@ public static class ImageController await tileManager .CreateSection(dto) - .DangerousWriteToPipe(response.BodyWriter, dto.OutputScale); + .DangerousWriteToPipe(response.BodyWriter, dto.OutputScale, cancellationToken); await response.CompleteAsync(); } - public static async Task GenerateRandomImage(HttpResponse response, TileManager tileManager) + public static async Task GenerateRandomImage( + HttpResponse response, + TileManager tileManager, + CancellationToken cancellationToken) { response.StatusCode = 200; response.ContentType = "image/png"; @@ -47,7 +54,7 @@ public static class ImageController var scale = float.Clamp(480f / int.Max(section.Width, section.Height), 0.01f, 1f); Console.WriteLine($"Generate random image for {coordinatePair} scale: {scale}"); - await section.DangerousWriteToPipe(response.BodyWriter, scale); + await section.DangerousWriteToPipe(response.BodyWriter, scale, cancellationToken); await response.CompleteAsync(); } } \ No newline at end of file diff --git a/App/Utils.cs b/App/Utils.cs index 2b96e01..edc1087 100644 --- a/App/Utils.cs +++ b/App/Utils.cs @@ -21,9 +21,13 @@ public static class Utils await imageCreator.WriteToStream(stream, scale!.Value); } - public static async Task DangerousWriteToPipe(this GridSection section, PipeWriter pipeWriter, float? scale) + public static async Task DangerousWriteToPipe( + this GridSection section, + PipeWriter pipeWriter, + float? scale, + CancellationToken cancellationToken = default) { var imageCreator = new DangerousImageCreator(section); - await imageCreator.WriteToPipe(pipeWriter, scale!.Value); + await imageCreator.WriteToPipe(pipeWriter, scale!.Value, cancellationToken); } } \ No newline at end of file diff --git a/Domain/ImageCreators/DangerousImageCreator.cs b/Domain/ImageCreators/DangerousImageCreator.cs index 2346479..ebf6441 100644 --- a/Domain/ImageCreators/DangerousImageCreator.cs +++ b/Domain/ImageCreators/DangerousImageCreator.cs @@ -57,6 +57,12 @@ public sealed class DangerousImageCreator : IDisposable using var yEndMap = MemoryAllocator.Allocate(targetWidth); var yStart = OffsetY; + + var pxInt32 = Int32Pixel.Zero; + ref var px = ref pxInt32; + ref var rChannel = ref Unsafe.As(ref px); + ref var gChannel = ref Unsafe.Add(ref rChannel, 4); + ref var bChannel = ref Unsafe.Add(ref rChannel, 8); var outputTaskQueue = TaskHelper.SynchronizedTaskFactory.StartNew(() => { }, cancellationToken); for (var y = 0; y < targetHeight; y++) @@ -75,15 +81,10 @@ public sealed class DangerousImageCreator : IDisposable } int xStart = OffsetX, x0 = 0; - - var pxInt32 = Int32Pixel.Zero; - ref var px = ref pxInt32; - ref var rChannel = ref Unsafe.As(ref px); - ref var gChannel = ref Unsafe.Add(ref rChannel, 4); - ref var bChannel = ref Unsafe.Add(ref rChannel, 8); var outputBuffer = MemoryAllocator.Allocate(outputBufferSize); ref var outputChannel = ref outputBuffer.Span[0]; + var boxHeight = yEnd - yStart; for (int x1 = 0; x1 < targetWidth; x1++) { var xEnd = xLookup[x1]; @@ -92,7 +93,7 @@ public sealed class DangerousImageCreator : IDisposable px += yStartMap[x0]; px -= yEndMap[x0]; px -= yStartMap[x1]; - px /= Math.Max(1, (xEnd - xStart) * (yEnd - yStart)); + px /= Math.Max(1, (xEnd - xStart) * boxHeight); outputChannel = rChannel; outputChannel = ref Unsafe.Add(ref outputChannel, 1); @@ -108,13 +109,16 @@ public sealed class DangerousImageCreator : IDisposable } outputTaskQueue = outputTaskQueue - .ContinueWith(_ => encoder.WriteData(outputBuffer, cancellationToken: cancellationToken), cancellationToken); + .ContinueWith(async _ => + { + await encoder.WriteDataAsync(outputBuffer, cancellationToken: cancellationToken); + }, cancellationToken); yStart = yEnd; } await outputTaskQueue; - encoder.WriteEndOfFile(cancellationToken); + await encoder.WriteEndOfFileAsync(cancellationToken); } private void MapRow( diff --git a/Infra/Buffers/ArrayOwner.cs b/Infra/Buffers/ArrayOwner.cs index 2c17cb5..f1e30fb 100644 --- a/Infra/Buffers/ArrayOwner.cs +++ b/Infra/Buffers/ArrayOwner.cs @@ -7,10 +7,11 @@ public class ArrayOwner : IBuffer where T : unmanaged private readonly ArrayPool _owner; private readonly T[] _buffer; - public ArrayOwner(ArrayPool owner, int size) + public ArrayOwner(ArrayPool owner, int length) { _owner = owner; - _buffer = owner.Rent(size); + _buffer = owner.Rent(length); + Length = length; } ~ArrayOwner() => Dispose(); @@ -26,4 +27,6 @@ public class ArrayOwner : IBuffer where T : unmanaged public Span Span => _buffer; public T[] Array => _buffer; + + public int Length { get; } } \ No newline at end of file diff --git a/Infra/Buffers/IBuffer.cs b/Infra/Buffers/IBuffer.cs index 2eff74b..de13308 100644 --- a/Infra/Buffers/IBuffer.cs +++ b/Infra/Buffers/IBuffer.cs @@ -5,4 +5,6 @@ public interface IBuffer : IDisposable where T : unmanaged ref T this[int index] { get; } Span Span { get; } + + int Length { get; } } \ No newline at end of file diff --git a/Infra/Buffers/MemoryAllocator.cs b/Infra/Buffers/MemoryAllocator.cs index 75787f0..0d40d63 100644 --- a/Infra/Buffers/MemoryAllocator.cs +++ b/Infra/Buffers/MemoryAllocator.cs @@ -1,4 +1,5 @@ using System.Buffers; +using System.Runtime.CompilerServices; namespace StitchATon2.Infra.Buffers; @@ -15,4 +16,29 @@ public static class MemoryAllocator public static MemoryManager AllocateImmovable(int count) where T : unmanaged => new ImmovableMemory(count); + + public static unsafe IBuffer Clone(this IBuffer buffer) where T : unmanaged + { + if (buffer is UnmanagedMemory unmanagedMemory) + { + var newBuffer = new UnmanagedMemory(buffer.Length); + var byteCount = (uint)(Unsafe.SizeOf() * buffer.Length); + Unsafe.CopyBlock(newBuffer.Pointer, unmanagedMemory.Pointer, byteCount); + return newBuffer; + } + + throw new NotSupportedException(); + } + + public static unsafe void Copy(this IBuffer source, IBuffer destination, int count) where T : unmanaged + { + if (source is UnmanagedMemory sourceBuffer && destination is UnmanagedMemory destinationBuffer) + { + var byteCount = (uint)(Unsafe.SizeOf() * count); + Unsafe.CopyBlock(destinationBuffer.Pointer, sourceBuffer.Pointer, byteCount); + return; + } + + throw new NotSupportedException(); + } } \ No newline at end of file diff --git a/Infra/Buffers/UnmanagedMemory.cs b/Infra/Buffers/UnmanagedMemory.cs index 7f61b3d..6b97f1d 100644 --- a/Infra/Buffers/UnmanagedMemory.cs +++ b/Infra/Buffers/UnmanagedMemory.cs @@ -9,18 +9,19 @@ namespace StitchATon2.Infra.Buffers; /// internal sealed unsafe class UnmanagedMemory : IBuffer where T : unmanaged { - private readonly T* _pointer; - private readonly int _count; + internal readonly T* Pointer; private bool _disposed; + + public int Length { get; } - public ref T this[int index] => ref Unsafe.AsRef(_pointer + index); + public ref T this[int index] => ref Unsafe.AsRef(Pointer + index); - public Span Span => new(_pointer, _count); + public Span Span => new(Pointer, Length); - public UnmanagedMemory(int count) + public UnmanagedMemory(int length) { - _pointer = (T*)NativeMemory.Alloc((nuint)count, (nuint)Unsafe.SizeOf()); - _count = count; + Pointer = (T*)NativeMemory.Alloc((nuint)length, (nuint)Unsafe.SizeOf()); + Length = length; } ~UnmanagedMemory() => Dispose(); @@ -29,7 +30,7 @@ internal sealed unsafe class UnmanagedMemory : IBuffer where T : unmanaged { if (!_disposed) { - NativeMemory.Free(_pointer); + NativeMemory.Free(Pointer); GC.SuppressFinalize(this); _disposed = true; } diff --git a/Infra/Encoders/PngPipeEncoder.cs b/Infra/Encoders/PngPipeEncoder.cs index 8aef896..965602d 100644 --- a/Infra/Encoders/PngPipeEncoder.cs +++ b/Infra/Encoders/PngPipeEncoder.cs @@ -10,6 +10,7 @@ public class PngPipeEncoder : IDisposable { private const int BufferSize = 8 * 1024; private const int FlushThreshold = 1024; + private const int PipeChunkThreshold = 16 * 1024; private readonly PipeWriter _outputPipe; private readonly MemoryStream _memoryStream; @@ -60,33 +61,33 @@ public class PngPipeEncoder : IDisposable _outputPipe.Write(headerBytes); } - public void WriteData(IBuffer buffer, bool disposeBuffer = true, CancellationToken cancellationToken = default) + public async Task WriteDataAsync(IBuffer buffer, bool disposeBuffer = true, CancellationToken cancellationToken = default) { _zlibStream.Write([0]); - var dataSlice = buffer.Span; - while (dataSlice.Length > FlushThreshold) + var offset = 0; + while (buffer.Length - offset > FlushThreshold) { - _zlibStream.Write(dataSlice[..FlushThreshold]); - _zlibStream.Flush(); - dataSlice = dataSlice[FlushThreshold..]; - if(_memoryStream.Length >= BufferSize) - Flush(cancellationToken); + _zlibStream.Write(buffer.Span.Slice(offset, FlushThreshold)); + await _zlibStream.FlushAsync(cancellationToken); + offset += FlushThreshold; + if(_outputPipe.UnflushedBytes >= PipeChunkThreshold) + await FlushAsync(cancellationToken); } - if (dataSlice.Length > 0) + if (buffer.Length > offset) { - _zlibStream.Write(dataSlice); - _zlibStream.Flush(); + _zlibStream.Write(buffer.Span[offset..]); + await _zlibStream.FlushAsync(cancellationToken); _shouldFlush = true; } if(disposeBuffer) buffer.Dispose(); } - private void Flush(CancellationToken cancellationToken) + private async Task FlushAsync(CancellationToken cancellationToken) { - _zlibStream.Flush(); + await _zlibStream.FlushAsync(cancellationToken); var dataSize = (int)(_memoryStream.Length - 8); _memoryStream.Write("\0\0\0\0"u8); @@ -102,15 +103,17 @@ public class PngPipeEncoder : IDisposable BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(dataSize + 8), crc); _outputPipe.Write(buffer.AsSpan(0, dataSize + 12)); + await _outputPipe.FlushAsync(cancellationToken); + _memoryStream.SetLength(8); _memoryStream.Position = 8; _shouldFlush = false; } - public void WriteEndOfFile(CancellationToken cancellationToken = default) + public async Task WriteEndOfFileAsync(CancellationToken cancellationToken = default) { if(_shouldFlush) - Flush(cancellationToken); + await FlushAsync(cancellationToken); Span endChunk = [ 0x00, 0x00, 0x00, 0x00, // Length diff --git a/Infra/ImageIntegral.cs b/Infra/ImageIntegral.cs index f3d20e4..8819ea2 100644 --- a/Infra/ImageIntegral.cs +++ b/Infra/ImageIntegral.cs @@ -6,6 +6,7 @@ using SixLabors.ImageSharp.Formats; using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.PixelFormats; using StitchATon2.Infra.Buffers; +using StitchATon2.Infra.Synchronization; namespace StitchATon2.Infra; @@ -58,7 +59,7 @@ public class ImageIntegral : IDisposable { if (_memoryMappedFile is null) { - Task.Factory.StartNew(() => Initialize(cancellationToken), cancellationToken); + Task.Run(() => Initialize(cancellationToken), cancellationToken); _initializationLock.Wait(cancellationToken); _initializationLock.Dispose(); } @@ -86,8 +87,8 @@ public class ImageIntegral : IDisposable } var taskQueue = backedFileStream == null - ? Task.CompletedTask - : AllocateBackedFile(backedFileStream, header); + ? TaskHelper.SynchronizedTaskFactory.StartNew(() => { }, cancellationToken) + : AllocateBackedFile(backedFileStream, header, cancellationToken); taskQueue = taskQueue.ContinueWith( _ => @@ -129,7 +130,7 @@ public class ImageIntegral : IDisposable var imageBuffer = image.Frames.RootFrame.PixelBuffer; var accumulator = Int32Pixel.Zero; - var buffer = MemoryAllocator.AllocateArray(_width); + var buffer = MemoryAllocator.Allocate(_width); var processedRows = _processedRows; Interlocked.Exchange(ref _queueCounter, 0); @@ -143,50 +144,55 @@ public class ImageIntegral : IDisposable buffer[x] = accumulator; } - taskQueue = QueueWriterTask(taskQueue, 0, buffer.Clone(_width), cancellationToken); + taskQueue = QueueWriterTask(taskQueue, 0, buffer.Clone(), cancellationToken); processedRows++; } else { - ReadRow(processedRows - 1, buffer); + ReadRow(processedRows - 1, buffer.Span); } if(cancellationToken.IsCancellationRequested) return; var prevBuffer = buffer; - buffer = MemoryAllocator.AllocateArray(_width); - - for (int y = processedRows; y < image.Height; y++) + buffer = MemoryAllocator.Allocate(_width); + try { - var sourceRow = imageBuffer.DangerousGetRowSpan(y); - accumulator = (Int32Pixel)sourceRow[0]; - buffer[0] = accumulator + prevBuffer[0]; - - // Process all other columns - for (var x = 1; x < sourceRow.Length; x++) + for (int y = processedRows; y < image.Height; y++) { - accumulator.Accumulate(sourceRow[x]); - buffer[x] = accumulator + prevBuffer[x]; + var sourceRow = imageBuffer.DangerousGetRowSpan(y); + accumulator = (Int32Pixel)sourceRow[0]; + buffer[0] = accumulator + prevBuffer[0]; + + // Process all other columns + for (var x = 1; x < sourceRow.Length; x++) + { + accumulator.Accumulate(sourceRow[x]); + buffer[x] = accumulator + prevBuffer[x]; + } + + if (_queueCounter >= MaxProcessingQueue) + { + _queueLock.Reset(); + _queueLock.Wait(cancellationToken); + } + + if(cancellationToken.IsCancellationRequested) + break; + + var writeBuffer = prevBuffer; + buffer.Copy(writeBuffer, _width); + taskQueue = QueueWriterTask(taskQueue, y, writeBuffer, cancellationToken); + prevBuffer = buffer; + buffer = MemoryAllocator.Allocate(_width); } - - if (_queueCounter >= MaxProcessingQueue) - { - _queueLock.Reset(); - _queueLock.Wait(cancellationToken); - } - - if(cancellationToken.IsCancellationRequested) - break; - - var writeBuffer = prevBuffer; - Array.Copy(buffer.Array, writeBuffer.Array, image.Width); - taskQueue = QueueWriterTask(taskQueue, y, writeBuffer, cancellationToken); - prevBuffer = buffer; - buffer = MemoryAllocator.AllocateArray(_width); + } + finally + { + buffer.Dispose(); } - buffer.Dispose(); if(cancellationToken.IsCancellationRequested) return; @@ -205,7 +211,7 @@ public class ImageIntegral : IDisposable private Task QueueWriterTask( Task taskQueue, int row, - ArrayOwner writeBuffer, + IBuffer writeBuffer, CancellationToken cancellationToken) { Interlocked.Increment(ref _queueCounter); @@ -213,7 +219,7 @@ public class ImageIntegral : IDisposable return taskQueue.ContinueWith(_ => { using (var view = AcquireView(row, MemoryMappedFileAccess.Write)) - view.WriteArray(0, writeBuffer.Array, 0, _width); + view.DangerousWriteSpan(0, writeBuffer.Span, 0, _width); writeBuffer.Dispose(); _rowLocks!.Memory.Span[row].Set(); @@ -249,12 +255,6 @@ public class ImageIntegral : IDisposable view.DangerousReadSpan(0, buffer, 0, _width); } - private void ReadRow(int row, ArrayOwner buffer) - { - using var view = AcquireView(row, MemoryMappedFileAccess.Read); - view.DangerousReadSpan(0, buffer.Span, 0, _width); - } - private FileStream? InitializeBackedFile(string path, out Header header) { var expectedHeader = Header.CreateInitial(_width, _height); @@ -311,33 +311,35 @@ public class ImageIntegral : IDisposable return fs; } - private static async Task AllocateBackedFile(FileStream fileStream, Header header) + private static Task AllocateBackedFile(FileStream fileStream, Header header, CancellationToken cancellationToken) { - // The input filestream is expected to be empty with - // initial cursor at the beginning of the file and the content - // is pre-allocated for at least Header.Length bytes - // No other process should be accessed the file while being - // allocated. - // Allocated bytes is not necessary to be zeroed. + return TaskHelper.SynchronizedTaskFactory.StartNew(() => + { + // The input filestream is expected to be empty with + // initial cursor at the beginning of the file and the content + // is pre-allocated for at least Header.Length bytes + // No other process should be accessed the file while being + // allocated. + // Allocated bytes is not necessary to be zeroed. - // const int writeBufferSize = 4 * 1024; - // using var writeBuffer = MemoryPool.Shared.Rent(writeBufferSize); - // - // var written = 0; - // while (written + writeBufferSize < header.Length) - // { - // await fileStream.WriteAsync(writeBuffer.Memory, cancellationToken); - // written += writeBufferSize; - // } - // - // if (written < header.Length) - // { - // await fileStream.WriteAsync(writeBuffer.Memory[..(header.Length - written)], cancellationToken); - // } + // const int writeBufferSize = 4 * 1024; + // using var writeBuffer = MemoryPool.Shared.Rent(writeBufferSize); + // + // var written = 0; + // while (written + writeBufferSize < header.Length) + // { + // await fileStream.WriteAsync(writeBuffer.Memory, cancellationToken); + // written += writeBufferSize; + // } + // + // if (written < header.Length) + // { + // await fileStream.WriteAsync(writeBuffer.Memory[..(header.Length - written)], cancellationToken); + // } - fileStream.SetLength(header.Length + Header.Size); - - await fileStream.DisposeAsync(); + fileStream.SetLength(header.Length + Header.Size); + fileStream.Dispose(); + }, cancellationToken); } [StructLayout(LayoutKind.Sequential)] diff --git a/Infra/Utils.cs b/Infra/Utils.cs index 4ca34c3..3b92da5 100644 --- a/Infra/Utils.cs +++ b/Infra/Utils.cs @@ -1,16 +1,13 @@ using System.IO.MemoryMappedFiles; -using System.Runtime.CompilerServices; -using System.Runtime.InteropServices; -using StitchATon2.Infra.Buffers; namespace StitchATon2.Infra; -public static class Utils +internal static class Utils { private static unsafe uint AlignedSizeOf() where T : unmanaged { uint size = (uint)sizeof(T); - return size is 1 or 2 ? size : (uint)((size + 3) & (~3)); + return size is 1 or 2 ? size : (uint)((size + 3) & ~3); } internal static void DangerousReadSpan(this MemoryMappedViewAccessor view, long position, Span span, int offset, int count) @@ -36,10 +33,10 @@ public static class Utils view.SafeMemoryMappedViewHandle.ReadSpan(byteOffset, span.Slice(offset, n)); } - public static ArrayOwner Clone(this ArrayOwner arrayOwner, int length) where T : unmanaged + internal static void DangerousWriteSpan(this MemoryMappedViewAccessor view, long position, Span span, int offset, int count) + where T : unmanaged { - var newArrayOwner = MemoryAllocator.AllocateArray(length); - Array.Copy(arrayOwner.Array, 0, newArrayOwner.Array, 0, length); - return newArrayOwner; + var byteOffset = (ulong)(view.PointerOffset + position); + view.SafeMemoryMappedViewHandle.WriteSpan(byteOffset, span.Slice(offset, count)); } } \ No newline at end of file -- 2.49.0 From 0472bfe58e9496f6bf314bcd7ba415f2275e10b7 Mon Sep 17 00:00:00 2001 From: dennisarfan Date: Fri, 1 Aug 2025 09:51:39 +0700 Subject: [PATCH 3/4] solve edge case --- App/Controllers/ImageController.cs | 2 + App/Models/GenerateImageDto.cs | 5 + App/Program.cs | 2 +- Domain/GridSection.cs | 5 + Domain/ImageCreators/DangerousImageCreator.cs | 261 ++++++++++++++++-- Domain/TileManager.cs | 21 +- Domain/Utils.cs | 49 +++- Infra/Buffers/PooledMemoryStream.cs | 102 +++++++ Infra/Encoders/Crc32.cs | 11 + Infra/Encoders/PngPipeEncoder.cs | 2 +- Infra/Encoders/UnsafePngEncoder.cs | 166 +++++++++++ Infra/ImageIntegral.cs | 17 +- Infra/Int32Pixel.cs | 6 + StitchATon2.Benchmark/SingleTileBenchmark.cs | 77 ++++++ StitchATon2.sln | 6 + 15 files changed, 685 insertions(+), 47 deletions(-) create mode 100644 Infra/Buffers/PooledMemoryStream.cs create mode 100644 Infra/Encoders/UnsafePngEncoder.cs create mode 100644 StitchATon2.Benchmark/SingleTileBenchmark.cs diff --git a/App/Controllers/ImageController.cs b/App/Controllers/ImageController.cs index e04cfb0..e723baf 100644 --- a/App/Controllers/ImageController.cs +++ b/App/Controllers/ImageController.cs @@ -25,6 +25,8 @@ public static class ImageController response.StatusCode = 200; response.ContentType = "image/png"; + + Console.WriteLine($"Generate image for {dto}"); await tileManager .CreateSection(dto) diff --git a/App/Models/GenerateImageDto.cs b/App/Models/GenerateImageDto.cs index 85a726d..1d77d3a 100644 --- a/App/Models/GenerateImageDto.cs +++ b/App/Models/GenerateImageDto.cs @@ -69,4 +69,9 @@ public class GenerateImageDto yield return (fieldName, $"{fieldName} must be less than or equal to {max}."); } } + + public override string ToString() + { + return $"CoordinatePair: {CanvasRect}, Crop: [{CropOffset![0]} {CropOffset[1]} {CropSize![0]} {CropSize[1]}], OutputScale: {OutputScale}"; + } } \ No newline at end of file diff --git a/App/Program.cs b/App/Program.cs index 8bec90f..04c5523 100644 --- a/App/Program.cs +++ b/App/Program.cs @@ -5,7 +5,7 @@ using StitchATon2.Domain; var builder = WebApplication.CreateSlimBuilder(args); -using var tileManager = new TileManager(Configuration.Default); +var tileManager = new TileManager(Configuration.Default); builder.Services.AddSingleton(tileManager); builder.Services.ConfigureHttpJsonOptions(options => diff --git a/Domain/GridSection.cs b/Domain/GridSection.cs index 72d1a59..3b9a9ea 100644 --- a/Domain/GridSection.cs +++ b/Domain/GridSection.cs @@ -47,5 +47,10 @@ public class GridSection (var rowOffset, OffsetY) = Math.DivRem(y0, config.Height); Origin = tileManager.GetTile(col0 + columnOffset, row0 + rowOffset); + Console.Write($"Origin: {Origin.Coordinate} ({Origin.Column}, {Origin.Row}) "); + Console.Write($"Tile offset: [{columnOffset} {rowOffset}] "); + Console.Write($"Pixel offset: [{OffsetX} {OffsetY}] "); + Console.Write($"Size: [{Width}x{Height}]"); + Console.WriteLine(); } } \ No newline at end of file diff --git a/Domain/ImageCreators/DangerousImageCreator.cs b/Domain/ImageCreators/DangerousImageCreator.cs index ebf6441..04453b5 100644 --- a/Domain/ImageCreators/DangerousImageCreator.cs +++ b/Domain/ImageCreators/DangerousImageCreator.cs @@ -39,25 +39,35 @@ public sealed class DangerousImageCreator : IDisposable ~DangerousImageCreator() => Dispose(); - public async Task WriteToPipe(PipeWriter outputPipe, float scale, CancellationToken cancellationToken = default) + public async Task WriteToPipe2(PipeWriter outputPipe, float scale, CancellationToken cancellationToken = default) { var scaleFactor = MathF.ReciprocalEstimate(scale); var targetWidth = (int)(Width / scaleFactor); var targetHeight = (int)(Height / scaleFactor); + if (targetHeight == 0 || targetWidth == 0) + return; var encoder = new PngPipeEncoder(outputPipe, targetWidth, targetHeight); encoder.WriteHeader(); var outputBufferSize = targetWidth * Unsafe.SizeOf(); - using var xLookup = Utils.BoundsMatrix(scaleFactor, targetWidth, FullWidth, OffsetX); - using var yLookup = Utils.BoundsMatrix(scaleFactor, targetHeight, FullHeight, OffsetY); + var xLookup = Utils.DoubleBoundsMatrix(scaleFactor, targetWidth, FullWidth, OffsetX); + var yLookup = Utils.DoubleBoundsMatrix(scaleFactor, targetHeight, FullHeight, OffsetY); - using var yStartMap = MemoryAllocator.Allocate(targetWidth); - using var yEndMap = MemoryAllocator.Allocate(targetWidth); + using var xLookup0 = xLookup.Item1; + using var xLookup1 = xLookup.Item2; + using var yLookup0 = yLookup.Item1; + using var yLookup1 = yLookup.Item2; + + using var yStartMap0 = MemoryAllocator.Allocate(targetWidth); + using var yStartMap1 = MemoryAllocator.Allocate(targetWidth); + using var yEndMap0 = MemoryAllocator.Allocate(targetWidth); + using var yEndMap1 = MemoryAllocator.Allocate(targetWidth); - var yStart = OffsetY; - + // var yStart = OffsetY; + + // Use pixel referencing to eliminate type casting var pxInt32 = Int32Pixel.Zero; ref var px = ref pxInt32; ref var rChannel = ref Unsafe.As(ref px); @@ -67,27 +77,151 @@ public sealed class DangerousImageCreator : IDisposable var outputTaskQueue = TaskHelper.SynchronizedTaskFactory.StartNew(() => { }, cancellationToken); for (var y = 0; y < targetHeight; y++) { - var yEnd = yLookup[y]; + var yStart = yLookup0[y]; + var yEnd = yLookup1[y]; var (localRow0, localOffsetY0) = int.DivRem(yStart, TileHeight); - MapRow(localRow0, localOffsetY0, xLookup, targetWidth, yStartMap); + MapRow(localRow0, localOffsetY0, xLookup0, targetWidth, yStartMap0); + MapRow(localRow0, localOffsetY0, xLookup1, targetWidth, yStartMap1); var (localRow1, localOffsetY1) = int.DivRem(yEnd, TileHeight); - MapRow(localRow1, localOffsetY1, xLookup, targetWidth, yEndMap); + MapRow(localRow1, localOffsetY1, xLookup0, targetWidth, yEndMap0); + MapRow(localRow1, localOffsetY1, xLookup1, targetWidth, yEndMap1); if (localRow0 != localRow1) { - MapRow(localRow0, BottomPixelIndex, xLookup, targetWidth, yEndMap, true); + MapRow(localRow0, BottomPixelIndex, xLookup0, targetWidth, yEndMap0, true); + MapRow(localRow0, BottomPixelIndex, xLookup1, targetWidth, yEndMap1, true); } - int xStart = OffsetX, x0 = 0; + // int xStart = OffsetX, x0 = 0; var outputBuffer = MemoryAllocator.Allocate(outputBufferSize); ref var outputChannel = ref outputBuffer.Span[0]; - var boxHeight = yEnd - yStart; - for (int x1 = 0; x1 < targetWidth; x1++) + var boxHeight = Math.Max(1, yEnd - yStart); + for (int x = 0; x < targetWidth; x++) { - var xEnd = xLookup[x1]; + var xStart = xLookup0[x]; + var xEnd = xLookup1[x]; + + px = yEndMap1[x]; + px += yStartMap0[x]; + px -= yEndMap0[x]; + px -= yStartMap1[x]; + px /= Math.Max(1, xEnd - xStart) * boxHeight; + + outputChannel = rChannel; + outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1); + + outputChannel = gChannel; + outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1); + + outputChannel = bChannel; + outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1); + + // xStart = xEnd; + // x0 = x; + } + + outputTaskQueue = outputTaskQueue + .ContinueWith(async _ => + { + await encoder.WriteDataAsync(outputBuffer, cancellationToken: cancellationToken); + }, cancellationToken); + + yStart = yEnd; + } + + await outputTaskQueue; + await encoder.WriteEndOfFileAsync(cancellationToken); + } + + public async Task WriteToPipe(PipeWriter outputPipe, float scale, CancellationToken cancellationToken = default) + { + var scaleFactor = MathF.ReciprocalEstimate(scale); + var targetWidth = (int)(Width / scaleFactor); + var targetHeight = (int)(Height / scaleFactor); + if (targetHeight == 0 || targetWidth == 0) + return; + + var encoder = new PngPipeEncoder(outputPipe, targetWidth, targetHeight); + encoder.WriteHeader(); + + Task outputTaskQueue; + + var outputBufferSize = targetWidth * Unsafe.SizeOf(); + + using var xLookup = Utils.BoundsMatrix(scaleFactor, targetWidth, FullWidth, OffsetX); + using var yLookup = Utils.BoundsMatrix(scaleFactor, targetHeight, FullHeight, OffsetY); + + using var yStartMap = MemoryAllocator.Allocate(targetWidth + 1); + using var yEndMap = MemoryAllocator.Allocate(targetWidth + 1); + // OffsetX-(int)float.Ceiling(scaleFactor) + int yStart = OffsetY, + yEnd = yLookup[0], + xStart = OffsetX, + x0 = 0; + + + + // Use pixel referencing to eliminate type casting + var pxInt32 = Int32Pixel.Zero; + ref var px = ref pxInt32; + ref var rChannel = ref Unsafe.As(ref px); + ref var gChannel = ref Unsafe.Add(ref rChannel, 4); + ref var bChannel = ref Unsafe.Add(ref rChannel, 8); + + // First row + var (localRow0, localOffsetY0) = int.DivRem(yStart, TileHeight); + var (localRow1, localOffsetY1) = int.DivRem(yEnd, TileHeight); + { + switch (localOffsetY0) + { + // Cross row tile, no need to handle if it's first row tile for now + // the provided asset is bordered black anyway + case 0 when TileOrigin.Row > 1: + localOffsetY0 = BottomPixelIndex; + localRow0--; + break; + case > 0: + localOffsetY0--; + break; + } + + MapRow(localRow0, localOffsetY0, xLookup, targetWidth, yStartMap); + MapRow(localRow1, localOffsetY1, xLookup, targetWidth, yEndMap); + + // Cross row + if (localRow0 != localRow1) + MapRow(localRow0, BottomPixelIndex, xLookup, targetWidth, yEndMap, true); + + var outputBuffer = MemoryAllocator.Allocate(outputBufferSize); + ref var outputChannel = ref outputBuffer.Span[0]; + var boxHeight = yEnd - yStart; + + // Render first pixel row + var xEnd = xLookup[0]; + px = yEndMap[0]; + px += yStartMap[^1]; + px -= yEndMap[^1]; + px -= yStartMap[0]; + px /= Math.Max(1, (xEnd - xStart) * boxHeight); + + outputChannel = rChannel; + outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1); + + outputChannel = gChannel; + outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1); + + outputChannel = bChannel; + outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1); + + xStart = xEnd; + + // Render entire pixel row + for (int x1 = 1; x1 < targetWidth; x1++) + { + xEnd = xLookup[x1]; px = yEndMap[x1]; px += yStartMap[x0]; @@ -96,13 +230,83 @@ public sealed class DangerousImageCreator : IDisposable px /= Math.Max(1, (xEnd - xStart) * boxHeight); outputChannel = rChannel; - outputChannel = ref Unsafe.Add(ref outputChannel, 1); + outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1); outputChannel = gChannel; - outputChannel = ref Unsafe.Add(ref outputChannel, 1); + outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1); outputChannel = bChannel; - outputChannel = ref Unsafe.Add(ref outputChannel, 1); + outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1); + + xStart = xEnd; + x0 = x1; + } + + outputTaskQueue = TaskHelper.SynchronizedTaskFactory.StartNew(async _ => + { + await encoder.WriteDataAsync(outputBuffer, cancellationToken: cancellationToken); + }, null, cancellationToken); + + yStart = yEnd; + } + + for (var y = 1; y < targetHeight; y++) + { + yEnd = yLookup[y]; + + (localRow0, localOffsetY0) = int.DivRem(yStart, TileHeight); + MapRow(localRow0, localOffsetY0, xLookup, targetWidth, yStartMap); + + (localRow1, localOffsetY1) = int.DivRem(yEnd, TileHeight); + MapRow(localRow1, localOffsetY1, xLookup, targetWidth, yEndMap); + + // Cross row + if (localRow0 != localRow1) + MapRow(localRow0, BottomPixelIndex, xLookup, targetWidth, yEndMap, true); + + xStart = OffsetX; + x0 = 0; + + var outputBuffer = MemoryAllocator.Allocate(outputBufferSize); + ref var outputChannel = ref outputBuffer.Span[0]; + var boxHeight = yEnd - yStart; + + var xEnd = xLookup[0]; + px = yEndMap[0]; + px += yStartMap[^1]; + px -= yEndMap[^1]; + px -= yStartMap[0]; + px /= Math.Max(1, (xEnd - xStart) * boxHeight); + + outputChannel = rChannel; + outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1); + + outputChannel = gChannel; + outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1); + + outputChannel = bChannel; + outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1); + + xStart = xEnd; + + for (int x1 = 1; x1 < targetWidth; x1++) + { + xEnd = xLookup[x1]; + + px = yEndMap[x1]; + px += yStartMap[x0]; + px -= yEndMap[x0]; + px -= yStartMap[x1]; + px /= Math.Max(1, (xEnd - xStart) * boxHeight); + + outputChannel = rChannel; + outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1); + + outputChannel = gChannel; + outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1); + + outputChannel = bChannel; + outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1); xStart = xEnd; x0 = x1; @@ -136,10 +340,31 @@ public sealed class DangerousImageCreator : IDisposable var written = 0; var destinationSpan = destination.Span; var readBufferSpan = _mmfReadBuffer.Span; + + var negative = sourceMap[0] - 1; + var negativePixel = appendMode ? destinationSpan[^1] : Int32Pixel.Zero; + if (negative >= 0) + { + negativePixel = readBufferSpan[negative]; + } + else if(currentTile.Column > 1) + { + // Cross row tile, no need to handle if it's first column tile for now + // the provided asset is bordered black anyway + TileManager.GetAdjacent(currentTile, -1, 0) + .Integral + .Acquire(yOffset, readBufferSpan); + negativePixel = readBufferSpan[RightmostPixelIndex]; + xAdder = readBufferSpan[RightmostPixelIndex]; + } + + destinationSpan[^1] = negativePixel; + while (true) { currentTile.Integral.Acquire(yOffset, readBufferSpan); int localX; + if (appendMode) { while (written < sourceMap.Length && (localX = sourceMap[written] - xOffset) < TileWidth) diff --git a/Domain/TileManager.cs b/Domain/TileManager.cs index 9bb20b1..0b9864f 100644 --- a/Domain/TileManager.cs +++ b/Domain/TileManager.cs @@ -5,29 +5,26 @@ using StitchATon2.Infra.Buffers; namespace StitchATon2.Domain; -public sealed class TileManager : IDisposable +public sealed class TileManager { - private readonly IMemoryOwner _tiles; + private readonly Tile[] _tiles; public Configuration Configuration { get; } public TileManager(Configuration config) { Configuration = config; - _tiles = MemoryAllocator.AllocateManaged(config.TileCount); - var tilesSpan = _tiles.Memory.Span; + _tiles = new Tile[Configuration.TileCount]; for (var id = 0; id < config.TileCount; id++) - tilesSpan[id] = CreateTile(id); + _tiles[id] = CreateTile(id); Console.WriteLine("Tile manager created"); } - - ~TileManager() => Dispose(); private Tile CreateTile(int id) { var (row, column) = int.DivRem(id, Configuration.Columns); - var coordinate = $"{Utils.GetSBSNotation(++row)}{++column}"; + var coordinate = $"{Utils.GetSBSNotationRow(++row)}{++column}"; return new Tile { Id = id, @@ -47,7 +44,7 @@ public sealed class TileManager : IDisposable private int GetId(int column, int row) => column - 1 + (row - 1) * Configuration.Columns; [MethodImpl(MethodImplOptions.AggressiveInlining)] - public Tile GetTile(int id) => _tiles.Memory.Span[id]; + public Tile GetTile(int id) => _tiles[id]; [MethodImpl(MethodImplOptions.AggressiveInlining)] public Tile GetTile(int column, int row) => GetTile(GetId(column, row)); @@ -99,10 +96,4 @@ public sealed class TileManager : IDisposable cropY, cropWidth, cropHeight); - - public void Dispose() - { - _tiles.Dispose(); - GC.SuppressFinalize(this); - } } \ No newline at end of file diff --git a/Domain/Utils.cs b/Domain/Utils.cs index 4cde46b..63d56c0 100644 --- a/Domain/Utils.cs +++ b/Domain/Utils.cs @@ -9,7 +9,7 @@ namespace StitchATon2.Domain; public static class Utils { [Pure] - public static string GetSBSNotation(int row) + public static string GetSBSNotationRow(int row) => row <= 26 ? new string([(char)(row + 'A' - 1)]) : new string(['A', (char)(row + 'A' - 27)]); @@ -70,7 +70,54 @@ public static class Utils resultSpan[i] = Vector.Add(resultSpan[i], vectorOffset); resultSpan[i] = Vector.ClampNative(resultSpan[i], vectorMin, vectorMax); } + + var negative = float.Ceiling(scaleFactor); return result; } + + public static (IBuffer, IBuffer) DoubleBoundsMatrix(float scaleFactor, int length, int max, int offset) + { + var vectorSize = DivCeil(length, Vector.Count); + using var startBuffer = MemoryAllocator.Allocate>(vectorSize); + using var endBuffer = MemoryAllocator.Allocate>(vectorSize); + + var startSpan = startBuffer.Span; + var endSpan = endBuffer.Span; + + var vectorMin = Vector.Zero; + var vectorOne = Vector.One; + var vectorMax = Vector.Create(max); + var vectorScale = Vector.Create(scaleFactor); + var vectorOffset = new Vector(offset - 1); + + for (int i = 0, seq = 0; i < vectorSize; i++, seq += Vector.Count) + { + startSpan[i] = Vector.CreateSequence(seq, 1f); + startSpan[i] = Vector.Multiply(startSpan[i], vectorScale); + endSpan[i] = Vector.Add(vectorScale, startSpan[i]); + endSpan[i] = Vector.Ceiling(endSpan[i]); + } + + var resultStart = MemoryAllocator.Allocate(vectorSize * Vector.Count); + var resultEnd = MemoryAllocator.Allocate(vectorSize * Vector.Count); + + var resultStartSpan = MemoryMarshal.Cast>(resultStart.Span); + var resultEndSpan = MemoryMarshal.Cast>(resultEnd.Span); + + for (var i = 0; i < vectorSize; i++) + { + resultStartSpan[i] = Vector.ConvertToInt32(startSpan[i]); + resultStartSpan[i] = Vector.Subtract(resultStartSpan[i], vectorOne); + resultStartSpan[i] = Vector.Add(resultStartSpan[i], vectorOffset); + resultStartSpan[i] = Vector.Clamp(resultStartSpan[i], vectorMin, vectorMax); + + resultEndSpan[i] = Vector.ConvertToInt32(endSpan[i]); + resultEndSpan[i] = Vector.Subtract(resultEndSpan[i], vectorOne); + resultEndSpan[i] = Vector.Add(resultEndSpan[i], vectorOffset); + resultEndSpan[i] = Vector.Clamp(resultEndSpan[i], vectorMin, vectorMax); + } + + return (resultStart, resultEnd); + } } \ No newline at end of file diff --git a/Infra/Buffers/PooledMemoryStream.cs b/Infra/Buffers/PooledMemoryStream.cs new file mode 100644 index 0000000..89adb83 --- /dev/null +++ b/Infra/Buffers/PooledMemoryStream.cs @@ -0,0 +1,102 @@ +using System.Buffers; + +namespace StitchATon2.Infra.Buffers; + +public class PooledMemoryStream : Stream +{ + private byte[] _buffer; + private int _length; + private int _position; + private readonly ArrayPool _pool; + private bool _disposed; + + public PooledMemoryStream(int initialCapacity = 1024, ArrayPool? pool = null) + { + _pool = pool ?? ArrayPool.Shared; + _buffer = _pool.Rent(initialCapacity); + } + + public override bool CanRead => !_disposed; + public override bool CanSeek => !_disposed; + public override bool CanWrite => !_disposed; + public override long Length => _length; + public override long Position + { + get => _position; + set + { + if (_disposed) throw new ObjectDisposedException(nameof(PooledMemoryStream)); + if (value < 0 || value > int.MaxValue) throw new ArgumentOutOfRangeException(); + _position = (int)value; + } + } + + public byte[] GetBuffer() => _buffer; + public ArraySegment GetWrittenSegment() => new(_buffer, 0, _length); + + public override void Flush() { /* no-op */ } + + public override int Read(byte[] buffer, int offset, int count) + { + if (_disposed) throw new ObjectDisposedException(nameof(PooledMemoryStream)); + int available = _length - _position; + int toRead = Math.Min(count, available); + Buffer.BlockCopy(_buffer, _position, buffer, offset, toRead); + _position += toRead; + return toRead; + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (_disposed) throw new ObjectDisposedException(nameof(PooledMemoryStream)); + EnsureCapacity(_position + count); + Buffer.BlockCopy(buffer, offset, _buffer, _position, count); + _position += count; + _length = Math.Max(_length, _position); + } + + public override long Seek(long offset, SeekOrigin origin) + { + if (_disposed) throw new ObjectDisposedException(nameof(PooledMemoryStream)); + int newPos = origin switch + { + SeekOrigin.Begin => (int)offset, + SeekOrigin.Current => _position + (int)offset, + SeekOrigin.End => _length + (int)offset, + _ => throw new ArgumentOutOfRangeException() + }; + if (newPos < 0) throw new IOException("Negative position"); + _position = newPos; + return _position; + } + + public override void SetLength(long value) + { + if (_disposed) throw new ObjectDisposedException(nameof(PooledMemoryStream)); + if (value < 0 || value > int.MaxValue) throw new ArgumentOutOfRangeException(); + EnsureCapacity((int)value); + _length = (int)value; + if (_position > _length) _position = _length; + } + + private void EnsureCapacity(int size) + { + if (size <= _buffer.Length) return; + int newSize = Math.Max(size, _buffer.Length * 2); + byte[] newBuffer = _pool.Rent(newSize); + Buffer.BlockCopy(_buffer, 0, newBuffer, 0, _length); + _pool.Return(_buffer, clearArray: true); + _buffer = newBuffer; + } + + protected override void Dispose(bool disposing) + { + if (!_disposed) + { + _pool.Return(_buffer, clearArray: true); + _buffer = Array.Empty(); + _disposed = true; + } + base.Dispose(disposing); + } +} \ No newline at end of file diff --git a/Infra/Encoders/Crc32.cs b/Infra/Encoders/Crc32.cs index e3f106a..32c26d0 100644 --- a/Infra/Encoders/Crc32.cs +++ b/Infra/Encoders/Crc32.cs @@ -15,6 +15,17 @@ public static class Crc32 return ~crc; } + public static uint Compute(Stream stream, int count, uint initial = 0xFFFFFFFF) + { + uint crc = initial; + while (count-- > 0) + { + crc = Table[(crc ^ stream.ReadByte()) & 0xFF] ^ (crc >> 8); + } + + return ~crc; + } + private static uint[] GenerateTable() { const uint poly = 0xEDB88320; diff --git a/Infra/Encoders/PngPipeEncoder.cs b/Infra/Encoders/PngPipeEncoder.cs index 965602d..9a2b844 100644 --- a/Infra/Encoders/PngPipeEncoder.cs +++ b/Infra/Encoders/PngPipeEncoder.cs @@ -71,7 +71,7 @@ public class PngPipeEncoder : IDisposable _zlibStream.Write(buffer.Span.Slice(offset, FlushThreshold)); await _zlibStream.FlushAsync(cancellationToken); offset += FlushThreshold; - if(_outputPipe.UnflushedBytes >= PipeChunkThreshold) + if(_memoryStream.Length >= BufferSize) await FlushAsync(cancellationToken); } diff --git a/Infra/Encoders/UnsafePngEncoder.cs b/Infra/Encoders/UnsafePngEncoder.cs new file mode 100644 index 0000000..87d14e0 --- /dev/null +++ b/Infra/Encoders/UnsafePngEncoder.cs @@ -0,0 +1,166 @@ +using System.Buffers; +using System.Buffers.Binary; +using System.IO.Compression; +using System.IO.Pipelines; +using StitchATon2.Infra.Buffers; + +namespace StitchATon2.Infra.Encoders; + +public class UnsafePngEncoder : IDisposable +{ + private const int BufferSize = 8 * 1024; + private const int FlushThreshold = 1024; + private const int PipeChunkThreshold = 16 * 1024; + + private readonly PipeWriter _outputPipe; + private readonly int _width; + private readonly int _height; + + private MemoryHandle? _memoryHandle; + private readonly RawPointerStream _memoryStream;// = new RawPointerStream(); + private ZLibStream? _zlibStream;// = new ZLibStream(_memoryStream, CompressionLevel.Optimal, leaveOpen: true); + private bool _disposed; + private bool _shouldFlush; + + public UnsafePngEncoder(PipeWriter outputPipe, int width, int height) + { + _outputPipe = outputPipe; + _width = width; + _height = height; + + _memoryStream = new RawPointerStream(); + } + + ~UnsafePngEncoder() => Dispose(); + + public void WriteHeader() + { + Span headerBytes = [ + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG Signature + 0x00, 0x00, 0x00, 0x0D, // Length + + // IHDR chunk + 0x49, 0x48, 0x44, 0x52, // IHDR + 0x00, 0x00, 0x00, 0x00, // Reserve to write Width + 0x00, 0x00, 0x00, 0x00, // Reserve to write Height + 0x08, // Bit depth + 0x02, // Color type + 0x00, // Compression method + 0x00, // Filter method + 0x00, // Interlace method + 0x00, 0x00, 0x00, 0x00, // Reserve to write CRC-32 + ]; + + BinaryPrimitives.WriteInt32BigEndian(headerBytes[16..], _width); + BinaryPrimitives.WriteInt32BigEndian(headerBytes[20..], _height); + var crc = Crc32.Compute(headerBytes.Slice(12, 17)); + + BinaryPrimitives.WriteUInt32BigEndian(headerBytes[29..], crc); + + _outputPipe.Write(headerBytes); + } + + private unsafe void Initialize() + { + if (_memoryHandle == null) + { + var memory = _outputPipe.GetMemory(PipeChunkThreshold); + var handle = memory.Pin(); + _memoryStream.Initialize((byte*)handle.Pointer, 0, memory.Length); + _memoryHandle = handle; + + _memoryStream.SetLength(8); + _memoryStream.Position = 8; + _zlibStream = new ZLibStream(_memoryStream, CompressionLevel.Optimal, true); + } + } + + public async Task WriteDataAsync(IBuffer buffer, bool disposeBuffer = true, CancellationToken cancellationToken = default) + { + Initialize(); + _zlibStream!.Write([0]); + + var offset = 0; + while (buffer.Length - offset > FlushThreshold) + { + _zlibStream.Write(buffer.Span.Slice(offset, FlushThreshold)); + await _zlibStream.FlushAsync(cancellationToken); + offset += FlushThreshold; + if(_memoryStream.Length >= BufferSize) + await FlushAsync(cancellationToken); + } + + if (buffer.Length > offset) + { + _zlibStream.Write(buffer.Span[offset..]); + await _zlibStream.FlushAsync(cancellationToken); + _shouldFlush = true; + } + + if(disposeBuffer) buffer.Dispose(); + } + + private async Task FlushAsync(CancellationToken cancellationToken) + { + await _zlibStream!.FlushAsync(cancellationToken); + var dataSize = (int)(_memoryStream.Length - 8); + + _memoryStream.Position = 0; + Span buffer = stackalloc byte[4]; + BinaryPrimitives.WriteInt32BigEndian(buffer, dataSize); + _memoryStream.Write(buffer); + _memoryStream.Write("IDAT"u8); + + _memoryStream.Position = 4; + + // write Crc + var crc = Crc32.Compute(_memoryStream, dataSize + 4); + BinaryPrimitives.WriteUInt32BigEndian(buffer, crc); + _memoryStream.Write(buffer); + + _outputPipe.Advance((int)_memoryStream.Length); + + await _memoryStream.DisposeAsync(); + _memoryHandle!.Value.Dispose(); + _memoryHandle = null; + + _shouldFlush = false; + } + + public async Task WriteEndOfFileAsync(CancellationToken cancellationToken = default) + { + if(_shouldFlush) + await FlushAsync(cancellationToken); + + Span endChunk = [ + 0x00, 0x00, 0x00, 0x00, // Length + 0x49, 0x45, 0x4E, 0x44, // IEND + 0xAE, 0x42, 0x60, 0x82, // Crc + ]; + + _outputPipe.Write(endChunk); + Dispose(); + } + + public void Dispose() + { + if (!_disposed) + { + if (_memoryHandle != null) + { + _zlibStream!.Dispose(); + _memoryStream.Dispose(); + } + _disposed = true; + GC.SuppressFinalize(this); + } + } + + private unsafe class RawPointerStream : UnmanagedMemoryStream + { + public void Initialize(byte* pointer, int length, int capacity) + { + Initialize(pointer, length, capacity, FileAccess.ReadWrite); + } + } +} \ No newline at end of file diff --git a/Infra/ImageIntegral.cs b/Infra/ImageIntegral.cs index 8819ea2..0a884ce 100644 --- a/Infra/ImageIntegral.cs +++ b/Infra/ImageIntegral.cs @@ -1,4 +1,3 @@ -using System.Buffers; using System.IO.MemoryMappedFiles; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -19,7 +18,7 @@ public class ImageIntegral : IDisposable private readonly int _width; private readonly int _height; - private IMemoryOwner? _rowLocks; + private ManualResetEventSlim[]? _rowLocks; private MemoryMappedFile? _memoryMappedFile; private readonly Lock _lock = new(); @@ -66,7 +65,7 @@ public class ImageIntegral : IDisposable } } - _rowLocks?.Memory.Span[row].Wait(cancellationToken); + _rowLocks?[row].Wait(cancellationToken); } private void Initialize(CancellationToken cancellationToken) @@ -101,12 +100,11 @@ public class ImageIntegral : IDisposable // initialize resource gating, all rows is expected to be locked // if the backed file require to allocate, it should be safe to do this // asynchronously - var rowLocks = MemoryAllocator.AllocateManaged(_height); - var rowLocksSpan = rowLocks.Memory.Span; + var rowLocks = new ManualResetEventSlim[_height]; for (int i = 0; i < _height; i++) { var isOpen = i < header.ProcessedRows; - rowLocksSpan[i] = new ManualResetEventSlim(isOpen); + rowLocks[i] = new ManualResetEventSlim(isOpen); } _rowLocks = rowLocks; @@ -222,7 +220,7 @@ public class ImageIntegral : IDisposable view.DangerousWriteSpan(0, writeBuffer.Span, 0, _width); writeBuffer.Dispose(); - _rowLocks!.Memory.Span[row].Set(); + _rowLocks![row].Set(); Interlocked.Increment(ref _processedRows); using (var view = AcquireHeaderView(MemoryMappedFileAccess.Write)) @@ -370,11 +368,8 @@ public class ImageIntegral : IDisposable if (_rowLocks is { } locks) { _rowLocks = null; - var lockSpan = locks.Memory.Span; for(int i = 0; i < _height; i++) - lockSpan[i].Dispose(); - - locks.Dispose(); + locks[i].Dispose(); } } diff --git a/Infra/Int32Pixel.cs b/Infra/Int32Pixel.cs index 836d378..1078a60 100644 --- a/Infra/Int32Pixel.cs +++ b/Infra/Int32Pixel.cs @@ -68,6 +68,12 @@ public record struct Int32Pixel return new Int32Pixel(a.R / b, a.G / b, a.B / b); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Int32Pixel operator /(Int32Pixel a, float b) + { + return new Int32Pixel((byte)(a.R / b), (byte)(a.G / b), (byte)(a.B / b)); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static explicit operator Int32Pixel(Rgb24 pixel) { diff --git a/StitchATon2.Benchmark/SingleTileBenchmark.cs b/StitchATon2.Benchmark/SingleTileBenchmark.cs new file mode 100644 index 0000000..2e806d4 --- /dev/null +++ b/StitchATon2.Benchmark/SingleTileBenchmark.cs @@ -0,0 +1,77 @@ +using System.Net.Http.Json; +using BenchmarkDotNet.Attributes; +using StitchATon2.Domain; + +namespace StitchATon2.Benchmark; + +public class SingleTileBenchmark +{ + private const string Url = "http://localhost:5088/api/image/generate"; + private readonly TileManager _tileManager = new(Configuration.Default); + private readonly Random _random = new(); + private readonly HttpClient _client = new(); + + private string GetRandomCoordinatePair() + { + var maxId = _tileManager.Configuration.Rows + * _tileManager.Configuration.Columns; + + var id = _random.Next(maxId); + var tile = _tileManager.GetTile(id); + return $"{tile.Coordinate}:{tile.Coordinate}"; + } + + private JsonContent GetRandomDto(float scale, float offsetX = 0, float offsetY = 0) + { + return JsonContent.Create(new + { + canvas_rect = GetRandomCoordinatePair(), + crop_offset = (float[]) [offsetX, offsetY], + crop_size = (float[]) [1f - offsetX, 1f - offsetY], + output_scale = scale, + }); + } + + private JsonContent GetRandomDtoWithOffset(float scale) + { + var offsetX = _random.NextSingle(); + var offsetY = _random.NextSingle(); + return GetRandomDto(scale, offsetX, offsetY); + } + + [Benchmark] + public async Task NoScaling() + { + await _client.PostAsync(Url, GetRandomDto(1f)); + } + + [Benchmark] + public async Task ScaleHalf() + { + await _client.PostAsync(Url, GetRandomDto(.5f)); + } + + [Benchmark] + public async Task ScaleQuarter() + { + await _client.PostAsync(Url, GetRandomDto(.25f)); + } + + [Benchmark] + public async Task NoScalingWithOffset() + { + await _client.PostAsync(Url, GetRandomDtoWithOffset(1f)); + } + + [Benchmark] + public async Task ScaleHalfWithOffset() + { + await _client.PostAsync(Url, GetRandomDtoWithOffset(.5f)); + } + + [Benchmark] + public async Task ScaleQuarterWithOffset() + { + await _client.PostAsync(Url, GetRandomDtoWithOffset(.25f)); + } +} \ No newline at end of file diff --git a/StitchATon2.sln b/StitchATon2.sln index 31e5c57..702b5a5 100644 --- a/StitchATon2.sln +++ b/StitchATon2.sln @@ -6,6 +6,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StitchATon2.Domain", "Domai EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StitchATon2.Infra", "Infra\StitchATon2.Infra.csproj", "{E602F3FC-6139-4B30-AC5A-75815E6340A4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StitchATon2.Benchmark", "StitchATon2.Benchmark\StitchATon2.Benchmark.csproj", "{2F9B169C-C799-4489-B864-F912D69C5D3E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -24,5 +26,9 @@ Global {E602F3FC-6139-4B30-AC5A-75815E6340A4}.Debug|Any CPU.Build.0 = Debug|Any CPU {E602F3FC-6139-4B30-AC5A-75815E6340A4}.Release|Any CPU.ActiveCfg = Release|Any CPU {E602F3FC-6139-4B30-AC5A-75815E6340A4}.Release|Any CPU.Build.0 = Release|Any CPU + {2F9B169C-C799-4489-B864-F912D69C5D3E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F9B169C-C799-4489-B864-F912D69C5D3E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F9B169C-C799-4489-B864-F912D69C5D3E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F9B169C-C799-4489-B864-F912D69C5D3E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal -- 2.49.0 From d3dfdd6a7450a318bc9e5af7d49f83d92fefa660 Mon Sep 17 00:00:00 2001 From: Dennis Arfan Date: Fri, 1 Aug 2025 22:13:13 +0700 Subject: [PATCH 4/4] solve 'edge' case and pass cancellation token --- App/Controllers/ImageController.cs | 4 +- App/Utils.cs | 14 +- Domain/Configuration.cs | 9 +- Domain/ImageCreators/DangerousImageCreator.cs | 331 +++++------------- Domain/ImageCreators/ImageCreator.cs | 160 --------- Domain/TileManager.cs | 4 +- Domain/Utils.cs | 57 +-- Infra/Buffers/ArrayOwner.cs | 3 +- Infra/Buffers/IBuffer.cs | 2 + Infra/Buffers/ImmovableMemory.cs | 40 ++- Infra/Buffers/MemoryAllocator.cs | 30 +- Infra/Buffers/UnmanagedMemory.cs | 4 +- Infra/Encoders/PngPipeEncoder.cs | 40 ++- Infra/Encoders/PngStreamEncoder.cs | 55 +-- StitchATon2.sln | 6 + 15 files changed, 208 insertions(+), 551 deletions(-) delete mode 100644 Domain/ImageCreators/ImageCreator.cs diff --git a/App/Controllers/ImageController.cs b/App/Controllers/ImageController.cs index e723baf..3d92b95 100644 --- a/App/Controllers/ImageController.cs +++ b/App/Controllers/ImageController.cs @@ -30,7 +30,7 @@ public static class ImageController await tileManager .CreateSection(dto) - .DangerousWriteToPipe(response.BodyWriter, dto.OutputScale, cancellationToken); + .WriteToPipe(response.BodyWriter, dto.OutputScale, cancellationToken); await response.CompleteAsync(); } @@ -56,7 +56,7 @@ public static class ImageController var scale = float.Clamp(480f / int.Max(section.Width, section.Height), 0.01f, 1f); Console.WriteLine($"Generate random image for {coordinatePair} scale: {scale}"); - await section.DangerousWriteToPipe(response.BodyWriter, scale, cancellationToken); + await section.WriteToPipe(response.BodyWriter, scale, cancellationToken); await response.CompleteAsync(); } } \ No newline at end of file diff --git a/App/Utils.cs b/App/Utils.cs index edc1087..29e101d 100644 --- a/App/Utils.cs +++ b/App/Utils.cs @@ -15,19 +15,23 @@ public static class Utils dto.CropSize![0], dto.CropSize![1]); - public static async Task WriteToStream(this GridSection section, Stream stream, float? scale) + public static async Task WriteToStream( + this GridSection section, + Stream stream, + float? scale, + CancellationToken cancellationToken = default) { - var imageCreator = new ImageCreator(section); - await imageCreator.WriteToStream(stream, scale!.Value); + using var imageCreator = new DangerousImageCreator(section); + await imageCreator.WriteToStream(stream, scale!.Value, cancellationToken); } - public static async Task DangerousWriteToPipe( + public static async Task WriteToPipe( this GridSection section, PipeWriter pipeWriter, float? scale, CancellationToken cancellationToken = default) { - var imageCreator = new DangerousImageCreator(section); + using var imageCreator = new DangerousImageCreator(section); await imageCreator.WriteToPipe(pipeWriter, scale!.Value, cancellationToken); } } \ No newline at end of file diff --git a/Domain/Configuration.cs b/Domain/Configuration.cs index 644a6bc..93d3155 100644 --- a/Domain/Configuration.cs +++ b/Domain/Configuration.cs @@ -20,25 +20,20 @@ public class Configuration public int TileCount => Columns * Rows; - public required int ImageCacheCapacity { get; init; } - public required int IntegralCacheCapacity { get; init; } - public static Configuration Default { get { - var assetPath = Environment.GetEnvironmentVariable("ASSET_PATH_RO")!; + var assetPath = Environment.GetEnvironmentVariable("ASSET_PATH_RO"); var cachePath = Path.Combine(Path.GetTempPath(), "d42df2a2-60ac-4dc3-a6b9-d4c04f2e08e6"); return new Configuration { - AssetPath = assetPath, + AssetPath = assetPath!, CachePath = cachePath, Columns = 55, Rows = 31, Width = 720, Height = 720, - ImageCacheCapacity = 5, - IntegralCacheCapacity = 10, }; } } diff --git a/Domain/ImageCreators/DangerousImageCreator.cs b/Domain/ImageCreators/DangerousImageCreator.cs index 04453b5..483e288 100644 --- a/Domain/ImageCreators/DangerousImageCreator.cs +++ b/Domain/ImageCreators/DangerousImageCreator.cs @@ -39,33 +39,21 @@ public sealed class DangerousImageCreator : IDisposable ~DangerousImageCreator() => Dispose(); - public async Task WriteToPipe2(PipeWriter outputPipe, float scale, CancellationToken cancellationToken = default) + private Task Create(float scale, Action> writeRowCallback, CancellationToken cancellationToken) { var scaleFactor = MathF.ReciprocalEstimate(scale); var targetWidth = (int)(Width / scaleFactor); var targetHeight = (int)(Height / scaleFactor); if (targetHeight == 0 || targetWidth == 0) - return; - - var encoder = new PngPipeEncoder(outputPipe, targetWidth, targetHeight); - encoder.WriteHeader(); + return Task.CompletedTask; + using var xLookup = Utils.BoundsMatrix(scaleFactor, targetWidth, FullWidth, OffsetX); + using var yLookup = Utils.BoundsMatrix(scaleFactor, targetHeight, FullHeight, OffsetY); + + using var yStartMap = MemoryAllocator.Allocate(targetWidth + 1); + using var yEndMap = MemoryAllocator.Allocate(targetWidth + 1); + var outputBufferSize = targetWidth * Unsafe.SizeOf(); - - var xLookup = Utils.DoubleBoundsMatrix(scaleFactor, targetWidth, FullWidth, OffsetX); - var yLookup = Utils.DoubleBoundsMatrix(scaleFactor, targetHeight, FullHeight, OffsetY); - - using var xLookup0 = xLookup.Item1; - using var xLookup1 = xLookup.Item2; - using var yLookup0 = yLookup.Item1; - using var yLookup1 = yLookup.Item2; - - using var yStartMap0 = MemoryAllocator.Allocate(targetWidth); - using var yStartMap1 = MemoryAllocator.Allocate(targetWidth); - using var yEndMap0 = MemoryAllocator.Allocate(targetWidth); - using var yEndMap1 = MemoryAllocator.Allocate(targetWidth); - - // var yStart = OffsetY; // Use pixel referencing to eliminate type casting var pxInt32 = Int32Pixel.Zero; @@ -73,42 +61,38 @@ public sealed class DangerousImageCreator : IDisposable ref var rChannel = ref Unsafe.As(ref px); ref var gChannel = ref Unsafe.Add(ref rChannel, 4); ref var bChannel = ref Unsafe.Add(ref rChannel, 8); + + var taskQueue = TaskHelper.SynchronizedTaskFactory.StartNew(() => { }, cancellationToken); - var outputTaskQueue = TaskHelper.SynchronizedTaskFactory.StartNew(() => { }, cancellationToken); - for (var y = 0; y < targetHeight; y++) + for (var y = 1; y <= targetHeight; y++) { - var yStart = yLookup0[y]; - var yEnd = yLookup1[y]; + var yStart = yLookup[y - 1]; + var yEnd = yLookup[y]; var (localRow0, localOffsetY0) = int.DivRem(yStart, TileHeight); - MapRow(localRow0, localOffsetY0, xLookup0, targetWidth, yStartMap0); - MapRow(localRow0, localOffsetY0, xLookup1, targetWidth, yStartMap1); + MapRow(localRow0, localOffsetY0, xLookup, targetWidth + 1, yStartMap); var (localRow1, localOffsetY1) = int.DivRem(yEnd, TileHeight); - MapRow(localRow1, localOffsetY1, xLookup0, targetWidth, yEndMap0); - MapRow(localRow1, localOffsetY1, xLookup1, targetWidth, yEndMap1); + MapRow(localRow1, localOffsetY1, xLookup, targetWidth + 1, yEndMap); + // Cross row if (localRow0 != localRow1) - { - MapRow(localRow0, BottomPixelIndex, xLookup0, targetWidth, yEndMap0, true); - MapRow(localRow0, BottomPixelIndex, xLookup1, targetWidth, yEndMap1, true); - } - - // int xStart = OffsetX, x0 = 0; + MapRow(localRow0, BottomPixelIndex, xLookup, targetWidth + 1, yEndMap, true); var outputBuffer = MemoryAllocator.Allocate(outputBufferSize); ref var outputChannel = ref outputBuffer.Span[0]; - var boxHeight = Math.Max(1, yEnd - yStart); - for (int x = 0; x < targetWidth; x++) + var boxHeight = yEnd - yStart; + + for (int x1 = 1, x0 = 0; x1 <= targetWidth; x0 = x1++) { - var xStart = xLookup0[x]; - var xEnd = xLookup1[x]; + var xStart = xLookup[x1 - 1]; + var xEnd = xLookup[x1]; - px = yEndMap1[x]; - px += yStartMap0[x]; - px -= yEndMap0[x]; - px -= yStartMap1[x]; - px /= Math.Max(1, xEnd - xStart) * boxHeight; + px = yEndMap[x1]; + px += yStartMap[x0]; + px -= yEndMap[x0]; + px -= yStartMap[x1]; + px /= Math.Max(1, (xEnd - xStart) * boxHeight); outputChannel = rChannel; outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1); @@ -118,22 +102,24 @@ public sealed class DangerousImageCreator : IDisposable outputChannel = bChannel; outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1); - - // xStart = xEnd; - // x0 = x; + } + + if (cancellationToken.IsCancellationRequested) + { + outputBuffer.Dispose(); + return Task.FromCanceled(cancellationToken); } - outputTaskQueue = outputTaskQueue - .ContinueWith(async _ => - { - await encoder.WriteDataAsync(outputBuffer, cancellationToken: cancellationToken); - }, cancellationToken); - - yStart = yEnd; + cancellationToken.Register(outputBuffer.Dispose); + taskQueue = taskQueue + .ContinueWith( + _ => writeRowCallback.Invoke(outputBuffer), + cancellationToken, + TaskContinuationOptions.OnlyOnRanToCompletion, + TaskScheduler.Current); } - await outputTaskQueue; - await encoder.WriteEndOfFileAsync(cancellationToken); + return taskQueue; } public async Task WriteToPipe(PipeWriter outputPipe, float scale, CancellationToken cancellationToken = default) @@ -147,181 +133,32 @@ public sealed class DangerousImageCreator : IDisposable var encoder = new PngPipeEncoder(outputPipe, targetWidth, targetHeight); encoder.WriteHeader(); - Task outputTaskQueue; - - var outputBufferSize = targetWidth * Unsafe.SizeOf(); - - using var xLookup = Utils.BoundsMatrix(scaleFactor, targetWidth, FullWidth, OffsetX); - using var yLookup = Utils.BoundsMatrix(scaleFactor, targetHeight, FullHeight, OffsetY); - - using var yStartMap = MemoryAllocator.Allocate(targetWidth + 1); - using var yEndMap = MemoryAllocator.Allocate(targetWidth + 1); - // OffsetX-(int)float.Ceiling(scaleFactor) - int yStart = OffsetY, - yEnd = yLookup[0], - xStart = OffsetX, - x0 = 0; + await Create(scale, + output => encoder + .WriteDataAsync(output, cancellationToken: cancellationToken) + .Wait(cancellationToken), + cancellationToken); - - - // Use pixel referencing to eliminate type casting - var pxInt32 = Int32Pixel.Zero; - ref var px = ref pxInt32; - ref var rChannel = ref Unsafe.As(ref px); - ref var gChannel = ref Unsafe.Add(ref rChannel, 4); - ref var bChannel = ref Unsafe.Add(ref rChannel, 8); - - // First row - var (localRow0, localOffsetY0) = int.DivRem(yStart, TileHeight); - var (localRow1, localOffsetY1) = int.DivRem(yEnd, TileHeight); - { - switch (localOffsetY0) - { - // Cross row tile, no need to handle if it's first row tile for now - // the provided asset is bordered black anyway - case 0 when TileOrigin.Row > 1: - localOffsetY0 = BottomPixelIndex; - localRow0--; - break; - case > 0: - localOffsetY0--; - break; - } - - MapRow(localRow0, localOffsetY0, xLookup, targetWidth, yStartMap); - MapRow(localRow1, localOffsetY1, xLookup, targetWidth, yEndMap); - - // Cross row - if (localRow0 != localRow1) - MapRow(localRow0, BottomPixelIndex, xLookup, targetWidth, yEndMap, true); + await encoder.WriteEndOfFileAsync(cancellationToken); + } - var outputBuffer = MemoryAllocator.Allocate(outputBufferSize); - ref var outputChannel = ref outputBuffer.Span[0]; - var boxHeight = yEnd - yStart; - - // Render first pixel row - var xEnd = xLookup[0]; - px = yEndMap[0]; - px += yStartMap[^1]; - px -= yEndMap[^1]; - px -= yStartMap[0]; - px /= Math.Max(1, (xEnd - xStart) * boxHeight); - - outputChannel = rChannel; - outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1); - - outputChannel = gChannel; - outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1); - - outputChannel = bChannel; - outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1); - - xStart = xEnd; - - // Render entire pixel row - for (int x1 = 1; x1 < targetWidth; x1++) - { - xEnd = xLookup[x1]; - - px = yEndMap[x1]; - px += yStartMap[x0]; - px -= yEndMap[x0]; - px -= yStartMap[x1]; - px /= Math.Max(1, (xEnd - xStart) * boxHeight); - - outputChannel = rChannel; - outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1); - - outputChannel = gChannel; - outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1); - - outputChannel = bChannel; - outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1); - - xStart = xEnd; - x0 = x1; - } - - outputTaskQueue = TaskHelper.SynchronizedTaskFactory.StartNew(async _ => - { - await encoder.WriteDataAsync(outputBuffer, cancellationToken: cancellationToken); - }, null, cancellationToken); - - yStart = yEnd; - } + public async Task WriteToStream(Stream outputStream, float scale, CancellationToken cancellationToken = default) + { + var scaleFactor = MathF.ReciprocalEstimate(scale); + var targetWidth = (int)(Width / scaleFactor); + var targetHeight = (int)(Height / scaleFactor); + if (targetHeight == 0 || targetWidth == 0) + return; + + var encoder = new PngStreamEncoder(outputStream, targetWidth, targetHeight); + await encoder.WriteHeader(cancellationToken); + + await Create(scale, + output => encoder + .WriteDataAsync(output, cancellationToken: cancellationToken) + .Wait(cancellationToken), + cancellationToken); - for (var y = 1; y < targetHeight; y++) - { - yEnd = yLookup[y]; - - (localRow0, localOffsetY0) = int.DivRem(yStart, TileHeight); - MapRow(localRow0, localOffsetY0, xLookup, targetWidth, yStartMap); - - (localRow1, localOffsetY1) = int.DivRem(yEnd, TileHeight); - MapRow(localRow1, localOffsetY1, xLookup, targetWidth, yEndMap); - - // Cross row - if (localRow0 != localRow1) - MapRow(localRow0, BottomPixelIndex, xLookup, targetWidth, yEndMap, true); - - xStart = OffsetX; - x0 = 0; - - var outputBuffer = MemoryAllocator.Allocate(outputBufferSize); - ref var outputChannel = ref outputBuffer.Span[0]; - var boxHeight = yEnd - yStart; - - var xEnd = xLookup[0]; - px = yEndMap[0]; - px += yStartMap[^1]; - px -= yEndMap[^1]; - px -= yStartMap[0]; - px /= Math.Max(1, (xEnd - xStart) * boxHeight); - - outputChannel = rChannel; - outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1); - - outputChannel = gChannel; - outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1); - - outputChannel = bChannel; - outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1); - - xStart = xEnd; - - for (int x1 = 1; x1 < targetWidth; x1++) - { - xEnd = xLookup[x1]; - - px = yEndMap[x1]; - px += yStartMap[x0]; - px -= yEndMap[x0]; - px -= yStartMap[x1]; - px /= Math.Max(1, (xEnd - xStart) * boxHeight); - - outputChannel = rChannel; - outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1); - - outputChannel = gChannel; - outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1); - - outputChannel = bChannel; - outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1); - - xStart = xEnd; - x0 = x1; - } - - outputTaskQueue = outputTaskQueue - .ContinueWith(async _ => - { - await encoder.WriteDataAsync(outputBuffer, cancellationToken: cancellationToken); - }, cancellationToken); - - yStart = yEnd; - } - - await outputTaskQueue; await encoder.WriteEndOfFileAsync(cancellationToken); } @@ -333,33 +170,23 @@ public sealed class DangerousImageCreator : IDisposable IBuffer destination, bool appendMode = false) { + var currentTile = TileManager.TryGetAdjacent(TileOrigin, 0, rowOffset); + if (currentTile == null) + { + if (appendMode) return; + for (var i = 0; i < count; i++) + destination[i] = Int32Pixel.Zero; + + return; + } + var sourceMap = boundsMatrix.Span[..count]; - var currentTile = TileManager.GetAdjacent(TileOrigin, 0, rowOffset); var xAdder = Int32Pixel.Zero; var xOffset = 0; var written = 0; var destinationSpan = destination.Span; var readBufferSpan = _mmfReadBuffer.Span; - var negative = sourceMap[0] - 1; - var negativePixel = appendMode ? destinationSpan[^1] : Int32Pixel.Zero; - if (negative >= 0) - { - negativePixel = readBufferSpan[negative]; - } - else if(currentTile.Column > 1) - { - // Cross row tile, no need to handle if it's first column tile for now - // the provided asset is bordered black anyway - TileManager.GetAdjacent(currentTile, -1, 0) - .Integral - .Acquire(yOffset, readBufferSpan); - negativePixel = readBufferSpan[RightmostPixelIndex]; - xAdder = readBufferSpan[RightmostPixelIndex]; - } - - destinationSpan[^1] = negativePixel; - while (true) { currentTile.Integral.Acquire(yOffset, readBufferSpan); @@ -383,13 +210,19 @@ public sealed class DangerousImageCreator : IDisposable written++; } } - + if (written >= sourceMap.Length) break; + + currentTile = TileManager.TryGetAdjacent(currentTile, 1, 0); + if (currentTile == null) + { + destinationSpan[written] = destinationSpan[written - 1]; + break; + } xAdder += readBufferSpan[RightmostPixelIndex]; xOffset += TileWidth; - currentTile = TileManager.GetAdjacent(currentTile, 1, 0); } } diff --git a/Domain/ImageCreators/ImageCreator.cs b/Domain/ImageCreators/ImageCreator.cs deleted file mode 100644 index 7858389..0000000 --- a/Domain/ImageCreators/ImageCreator.cs +++ /dev/null @@ -1,160 +0,0 @@ -using System.Runtime.CompilerServices; -using SixLabors.ImageSharp.PixelFormats; -using StitchATon2.Infra; -using StitchATon2.Infra.Buffers; -using StitchATon2.Infra.Encoders; - -namespace StitchATon2.Domain.ImageCreators; - -public class ImageCreator : IDisposable -{ - private readonly GridSection _section; - - private int FullWidth => _section.TileManager.Configuration.FullWidth; - private int FullHeight => _section.TileManager.Configuration.FullHeight; - - private int OffsetX => _section.OffsetX; - private int OffsetY => _section.OffsetY; - - private int Width => _section.Width; - private int Height => _section.Height; - - private int TileWidth => _section.TileManager.Configuration.Width; - private int TileHeight => _section.TileManager.Configuration.Height; - private Tile TileOrigin => _section.Origin; - - private int RightmostPixelIndex => _section.TileManager.Configuration.RightTileIndex; - private int BottomPixelIndex => _section.TileManager.Configuration.BottomTileIndex; - - private TileManager TileManager => _section.TileManager; - - private readonly ArrayOwner _mmfReadBuffer; - - public ImageCreator(GridSection section) - { - _section = section; - _mmfReadBuffer = MemoryAllocator.AllocateArray(TileWidth); - } - - public async Task WriteToStream(Stream writableStream, float scale) - { - var scaleFactor = MathF.ReciprocalEstimate(scale); - var targetWidth = (int)(Width / scaleFactor); - var targetHeight = (int)(Height / scaleFactor); - - var encoder = new PngStreamEncoder(writableStream, targetWidth, targetHeight); - await encoder.WriteHeader(); - - var outputBufferSize = targetWidth * Unsafe.SizeOf(); - using var outputBuffer = MemoryAllocator.AllocateManaged(outputBufferSize); - - using var xLookup = Utils.BoundsMatrix(scaleFactor, targetWidth, FullWidth, OffsetX); - using var yLookup = Utils.BoundsMatrix(scaleFactor, targetHeight, FullHeight, OffsetY); - - using var yStartMap = MemoryAllocator.Allocate(targetWidth); - using var yEndMap = MemoryAllocator.Allocate(targetWidth); - - var yStart = OffsetY; - Task? outputTask = null; - for (var y = 0; y < targetHeight; y++) - { - var yEnd = yLookup[y]; - - var (localRow0, localOffsetY0) = int.DivRem(yStart, TileHeight); - MapRow(localRow0, localOffsetY0, xLookup.Span[..targetWidth], yStartMap); - - var (localRow1, localOffsetY1) = int.DivRem(yEnd, TileHeight); - MapRow(localRow1, localOffsetY1, xLookup.Span[..targetWidth], yEndMap); - - if (localRow0 != localRow1) - { - MapRowAppend(localRow0, BottomPixelIndex, xLookup.Span[..targetWidth], yEndMap); - } - - if(outputTask != null) - await outputTask; - - int xStart = OffsetX, x0 = 0; - for (int x1 = 0, i = 0; x1 < targetWidth; x1++) - { - var xEnd = xLookup[x1]; - - var pixel = yEndMap[x1]; - pixel += yStartMap[x0]; - pixel -= yEndMap[x0]; - pixel -= yStartMap[x1]; - - pixel /= Math.Max(1, (xEnd - xStart) * (yEnd - yStart)); - outputBuffer.Memory.Span[i++] = (byte)pixel.R; - outputBuffer.Memory.Span[i++] = (byte)pixel.G; - outputBuffer.Memory.Span[i++] = (byte)pixel.B; - - xStart = xEnd; - x0 = x1; - } - - outputTask = encoder.WriteData(outputBuffer.Memory[..outputBufferSize]); - yStart = yEnd; - } - - await encoder.WriteEndOfFile(); - } - - private void MapRow(int rowOffset, int yOffset, Span sourceMap, IBuffer destination) - { - var currentTile = TileManager.GetAdjacent(TileOrigin, 0, rowOffset); - var xAdder = Int32Pixel.Zero; - var xOffset = 0; - var written = 0; - while (true) - { - currentTile.Integral.Acquire(yOffset, _mmfReadBuffer.Array); - int localX; - while (written < sourceMap.Length && (localX = sourceMap[written] - xOffset) < TileWidth) - { - destination.Span[written] = _mmfReadBuffer[localX]; - destination.Span[written] += xAdder; - written++; - } - - if (written >= sourceMap.Length) - break; - - xAdder += _mmfReadBuffer[RightmostPixelIndex]; - xOffset += TileWidth; - currentTile = TileManager.GetAdjacent(currentTile, 1, 0); - } - } - - private void MapRowAppend(int rowOffset, int yOffset, Span sourceMap, IBuffer destination) - { - var currentTile = TileManager.GetAdjacent(TileOrigin, 0, rowOffset); - var xAdder = Int32Pixel.Zero; - var xOffset = 0; - var written = 0; - while (true) - { - currentTile.Integral.Acquire(yOffset, _mmfReadBuffer.Array); - int localX; - while (written < sourceMap.Length && (localX = sourceMap[written] - xOffset) < TileWidth) - { - destination.Span[written] += _mmfReadBuffer[localX]; - destination.Span[written] += xAdder; - written++; - } - - if (written >= sourceMap.Length) - break; - - xAdder += _mmfReadBuffer[RightmostPixelIndex]; - xOffset += TileWidth; - currentTile = TileManager.GetAdjacent(currentTile, 1, 0); - } - } - - public void Dispose() - { - _mmfReadBuffer.Dispose(); - GC.SuppressFinalize(this); - } -} \ No newline at end of file diff --git a/Domain/TileManager.cs b/Domain/TileManager.cs index 0b9864f..d2e7c61 100644 --- a/Domain/TileManager.cs +++ b/Domain/TileManager.cs @@ -24,7 +24,7 @@ public sealed class TileManager private Tile CreateTile(int id) { var (row, column) = int.DivRem(id, Configuration.Columns); - var coordinate = $"{Utils.GetSBSNotationRow(++row)}{++column}"; + var coordinate = $"{Utils.GetSbsNotationRow(++row)}{++column}"; return new Tile { Id = id, @@ -51,7 +51,7 @@ public sealed class TileManager public Tile GetTile(string coordinate) { - var (column, row) = Utils.GetSBSCoordinate(coordinate); + var (column, row) = Utils.GetSbsNotationCoordinate(coordinate); return GetTile(column, row); } diff --git a/Domain/Utils.cs b/Domain/Utils.cs index 63d56c0..607ab05 100644 --- a/Domain/Utils.cs +++ b/Domain/Utils.cs @@ -9,7 +9,7 @@ namespace StitchATon2.Domain; public static class Utils { [Pure] - public static string GetSBSNotationRow(int row) + public static string GetSbsNotationRow(int row) => row <= 26 ? new string([(char)(row + 'A' - 1)]) : new string(['A', (char)(row + 'A' - 27)]); @@ -21,7 +21,7 @@ public static class Utils } [Pure] - public static (int Column, int Row) GetSBSCoordinate(string coordinate) + public static (int Column, int Row) GetSbsNotationCoordinate(string coordinate) { var column = coordinate[^1] - '0'; if(char.IsDigit(coordinate[^2])) @@ -43,18 +43,18 @@ public static class Utils /// The offset to apply before clamping. public static IBuffer BoundsMatrix(float scaleFactor, int length, int max, int offset) { - var vectorSize = DivCeil(length, Vector.Count); + var vectorSize = DivCeil(length + 1, Vector.Count); using var buffer = MemoryAllocator.Allocate>(vectorSize); var span = buffer.Span; var vectorMin = Vector.Zero; - var vectorOffset = new Vector(offset - 1); + var vectorOffset = new Vector(offset); var vectorMax = new Vector(max - 1); var vectorScale = new Vector(scaleFactor); var vectorSequence = Vector.CreateSequence(0f, 1f); - var seq = 0f; + var seq = -1f; for (var i = 0; i < vectorSize; i++, seq += Vector.Count) { var sequence = new Vector(seq) + vectorSequence; @@ -70,54 +70,7 @@ public static class Utils resultSpan[i] = Vector.Add(resultSpan[i], vectorOffset); resultSpan[i] = Vector.ClampNative(resultSpan[i], vectorMin, vectorMax); } - - var negative = float.Ceiling(scaleFactor); return result; } - - public static (IBuffer, IBuffer) DoubleBoundsMatrix(float scaleFactor, int length, int max, int offset) - { - var vectorSize = DivCeil(length, Vector.Count); - using var startBuffer = MemoryAllocator.Allocate>(vectorSize); - using var endBuffer = MemoryAllocator.Allocate>(vectorSize); - - var startSpan = startBuffer.Span; - var endSpan = endBuffer.Span; - - var vectorMin = Vector.Zero; - var vectorOne = Vector.One; - var vectorMax = Vector.Create(max); - var vectorScale = Vector.Create(scaleFactor); - var vectorOffset = new Vector(offset - 1); - - for (int i = 0, seq = 0; i < vectorSize; i++, seq += Vector.Count) - { - startSpan[i] = Vector.CreateSequence(seq, 1f); - startSpan[i] = Vector.Multiply(startSpan[i], vectorScale); - endSpan[i] = Vector.Add(vectorScale, startSpan[i]); - endSpan[i] = Vector.Ceiling(endSpan[i]); - } - - var resultStart = MemoryAllocator.Allocate(vectorSize * Vector.Count); - var resultEnd = MemoryAllocator.Allocate(vectorSize * Vector.Count); - - var resultStartSpan = MemoryMarshal.Cast>(resultStart.Span); - var resultEndSpan = MemoryMarshal.Cast>(resultEnd.Span); - - for (var i = 0; i < vectorSize; i++) - { - resultStartSpan[i] = Vector.ConvertToInt32(startSpan[i]); - resultStartSpan[i] = Vector.Subtract(resultStartSpan[i], vectorOne); - resultStartSpan[i] = Vector.Add(resultStartSpan[i], vectorOffset); - resultStartSpan[i] = Vector.Clamp(resultStartSpan[i], vectorMin, vectorMax); - - resultEndSpan[i] = Vector.ConvertToInt32(endSpan[i]); - resultEndSpan[i] = Vector.Subtract(resultEndSpan[i], vectorOne); - resultEndSpan[i] = Vector.Add(resultEndSpan[i], vectorOffset); - resultEndSpan[i] = Vector.Clamp(resultEndSpan[i], vectorMin, vectorMax); - } - - return (resultStart, resultEnd); - } } \ No newline at end of file diff --git a/Infra/Buffers/ArrayOwner.cs b/Infra/Buffers/ArrayOwner.cs index f1e30fb..98f436a 100644 --- a/Infra/Buffers/ArrayOwner.cs +++ b/Infra/Buffers/ArrayOwner.cs @@ -24,7 +24,8 @@ public class ArrayOwner : IBuffer where T : unmanaged public ref T this[int index] => ref _buffer[index]; - public Span Span => _buffer; + public Span Span => _buffer.AsSpan(0, Length); + public Memory Memory => _buffer.AsMemory(0, Length); public T[] Array => _buffer; diff --git a/Infra/Buffers/IBuffer.cs b/Infra/Buffers/IBuffer.cs index de13308..a1f4f71 100644 --- a/Infra/Buffers/IBuffer.cs +++ b/Infra/Buffers/IBuffer.cs @@ -5,6 +5,8 @@ public interface IBuffer : IDisposable where T : unmanaged ref T this[int index] { get; } Span Span { get; } + + Memory Memory { get; } int Length { get; } } \ No newline at end of file diff --git a/Infra/Buffers/ImmovableMemory.cs b/Infra/Buffers/ImmovableMemory.cs index 4400c9a..24a0b25 100644 --- a/Infra/Buffers/ImmovableMemory.cs +++ b/Infra/Buffers/ImmovableMemory.cs @@ -4,34 +4,48 @@ using System.Runtime.InteropServices; namespace StitchATon2.Infra.Buffers; -internal sealed unsafe class ImmovableMemory : MemoryManager where T : unmanaged +internal sealed unsafe class ImmovableMemory : MemoryManager, IBuffer where T : unmanaged { - private readonly T* _pointer; - private readonly int _length; + internal readonly T* Pointer; private bool _disposed; - public ImmovableMemory(int count) + public ImmovableMemory(int length) { - _pointer = (T*)NativeMemory.Alloc((nuint)count, (nuint)Unsafe.SizeOf()); - _length = count; + Pointer = (T*)NativeMemory.Alloc((nuint)length, (nuint)Unsafe.SizeOf()); + Length = length; } protected override void Dispose(bool disposing) { - if (!_disposed) - { - NativeMemory.Free(_pointer); - _disposed = true; - } + if (_disposed) return; + NativeMemory.Free(Pointer); + _disposed = true; } public override Span GetSpan() - => new(_pointer, _length); + => _disposed + ? throw new ObjectDisposedException(nameof(ImmovableMemory)) + : new Span(Pointer, Length); public override MemoryHandle Pin(int elementIndex = 0) - => new(_pointer + elementIndex); + => _disposed + ? throw new ObjectDisposedException(nameof(ImmovableMemory)) + : new MemoryHandle(Pointer + elementIndex); public override void Unpin() { } + + public ref T this[int index] + { + get + { + if (_disposed) throw new ObjectDisposedException(nameof(ImmovableMemory)); + return ref Unsafe.AsRef(Pointer + index); + } + } + + public Span Span => GetSpan(); + + public int Length { get; } } \ No newline at end of file diff --git a/Infra/Buffers/MemoryAllocator.cs b/Infra/Buffers/MemoryAllocator.cs index 0d40d63..57a305e 100644 --- a/Infra/Buffers/MemoryAllocator.cs +++ b/Infra/Buffers/MemoryAllocator.cs @@ -6,7 +6,7 @@ namespace StitchATon2.Infra.Buffers; public static class MemoryAllocator { public static IBuffer Allocate(int count) where T : unmanaged - => new UnmanagedMemory(count); + => new ImmovableMemory(count); public static IMemoryOwner AllocateManaged(int count) => MemoryPool.Shared.Rent(count); @@ -14,31 +14,23 @@ public static class MemoryAllocator public static ArrayOwner AllocateArray(int count) where T : unmanaged => new(ArrayPool.Shared, count); - public static MemoryManager AllocateImmovable(int count) where T : unmanaged - => new ImmovableMemory(count); - public static unsafe IBuffer Clone(this IBuffer buffer) where T : unmanaged { - if (buffer is UnmanagedMemory unmanagedMemory) - { - var newBuffer = new UnmanagedMemory(buffer.Length); - var byteCount = (uint)(Unsafe.SizeOf() * buffer.Length); - Unsafe.CopyBlock(newBuffer.Pointer, unmanagedMemory.Pointer, byteCount); - return newBuffer; - } + if (buffer is not ImmovableMemory unmanagedMemory) + throw new NotSupportedException(); - throw new NotSupportedException(); + var newBuffer = new ImmovableMemory(buffer.Length); + var byteCount = (uint)(Unsafe.SizeOf() * buffer.Length); + Unsafe.CopyBlock(newBuffer.Pointer, unmanagedMemory.Pointer, byteCount); + return newBuffer; } public static unsafe void Copy(this IBuffer source, IBuffer destination, int count) where T : unmanaged { - if (source is UnmanagedMemory sourceBuffer && destination is UnmanagedMemory destinationBuffer) - { - var byteCount = (uint)(Unsafe.SizeOf() * count); - Unsafe.CopyBlock(destinationBuffer.Pointer, sourceBuffer.Pointer, byteCount); - return; - } + if (source is not ImmovableMemory sourceBuffer || destination is not ImmovableMemory destinationBuffer) + throw new NotSupportedException(); - throw new NotSupportedException(); + var byteCount = (uint)(Unsafe.SizeOf() * count); + Unsafe.CopyBlock(destinationBuffer.Pointer, sourceBuffer.Pointer, byteCount); } } \ No newline at end of file diff --git a/Infra/Buffers/UnmanagedMemory.cs b/Infra/Buffers/UnmanagedMemory.cs index 6b97f1d..c0be446 100644 --- a/Infra/Buffers/UnmanagedMemory.cs +++ b/Infra/Buffers/UnmanagedMemory.cs @@ -7,11 +7,13 @@ namespace StitchATon2.Infra.Buffers; /// Provide non-thread safe anti GC contiguous memory. /// /// +[Obsolete("Use immovable memory instead")] internal sealed unsafe class UnmanagedMemory : IBuffer where T : unmanaged { internal readonly T* Pointer; private bool _disposed; - + + public Memory Memory => throw new NotImplementedException(); public int Length { get; } public ref T this[int index] => ref Unsafe.AsRef(Pointer + index); diff --git a/Infra/Encoders/PngPipeEncoder.cs b/Infra/Encoders/PngPipeEncoder.cs index 9a2b844..02fbfdd 100644 --- a/Infra/Encoders/PngPipeEncoder.cs +++ b/Infra/Encoders/PngPipeEncoder.cs @@ -63,26 +63,32 @@ public class PngPipeEncoder : IDisposable public async Task WriteDataAsync(IBuffer buffer, bool disposeBuffer = true, CancellationToken cancellationToken = default) { - _zlibStream.Write([0]); - - var offset = 0; - while (buffer.Length - offset > FlushThreshold) + try { - _zlibStream.Write(buffer.Span.Slice(offset, FlushThreshold)); - await _zlibStream.FlushAsync(cancellationToken); - offset += FlushThreshold; - if(_memoryStream.Length >= BufferSize) - await FlushAsync(cancellationToken); + _zlibStream.Write([0]); + + var offset = 0; + while (buffer.Length - offset > FlushThreshold) + { + _zlibStream.Write(buffer.Span.Slice(offset, FlushThreshold)); + await _zlibStream.FlushAsync(cancellationToken); + offset += FlushThreshold; + if (_memoryStream.Length >= BufferSize) + await FlushAsync(cancellationToken); + } + + if (buffer.Length > offset) + { + _zlibStream.Write(buffer.Span[offset..]); + await _zlibStream.FlushAsync(cancellationToken); + _shouldFlush = true; + } } - - if (buffer.Length > offset) + finally { - _zlibStream.Write(buffer.Span[offset..]); - await _zlibStream.FlushAsync(cancellationToken); - _shouldFlush = true; + if(disposeBuffer) + buffer.Dispose(); } - - if(disposeBuffer) buffer.Dispose(); } private async Task FlushAsync(CancellationToken cancellationToken) @@ -101,7 +107,7 @@ public class PngPipeEncoder : IDisposable // write Crc var crc = Crc32.Compute(buffer.AsSpan(4, dataSize + 4)); BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(dataSize + 8), crc); - + _outputPipe.Write(buffer.AsSpan(0, dataSize + 12)); await _outputPipe.FlushAsync(cancellationToken); diff --git a/Infra/Encoders/PngStreamEncoder.cs b/Infra/Encoders/PngStreamEncoder.cs index 3b70cb4..a1a0359 100644 --- a/Infra/Encoders/PngStreamEncoder.cs +++ b/Infra/Encoders/PngStreamEncoder.cs @@ -1,5 +1,6 @@ using System.Buffers.Binary; using System.IO.Compression; +using StitchATon2.Infra.Buffers; namespace StitchATon2.Infra.Encoders; @@ -30,7 +31,7 @@ public class PngStreamEncoder : IDisposable, IAsyncDisposable ~PngStreamEncoder() => Dispose(); - public async Task WriteHeader() + public async Task WriteHeader(CancellationToken cancellationToken = default) { byte[] headerBytes = [ 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG Signature @@ -54,34 +55,42 @@ public class PngStreamEncoder : IDisposable, IAsyncDisposable BinaryPrimitives.WriteUInt32BigEndian(headerBytes.AsSpan(29), crc); - await _stream.WriteAsync(headerBytes); + await _stream.WriteAsync(headerBytes, cancellationToken); } - public async Task WriteData(Memory data) + public async Task WriteDataAsync(IBuffer buffer, bool disposeBuffer = true, CancellationToken cancellationToken = default) { - _zlibStream.Write([0]); - - var dataSlice = data; - while (dataSlice.Length > FlushThreshold) + try { - await _zlibStream.WriteAsync(dataSlice[..FlushThreshold]); - await _zlibStream.FlushAsync(); - dataSlice = dataSlice[FlushThreshold..]; - if(_memoryStream.Length >= BufferSize) - await Flush(); + _zlibStream.Write([0]); + + var dataSlice = buffer.Memory; + while (dataSlice.Length > FlushThreshold) + { + await _zlibStream.WriteAsync(dataSlice[..FlushThreshold], cancellationToken); + await _zlibStream.FlushAsync(cancellationToken); + dataSlice = dataSlice[FlushThreshold..]; + if (_memoryStream.Length >= BufferSize) + await FlushAsync(cancellationToken); + } + + if (dataSlice.Length > 0) + { + await _zlibStream.WriteAsync(dataSlice, cancellationToken); + await _zlibStream.FlushAsync(cancellationToken); + _shouldFlush = true; + } } - - if (dataSlice.Length > 0) + finally { - await _zlibStream.WriteAsync(dataSlice); - await _zlibStream.FlushAsync(); - _shouldFlush = true; + if(disposeBuffer) + buffer.Dispose(); } } - private async Task Flush() + private async Task FlushAsync(CancellationToken cancellationToken) { - await _zlibStream.FlushAsync(); + await _zlibStream.FlushAsync(cancellationToken); var dataSize = (int)(_memoryStream.Length - 8); _memoryStream.Write("\0\0\0\0"u8); @@ -96,16 +105,16 @@ public class PngStreamEncoder : IDisposable, IAsyncDisposable var crc = Crc32.Compute(buffer.AsSpan(4, dataSize + 4)); BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(dataSize + 8), crc); - await _stream.WriteAsync(buffer.AsMemory(0, dataSize + 12)); + await _stream.WriteAsync(buffer.AsMemory(0, dataSize + 12), cancellationToken); _memoryStream.SetLength(8); _memoryStream.Position = 8; _shouldFlush = false; } - public async ValueTask WriteEndOfFile() + public async ValueTask WriteEndOfFileAsync(CancellationToken cancellationToken = default) { if(_shouldFlush) - await Flush(); + await FlushAsync(cancellationToken); var endChunk = new byte[] { 0x00, 0x00, 0x00, 0x00, // Length @@ -113,7 +122,7 @@ public class PngStreamEncoder : IDisposable, IAsyncDisposable 0xAE, 0x42, 0x60, 0x82, // Crc }; - await _stream.WriteAsync(endChunk); + await _stream.WriteAsync(endChunk, cancellationToken); await DisposeAsync(); } diff --git a/StitchATon2.sln b/StitchATon2.sln index 702b5a5..cb172fd 100644 --- a/StitchATon2.sln +++ b/StitchATon2.sln @@ -8,6 +8,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StitchATon2.Infra", "Infra\ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StitchATon2.Benchmark", "StitchATon2.Benchmark\StitchATon2.Benchmark.csproj", "{2F9B169C-C799-4489-B864-F912D69C5D3E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ConsoleApp", "ConsoleApp\ConsoleApp.csproj", "{D5B2A2E9-974A-43DD-A5D1-E7226CD03AFF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -30,5 +32,9 @@ Global {2F9B169C-C799-4489-B864-F912D69C5D3E}.Debug|Any CPU.Build.0 = Debug|Any CPU {2F9B169C-C799-4489-B864-F912D69C5D3E}.Release|Any CPU.ActiveCfg = Release|Any CPU {2F9B169C-C799-4489-B864-F912D69C5D3E}.Release|Any CPU.Build.0 = Release|Any CPU + {D5B2A2E9-974A-43DD-A5D1-E7226CD03AFF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D5B2A2E9-974A-43DD-A5D1-E7226CD03AFF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D5B2A2E9-974A-43DD-A5D1-E7226CD03AFF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D5B2A2E9-974A-43DD-A5D1-E7226CD03AFF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal -- 2.49.0