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