From ef3b7d68fb391fb3e2f0c9bc8a03a66c2f616d13 Mon Sep 17 00:00:00 2001 From: dennisarfan Date: Wed, 30 Jul 2025 07:30:00 +0700 Subject: [PATCH] Initial commit --- .gitignore | 5 + .idea/.idea.StitchATon2/.idea/.gitignore | 13 + App/Controllers/ImageController.cs | 53 ++++ App/Models/GenerateImageDto.cs | 72 +++++ App/Program.cs | 28 ++ App/Properties/launchSettings.json | 16 + App/StitchATon2.App.csproj | 19 ++ App/Utils.cs | 22 ++ App/appsettings.Development.json | 8 + App/appsettings.json | 9 + Domain/Configuration.cs | 55 ++++ Domain/GridSection.cs | 51 +++ Domain/ImageCreators/ImageCreator.cs | 155 +++++++++ Domain/StitchATon2.Domain.csproj | 13 + Domain/Tile.cs | 15 + Domain/TileManager.cs | 108 +++++++ Domain/Utils.cs | 84 +++++ Infra/Buffers/ArrayOwner.cs | 29 ++ Infra/Buffers/IBuffer.cs | 8 + Infra/Buffers/MemoryAllocator.cs | 15 + Infra/Buffers/UnmanagedMemory.cs | 28 ++ Infra/Encoders/Crc32.cs | 33 ++ Infra/Encoders/PngStreamEncoder.cs | 143 +++++++++ Infra/ImageIntegral.cs | 387 +++++++++++++++++++++++ Infra/Int32Pixel.cs | 82 +++++ Infra/StitchATon2.Infra.csproj | 14 + Infra/Synchronization/TaskHelper.cs | 12 + Infra/Utils.cs | 56 ++++ StitchATon2.sln | 28 ++ global.json | 7 + 30 files changed, 1568 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.idea.StitchATon2/.idea/.gitignore create mode 100644 App/Controllers/ImageController.cs create mode 100644 App/Models/GenerateImageDto.cs create mode 100644 App/Program.cs create mode 100644 App/Properties/launchSettings.json create mode 100644 App/StitchATon2.App.csproj create mode 100644 App/Utils.cs create mode 100644 App/appsettings.Development.json create mode 100644 App/appsettings.json create mode 100644 Domain/Configuration.cs create mode 100644 Domain/GridSection.cs create mode 100644 Domain/ImageCreators/ImageCreator.cs create mode 100644 Domain/StitchATon2.Domain.csproj create mode 100644 Domain/Tile.cs create mode 100644 Domain/TileManager.cs create mode 100644 Domain/Utils.cs create mode 100644 Infra/Buffers/ArrayOwner.cs create mode 100644 Infra/Buffers/IBuffer.cs create mode 100644 Infra/Buffers/MemoryAllocator.cs create mode 100644 Infra/Buffers/UnmanagedMemory.cs create mode 100644 Infra/Encoders/Crc32.cs create mode 100644 Infra/Encoders/PngStreamEncoder.cs create mode 100644 Infra/ImageIntegral.cs create mode 100644 Infra/Int32Pixel.cs create mode 100644 Infra/StitchATon2.Infra.csproj create mode 100644 Infra/Synchronization/TaskHelper.cs create mode 100644 Infra/Utils.cs create mode 100644 StitchATon2.sln create mode 100644 global.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..add57be --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +bin/ +obj/ +/packages/ +riderModule.iml +/_ReSharper.Caches/ \ No newline at end of file diff --git a/.idea/.idea.StitchATon2/.idea/.gitignore b/.idea/.idea.StitchATon2/.idea/.gitignore new file mode 100644 index 0000000..c798477 --- /dev/null +++ b/.idea/.idea.StitchATon2/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/contentModel.xml +/modules.xml +/.idea.StitchATon2.iml +/projectSettingsUpdater.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/App/Controllers/ImageController.cs b/App/Controllers/ImageController.cs new file mode 100644 index 0000000..2b5386f --- /dev/null +++ b/App/Controllers/ImageController.cs @@ -0,0 +1,53 @@ +using System.Text.Json; +using StitchATon2.App.Models; +using StitchATon2.Domain; + +namespace StitchATon2.App.Controllers; + +public static class ImageController +{ + public static async Task GenerateImage(HttpResponse response, GenerateImageDto dto, TileManager tileManager) + { + if (dto.GetErrors() is { Count: > 0 } errors) + { + response.StatusCode = 422; + response.ContentType = "text/json"; + var errorBody = JsonSerializer.Serialize(errors, AppJsonSerializerContext.Default.DictionaryStringListString); + response.ContentLength = errorBody.Length; + await response.WriteAsync(errorBody); + await response.CompleteAsync(); + return; + } + + response.StatusCode = 200; + response.ContentType = "image/png"; + + await tileManager + .CreateSection(dto) + .WriteToStream(response.Body, dto.OutputScale); + + await response.CompleteAsync(); + } + + public static async Task GenerateRandomImage(HttpResponse response, TileManager tileManager) + { + response.StatusCode = 200; + response.ContentType = "image/png"; + + var maxId = tileManager.Configuration.Rows * tileManager.Configuration.Columns; + var id0 = Random.Shared.Next(maxId); + var id1 = Random.Shared.Next(maxId); + + var tile0 = tileManager.GetTile(id0); + var tile1 = tileManager.GetTile(id1); + var coordinatePair = $"{tile0.Coordinate}:{tile1.Coordinate}"; + + var section = tileManager.CreateSection(coordinatePair, 0, 0, 1, 1); + + 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 response.CompleteAsync(); + } +} \ No newline at end of file diff --git a/App/Models/GenerateImageDto.cs b/App/Models/GenerateImageDto.cs new file mode 100644 index 0000000..85a726d --- /dev/null +++ b/App/Models/GenerateImageDto.cs @@ -0,0 +1,72 @@ +using System.Text.Json.Serialization; + +namespace StitchATon2.App.Models; + +public class GenerateImageDto +{ + [JsonPropertyName("canvas_rect")] + public string? CanvasRect { get; set; } + + [JsonPropertyName("crop_offset")] + public float[]? CropOffset { get; set; } + + [JsonPropertyName("crop_size")] + public float[]? CropSize { get; set; } + + [JsonPropertyName("output_scale")] + public float? OutputScale { get; set; } + + public Dictionary> GetErrors() + { + return ValidateCanvasRect() + .Concat(ValidateNumberPair(CropOffset, "crop_offset")) + .Concat(ValidateNumberPair(CropSize, "crop_size")) + .Concat(ValidateNumber(OutputScale, "output_scale")) + .GroupBy(item => item.Item1) + .ToDictionary(item => item.Key, item => item.Select(p => p.Item2).ToList()); + } + + private IEnumerable<(string, string)> ValidateCanvasRect() + { + if (string.IsNullOrEmpty(CanvasRect)) + { + yield return ("canvas_rect", "canvas_rect is required."); + } + } + + private IEnumerable<(string, string)> ValidateNumberPair(float[]? numberPair, string fieldName) + { + if (numberPair is null) + { + yield return (fieldName, $"{fieldName} is required."); + } + else if (numberPair.Length != 2) + { + yield return (fieldName, $"{fieldName} must have exactly 2 elements."); + } + else + { + foreach (var item in ValidateNumber(numberPair[0], $"{fieldName}[0]")) + yield return item; + + foreach (var item in ValidateNumber(numberPair[1], $"{fieldName}[1]")) + yield return item; + } + } + + private IEnumerable<(string, string)> ValidateNumber(float? number, string fieldName, double min = 0.0, double max = 1.0) + { + if (number is null) + { + yield return (fieldName, $"{fieldName} is required."); + } + else if (number < min) + { + yield return (fieldName, $"{fieldName} must be greater than or equal to {min}."); + } + else if (number > max) + { + yield return (fieldName, $"{fieldName} must be less than or equal to {max}."); + } + } +} \ No newline at end of file diff --git a/App/Program.cs b/App/Program.cs new file mode 100644 index 0000000..8bec90f --- /dev/null +++ b/App/Program.cs @@ -0,0 +1,28 @@ +using System.Text.Json.Serialization; +using StitchATon2.App.Controllers; +using StitchATon2.App.Models; +using StitchATon2.Domain; + +var builder = WebApplication.CreateSlimBuilder(args); + +using var tileManager = new TileManager(Configuration.Default); +builder.Services.AddSingleton(tileManager); + +builder.Services.ConfigureHttpJsonOptions(options => +{ + options.SerializerOptions.TypeInfoResolverChain.Insert(0, AppJsonSerializerContext.Default); +}); + +var app = builder.Build(); + +app.MapPost("/api/image/generate", ImageController.GenerateImage); +app.MapGet("/api/image/generate/random", ImageController.GenerateRandomImage); + +app.Run(); + +[JsonSourceGenerationOptions(WriteIndented = false)] +[JsonSerializable(typeof(GenerateImageDto))] +[JsonSerializable(typeof(Dictionary>))] +internal partial class AppJsonSerializerContext : JsonSerializerContext +{ +} \ No newline at end of file diff --git a/App/Properties/launchSettings.json b/App/Properties/launchSettings.json new file mode 100644 index 0000000..eb3fc05 --- /dev/null +++ b/App/Properties/launchSettings.json @@ -0,0 +1,16 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "todos", + "applicationUrl": "http://localhost:5088", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "ASSET_PATH_RO": "C:\\Storage\\tiles1705" + } + } + } +} diff --git a/App/StitchATon2.App.csproj b/App/StitchATon2.App.csproj new file mode 100644 index 0000000..4b7d0a2 --- /dev/null +++ b/App/StitchATon2.App.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + true + true + StitchATon2.App + + + + + + + + + + diff --git a/App/Utils.cs b/App/Utils.cs new file mode 100644 index 0000000..bf13bbd --- /dev/null +++ b/App/Utils.cs @@ -0,0 +1,22 @@ +using StitchATon2.App.Models; +using StitchATon2.Domain; +using StitchATon2.Domain.ImageCreators; + +namespace StitchATon2.App; + +public static class Utils +{ + public static GridSection CreateSection(this TileManager manager, GenerateImageDto dto) + => manager.CreateSection( + dto.CanvasRect!, + dto.CropOffset![0], + dto.CropOffset![1], + dto.CropSize![0], + dto.CropSize![1]); + + public static async Task WriteToStream(this GridSection section, Stream stream, float? scale) + { + var imageCreator = new ImageCreator(section); + await imageCreator.WriteToStream(stream, scale!.Value); + } +} \ No newline at end of file diff --git a/App/appsettings.Development.json b/App/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/App/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/App/appsettings.json b/App/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/App/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/Domain/Configuration.cs b/Domain/Configuration.cs new file mode 100644 index 0000000..644a6bc --- /dev/null +++ b/Domain/Configuration.cs @@ -0,0 +1,55 @@ +namespace StitchATon2.Domain; + +public class Configuration +{ + public required string AssetPath { get; init; } + public required string CachePath { get; init; } + + public required int Columns { get; init; } + public required int Rows { get; init; } + + public required int Width { get; init; } + public required int Height { get; init; } + + + public int FullWidth => Width * Columns; + public int FullHeight => Height * Rows; + + public int BottomTileIndex => Height - 1; + public int RightTileIndex => Width - 1; + + 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 cachePath = Path.Combine(Path.GetTempPath(), "d42df2a2-60ac-4dc3-a6b9-d4c04f2e08e6"); + return new Configuration + { + AssetPath = assetPath, + CachePath = cachePath, + Columns = 55, + Rows = 31, + Width = 720, + Height = 720, + ImageCacheCapacity = 5, + IntegralCacheCapacity = 10, + }; + } + } + + public string GetAssetPath(string assetName) + { + return Path.Combine(AssetPath, assetName); + } + + public string GetCachePath(string assetName) + { + return Path.Combine(CachePath, assetName); + } +} \ No newline at end of file diff --git a/Domain/GridSection.cs b/Domain/GridSection.cs new file mode 100644 index 0000000..72d1a59 --- /dev/null +++ b/Domain/GridSection.cs @@ -0,0 +1,51 @@ +namespace StitchATon2.Domain; + +public class GridSection +{ + public TileManager TileManager { get; } + + public int Width { get; } + public int Height { get; } + public int OffsetX { get; } + public int OffsetY { get; } + + public Tile Origin { get; } + + public GridSection( + TileManager tileManager, + string coordinatePair, + float cropX, + float cropY, + float cropWidth, + float cropHeight) + { + TileManager = tileManager; + var config = tileManager.Configuration; + + var (tile0, tile1) = tileManager.GetTilePair(coordinatePair); + + var (col0, col1) = tile0.Column < tile1.Column + ? (tile0.Column, tile1.Column) + : (tile1.Column, tile0.Column); + + var (row0, row1) = tile0.Row < tile1.Row + ? (tile0.Row, tile1.Row) + : (tile1.Row, tile0.Row); + + var gridWidth = (col1 - col0 + 1) * config.Width; + var gridHeight = (row1 - row0 + 1) * config.Height; + + var x0 = (int)(gridWidth * cropX); + var y0 = (int)(gridHeight * cropY); + var x1 = (int)(gridWidth * (cropWidth + cropX)); + var y1 = (int)(gridHeight * (cropHeight + cropY)); + + Width = x1 - x0; + Height = y1 - y0; + + (var columnOffset, OffsetX) = Math.DivRem(x0, config.Width); + (var rowOffset, OffsetY) = Math.DivRem(y0, config.Height); + + Origin = tileManager.GetTile(col0 + columnOffset, row0 + rowOffset); + } +} \ No newline at end of file diff --git a/Domain/ImageCreators/ImageCreator.cs b/Domain/ImageCreators/ImageCreator.cs new file mode 100644 index 0000000..d634c4d --- /dev/null +++ b/Domain/ImageCreators/ImageCreator.cs @@ -0,0 +1,155 @@ +using System.Buffers; +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 +{ + 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 Int32Pixel[] _mmfReadBuffer; + + public ImageCreator(GridSection section) + { + _section = section; + _mmfReadBuffer = ArrayPool.Shared.Rent(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); + 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); + 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); + } + } +} \ No newline at end of file diff --git a/Domain/StitchATon2.Domain.csproj b/Domain/StitchATon2.Domain.csproj new file mode 100644 index 0000000..6e68f4d --- /dev/null +++ b/Domain/StitchATon2.Domain.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/Domain/Tile.cs b/Domain/Tile.cs new file mode 100644 index 0000000..5ce26bb --- /dev/null +++ b/Domain/Tile.cs @@ -0,0 +1,15 @@ +using StitchATon2.Infra; + +namespace StitchATon2.Domain; + +public class Tile +{ + public required int Id { get; init; } + + public required int Column { get; init; } + public required int Row { get; init; } + + public required string Coordinate { get; init; } + + public required ImageIntegral Integral { get; init; } +} \ No newline at end of file diff --git a/Domain/TileManager.cs b/Domain/TileManager.cs new file mode 100644 index 0000000..9bb20b1 --- /dev/null +++ b/Domain/TileManager.cs @@ -0,0 +1,108 @@ +using System.Buffers; +using System.Runtime.CompilerServices; +using StitchATon2.Infra; +using StitchATon2.Infra.Buffers; + +namespace StitchATon2.Domain; + +public sealed class TileManager : IDisposable +{ + private readonly IMemoryOwner _tiles; + + public Configuration Configuration { get; } + + public TileManager(Configuration config) + { + Configuration = config; + _tiles = MemoryAllocator.AllocateManaged(config.TileCount); + var tilesSpan = _tiles.Memory.Span; + for (var id = 0; id < config.TileCount; id++) + tilesSpan[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}"; + return new Tile + { + Id = id, + Row = row, + Column = column, + Coordinate = coordinate, + Integral = new ImageIntegral( + imagePath: Configuration.GetAssetPath($"{coordinate}.png"), + outputDirectory: Configuration.CachePath, + width: Configuration.Width, + height: Configuration.Height + ), + }; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + 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]; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Tile GetTile(int column, int row) => GetTile(GetId(column, row)); + + public Tile GetTile(string coordinate) + { + var (column, row) = Utils.GetSBSCoordinate(coordinate); + return GetTile(column, row); + } + + public Tile? TryGetAdjacent(Tile tile, int columnOffset, int rowOffset) + { + var column = tile.Column + columnOffset; + if(column <= 0 || column > Configuration.Columns) + return null; + + var row = tile.Row + rowOffset; + if(row <= 0 || row > Configuration.Rows) + return null; + + return GetTile(column, row); + } + + public (Tile TopLeft, Tile BottomRight) GetTilePair(string coordinatePair) + { + var index = coordinatePair.IndexOf(':'); + var topLeft = GetTile(coordinatePair[..index++]); + var bottomRight = GetTile(coordinatePair[index..]); + + return (topLeft, bottomRight); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Tile GetAdjacent(Tile tile, int columnOffset, int rowOffset) + { + return GetTile(tile.Column + columnOffset, tile.Row + rowOffset); + } + + public GridSection CreateSection( + string coordinatePair, + float cropX, + float cropY, + float cropWidth, + float cropHeight) + => new( + this, + coordinatePair, + cropX, + 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 new file mode 100644 index 0000000..b3bd3d0 --- /dev/null +++ b/Domain/Utils.cs @@ -0,0 +1,84 @@ +using System.Diagnostics.Contracts; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using StitchATon2.Infra.Buffers; + +namespace StitchATon2.Domain; + +public static class Utils +{ + [Pure] + public static string GetSBSNotation(int row) + => row <= 26 + ? new string([(char)(row + 'A' - 1)]) + : new string(['A', (char)(row + 'A' - 27)]); + + [Pure, MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int DivCeil(int a, int b) + { + return (a + b - 1) / b; + } + + public static (int Column, int Row) GetSBSCoordinate(string coordinate) + { + var column = coordinate[^1] - '0'; + if(char.IsDigit(coordinate[^2])) + column += 10 * (coordinate[^2] - '0'); + + var row = char.IsLetter(coordinate[1]) + ? 26 + coordinate[1] - 'A' + 1 + : coordinate[0] - 'A' + 1; + + return (column, row); + } + + + public static IBuffer BoundsMatrix(float scaleFactor, int length, int max, int offset) + { + var vectorSize = DivCeil(length, Vector.Count); + using var buffer = MemoryAllocator.Allocate>(vectorSize); + + var span = buffer.Span; + var vectorMin = Vector.Zero; + var vectorOffset = new Vector(offset - 1); + var vectorMax = new Vector(max - 1); + var vectorScale = new Vector(scaleFactor); + + var vectorSequence = SequenceVector(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.Ceiling(span[i]); + } + + var result = MemoryAllocator.Allocate(vectorSize * Vector.Count); + var resultSpan = MemoryMarshal.Cast>(result.Span); + for (var i = 0; i < vectorSize; i++) + { + 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); + } + + 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/ArrayOwner.cs b/Infra/Buffers/ArrayOwner.cs new file mode 100644 index 0000000..2c17cb5 --- /dev/null +++ b/Infra/Buffers/ArrayOwner.cs @@ -0,0 +1,29 @@ +using System.Buffers; + +namespace StitchATon2.Infra.Buffers; + +public class ArrayOwner : IBuffer where T : unmanaged +{ + private readonly ArrayPool _owner; + private readonly T[] _buffer; + + public ArrayOwner(ArrayPool owner, int size) + { + _owner = owner; + _buffer = owner.Rent(size); + } + + ~ArrayOwner() => Dispose(); + + public void Dispose() + { + _owner.Return(_buffer); + GC.SuppressFinalize(this); + } + + public ref T this[int index] => ref _buffer[index]; + + public Span Span => _buffer; + + public T[] Array => _buffer; +} \ No newline at end of file diff --git a/Infra/Buffers/IBuffer.cs b/Infra/Buffers/IBuffer.cs new file mode 100644 index 0000000..2eff74b --- /dev/null +++ b/Infra/Buffers/IBuffer.cs @@ -0,0 +1,8 @@ +namespace StitchATon2.Infra.Buffers; + +public interface IBuffer : IDisposable where T : unmanaged +{ + ref T this[int index] { get; } + + Span Span { get; } +} \ No newline at end of file diff --git a/Infra/Buffers/MemoryAllocator.cs b/Infra/Buffers/MemoryAllocator.cs new file mode 100644 index 0000000..43c1590 --- /dev/null +++ b/Infra/Buffers/MemoryAllocator.cs @@ -0,0 +1,15 @@ +using System.Buffers; + +namespace StitchATon2.Infra.Buffers; + +public static class MemoryAllocator +{ + public static IBuffer Allocate(int count) where T : unmanaged + => new UnmanagedMemory(count); + + public static IMemoryOwner AllocateManaged(int count) + => MemoryPool.Shared.Rent(count); + + public static ArrayOwner AllocateArray(int count) where T : unmanaged + => new(ArrayPool.Shared, count); +} \ No newline at end of file diff --git a/Infra/Buffers/UnmanagedMemory.cs b/Infra/Buffers/UnmanagedMemory.cs new file mode 100644 index 0000000..4bf6064 --- /dev/null +++ b/Infra/Buffers/UnmanagedMemory.cs @@ -0,0 +1,28 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace StitchATon2.Infra.Buffers; + +internal sealed unsafe class UnmanagedMemory : IBuffer where T : unmanaged +{ + private readonly void* _pointer; + private readonly int _count; + + public ref T this[int index] => ref Unsafe.AsRef((T*)_pointer + index); // *((T*)_pointer + index); + + public Span Span => new(_pointer, _count); + + public UnmanagedMemory(int count) + { + _pointer = NativeMemory.Alloc((nuint)count, (nuint)Unsafe.SizeOf()); + _count = count; + } + + ~UnmanagedMemory() => Dispose(); + + public void Dispose() + { + NativeMemory.Free(_pointer); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/Infra/Encoders/Crc32.cs b/Infra/Encoders/Crc32.cs new file mode 100644 index 0000000..e3f106a --- /dev/null +++ b/Infra/Encoders/Crc32.cs @@ -0,0 +1,33 @@ +namespace StitchATon2.Infra.Encoders; + +public static class Crc32 +{ + private static readonly Lazy LazyTable = new(GenerateTable); + private static uint[] Table => LazyTable.Value; + + public static uint Compute(Span buffer, uint initial = 0xFFFFFFFF) + { + uint crc = initial; + foreach (var b in buffer) + { + crc = Table[(crc ^ b) & 0xFF] ^ (crc >> 8); + } + return ~crc; + } + + private static uint[] GenerateTable() + { + const uint poly = 0xEDB88320; + var table = new uint[256]; + for (uint i = 0; i < 256; i++) + { + uint c = i; + for (int j = 0; j < 8; j++) + { + c = (c & 1) != 0 ? (poly ^ (c >> 1)) : (c >> 1); + } + table[i] = c; + } + return table; + } +} diff --git a/Infra/Encoders/PngStreamEncoder.cs b/Infra/Encoders/PngStreamEncoder.cs new file mode 100644 index 0000000..3b70cb4 --- /dev/null +++ b/Infra/Encoders/PngStreamEncoder.cs @@ -0,0 +1,143 @@ +using System.Buffers.Binary; +using System.IO.Compression; + +namespace StitchATon2.Infra.Encoders; + +public class PngStreamEncoder : IDisposable, IAsyncDisposable +{ + private const int BufferSize = 8 * 1024; + private const int FlushThreshold = 1024; + + private readonly Stream _stream; + private readonly MemoryStream _memoryStream; + private readonly int _width; + private readonly int _height; + + private readonly ZLibStream _zlibStream; + private bool _disposed; + private bool _shouldFlush; + + public PngStreamEncoder(Stream writableStream, int width, int height) + { + _stream = writableStream; + _width = width; + _height = height; + _memoryStream = new MemoryStream(BufferSize * 2); + _zlibStream = new ZLibStream(_memoryStream, CompressionLevel.Optimal, leaveOpen: true); + _memoryStream.SetLength(8); + _memoryStream.Position = 8; + } + + ~PngStreamEncoder() => Dispose(); + + public async Task WriteHeader() + { + byte[] 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.AsSpan(16), _width); + BinaryPrimitives.WriteInt32BigEndian(headerBytes.AsSpan(20), _height); + var crc = Crc32.Compute(headerBytes.AsSpan(12, 17)); + + BinaryPrimitives.WriteUInt32BigEndian(headerBytes.AsSpan(29), crc); + + await _stream.WriteAsync(headerBytes); + } + + public async Task WriteData(Memory data) + { + _zlibStream.Write([0]); + + var dataSlice = data; + while (dataSlice.Length > FlushThreshold) + { + await _zlibStream.WriteAsync(dataSlice[..FlushThreshold]); + await _zlibStream.FlushAsync(); + dataSlice = dataSlice[FlushThreshold..]; + if(_memoryStream.Length >= BufferSize) + await Flush(); + } + + if (dataSlice.Length > 0) + { + await _zlibStream.WriteAsync(dataSlice); + await _zlibStream.FlushAsync(); + _shouldFlush = true; + } + } + + private async Task Flush() + { + await _zlibStream.FlushAsync(); + 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); + + await _stream.WriteAsync(buffer.AsMemory(0, dataSize + 12)); + _memoryStream.SetLength(8); + _memoryStream.Position = 8; + _shouldFlush = false; + } + + public async ValueTask WriteEndOfFile() + { + if(_shouldFlush) + await Flush(); + + var endChunk = new byte[] { + 0x00, 0x00, 0x00, 0x00, // Length + 0x49, 0x45, 0x4E, 0x44, // IEND + 0xAE, 0x42, 0x60, 0x82, // Crc + }; + + await _stream.WriteAsync(endChunk); + await DisposeAsync(); + } + + public void Dispose() + { + if (!_disposed) + { + _zlibStream.Dispose(); + _memoryStream.Dispose(); + _disposed = true; + } + + GC.SuppressFinalize(this); + } + + public async ValueTask DisposeAsync() + { + if (!_disposed) + { + await _zlibStream.DisposeAsync(); + await _memoryStream.DisposeAsync(); + _disposed = true; + } + + GC.SuppressFinalize(this); + } +} diff --git a/Infra/ImageIntegral.cs b/Infra/ImageIntegral.cs new file mode 100644 index 0000000..49a8d49 --- /dev/null +++ b/Infra/ImageIntegral.cs @@ -0,0 +1,387 @@ +using System.Buffers; +using System.IO.MemoryMappedFiles; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.Formats; +using SixLabors.ImageSharp.Formats.Png; +using SixLabors.ImageSharp.PixelFormats; +using StitchATon2.Infra.Buffers; + +namespace StitchATon2.Infra; + +public class ImageIntegral : IDisposable +{ + private const int MaxProcessingQueue = 4; + + private readonly string _imagePath; + private readonly string _outputDirectory; + private readonly int _width; + private readonly int _height; + + private IMemoryOwner? _rowLocks; + private MemoryMappedFile? _memoryMappedFile; + private readonly object _lock = new(); + + private readonly ManualResetEventSlim _queueLock = new(true); + private readonly ManualResetEventSlim _initializationLock = new(false); + + private volatile int _processedRows; + private volatile int _queueCounter; + + public ImageIntegral(string imagePath, string outputDirectory, int width, int height) + { + _imagePath = imagePath; + _outputDirectory = outputDirectory; + _width = width; + _height = height; + } + + ~ImageIntegral() => Dispose(); + + public void Acquire(int row, Int32Pixel[] buffer, CancellationToken cancellationToken = default) + { + Acquire(row, cancellationToken); + ReadRow(row, buffer); + } + + public void Acquire(int row, Span buffer, CancellationToken cancellationToken = default) + { + Acquire(row, cancellationToken); + ReadRow(row, buffer); + } + + private void Acquire(int row, CancellationToken cancellationToken) + { + if (_memoryMappedFile is null) + { + lock (_lock) + { + if (_memoryMappedFile is null) + { + Task.Factory.StartNew(() => Initialize(cancellationToken), cancellationToken); + _initializationLock.Wait(cancellationToken); + _initializationLock.Dispose(); + } + } + } + + _rowLocks?.Memory.Span[row].Wait(cancellationToken); + } + + private void Initialize(CancellationToken cancellationToken) + { + var fileName = Path.GetFileNameWithoutExtension(_imagePath); + var path = Path.Combine(_outputDirectory, $"{fileName}.mmf"); + + var backedFileStream = InitializeBackedFile(path, out var header); + _processedRows = header.ProcessedRows; + + if (header.ProcessedRows >= _height) + { + // When statement above is true + // then it is guaranteed that backed file is valid and fully processed + _memoryMappedFile = MemoryMappedFile.CreateFromFile(path, FileMode.Open); + _initializationLock.Set(); + return; + } + + var taskQueue = backedFileStream == null + ? Task.CompletedTask + : AllocateBackedFile(backedFileStream, header); + + taskQueue = taskQueue.ContinueWith( + _ => + { + _memoryMappedFile = MemoryMappedFile.CreateFromFile(path, FileMode.Open); + _initializationLock.Set(); + }, + cancellationToken); + + // 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; + for (int i = 0; i < _height; i++) + { + var isOpen = i < header.ProcessedRows; + rowLocksSpan[i] = new ManualResetEventSlim(isOpen); + } + + _rowLocks = rowLocks; + ProcessIntegral(taskQueue, cancellationToken); + } + + private void ProcessIntegral(Task taskQueue, CancellationToken cancellationToken) + { + PngDecoderOptions decoderOptions = new() + { + PngCrcChunkHandling = PngCrcChunkHandling.IgnoreAll, + GeneralOptions = new DecoderOptions + { + MaxFrames = 1, + SkipMetadata = true, + } + }; + + using var fileStream = File.OpenRead(_imagePath); + using var image = PngDecoder.Instance.Decode(decoderOptions, fileStream); + var imageBuffer = image.Frames.RootFrame.PixelBuffer; + + var accumulator = Int32Pixel.Zero; + var buffer = MemoryAllocator.AllocateArray(_width); + var processedRows = _processedRows; + Interlocked.Exchange(ref _queueCounter, 0); + + // First row + if (processedRows == 0) + { + var sourceRow = imageBuffer.DangerousGetRowSpan(0); + for (var x = 0; x < sourceRow.Length; x++) + { + accumulator.Accumulate(sourceRow[x]); + buffer[x] = accumulator; + } + + taskQueue = QueueWriterTask(taskQueue, 0, buffer.Clone(_width), cancellationToken); + processedRows++; + } + else + { + ReadRow(processedRows - 1, buffer); + } + + if(cancellationToken.IsCancellationRequested) + return; + + var prevBuffer = buffer; + buffer = MemoryAllocator.AllocateArray(_width); + + for (int y = processedRows; y < image.Height; y++) + { + 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; + Array.Copy(buffer.Array, writeBuffer.Array, image.Width); + taskQueue = QueueWriterTask(taskQueue, y, writeBuffer, cancellationToken); + prevBuffer = buffer; + buffer = MemoryAllocator.AllocateArray(_width); + } + + buffer.Dispose(); + if(cancellationToken.IsCancellationRequested) + return; + + taskQueue = taskQueue.ContinueWith(task => + { + if (task.IsCompletedSuccessfully) + { + DisposeRowLocks(); + _queueLock.Dispose(); + } + }, cancellationToken); + + taskQueue.Wait(cancellationToken); + } + + private Task QueueWriterTask( + Task taskQueue, + int row, + ArrayOwner writeBuffer, + CancellationToken cancellationToken) + { + Interlocked.Increment(ref _queueCounter); + cancellationToken.Register(writeBuffer.Dispose); + return taskQueue.ContinueWith(_ => + { + using (var view = AcquireView(row, MemoryMappedFileAccess.Write)) + view.WriteArray(0, writeBuffer.Array, 0, _width); + + writeBuffer.Dispose(); + _rowLocks!.Memory.Span[row].Set(); + Interlocked.Increment(ref _processedRows); + + using (var view = AcquireHeaderView(MemoryMappedFileAccess.Write)) + view.Write(16, _processedRows); + + Interlocked.Decrement(ref _queueCounter); + _queueLock.Set(); + }, cancellationToken); + } + + private MemoryMappedViewAccessor AcquireHeaderView(MemoryMappedFileAccess access) + => _memoryMappedFile!.CreateViewAccessor(0, Header.Size, access); + + private MemoryMappedViewAccessor AcquireView(int row, MemoryMappedFileAccess access) + { + var size = _width * Int32Pixel.Size; + var offset = row * size + Header.Size; + return _memoryMappedFile!.CreateViewAccessor(offset, size, access); + } + + private void ReadRow(int row, Int32Pixel[] readBuffer) + { + using var view = AcquireView(row, MemoryMappedFileAccess.Read); + view.ReadArray(0, readBuffer, 0, _width); + } + + private void ReadRow(int row, Span buffer) + { + using var view = AcquireView(row, MemoryMappedFileAccess.Read); + view.DangerousReadSpan(0, buffer, 0, _width); + } + + private void ReadRow(int row, IBuffer 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); + + // Expectation when file exists: + // - throws IOException when it's being processed, handle it if possible + // - returns null if file is valid + // - delete the existing file if it's not valid (modified from external) + FileStream fs; + if (File.Exists(path)) + { + fs = File.OpenRead(path); + if (fs.Length < expectedHeader.Length + Header.Size) + { + fs.Dispose(); + File.Delete(path); + } + + Span headerBytes = stackalloc byte[Header.Size]; + fs.ReadExactly(headerBytes); + + header = MemoryMarshal.Cast(headerBytes)[0]; + var isValid = expectedHeader.Identifier == header.Identifier + && expectedHeader.Width == header.Width + && expectedHeader.Height == header.Height + && expectedHeader.Length == header.Length; + + fs.Dispose(); + Console.WriteLine($"Image integral file found: {path}"); + + if (!isValid) File.Delete(path); + else return null; + } + + // Expected process: + // - Initialize file creation + // - Write the header + // - Allocate initial content by Header.Length + // - Action above should be done asynchronously and return the task handle. + var fsOptions = new FileStreamOptions + { + Access = FileAccess.Write, + Share = FileShare.None, + Mode = FileMode.CreateNew, + PreallocationSize = Header.Size, + }; + + Console.WriteLine($"Create image integral file: {path}"); + + Directory.CreateDirectory(_outputDirectory); + fs = File.Open(path, fsOptions); + fs.Write(MemoryMarshal.AsBytes([expectedHeader])); + header = expectedHeader; + return fs; + } + + private static async Task AllocateBackedFile(FileStream fileStream, Header header) + { + // 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); + // } + + fileStream.SetLength(header.Length + Header.Size); + + await fileStream.DisposeAsync(); + } + + [StructLayout(LayoutKind.Sequential)] + private struct Header + { + private const int Signature = 0x47544e49; // INTG + + public static int Size => Unsafe.SizeOf
(); + + public uint Identifier; + public int Width; + public int Height; + public int Length; + public int ProcessedRows; + + public static Header CreateInitial(int width, int height) => new() + { + Identifier = Signature, + Width = width, + Height = height, + Length = width * height * Int32Pixel.Size, + ProcessedRows = 0, + }; + } + + private void DisposeRowLocks() + { + if (_rowLocks is { } locks) + { + _rowLocks = null; + var lockSpan = locks.Memory.Span; + for(int i = 0; i < _height; i++) + lockSpan[i].Dispose(); + + locks.Dispose(); + } + } + + public void Dispose() + { + DisposeRowLocks(); + _memoryMappedFile?.Dispose(); + _queueLock.Dispose(); + _initializationLock.Dispose(); + GC.SuppressFinalize(this); + } +} \ No newline at end of file diff --git a/Infra/Int32Pixel.cs b/Infra/Int32Pixel.cs new file mode 100644 index 0000000..836d378 --- /dev/null +++ b/Infra/Int32Pixel.cs @@ -0,0 +1,82 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats; + +namespace StitchATon2.Infra; + +[StructLayout(LayoutKind.Sequential)] +public record struct Int32Pixel +{ + public static int Size + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => Unsafe.SizeOf(); + } + + public static Int32Pixel Zero + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new(); + } + + public int R; + public int G; + public int B; + + public Int32Pixel(int r, int g, int b) + { + R = r; + G = g; + B = b; + } + + public void Accumulate(Int32Pixel pixel) + { + R += pixel.R; + G += pixel.G; + B += pixel.B; + } + + public void Accumulate(Rgb24 pixel) + { + R += pixel.R; + G += pixel.G; + B += pixel.B; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Int32Pixel operator +(Int32Pixel a, Int32Pixel b) + { + return new Int32Pixel(a.R + b.R, a.G + b.G, a.B + b.B); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Int32Pixel operator +(Int32Pixel a, int b) + { + return new Int32Pixel(a.R + b, a.G + b, a.B + b); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Int32Pixel operator -(Int32Pixel a, Int32Pixel b) + { + return new Int32Pixel(a.R - b.R, a.G - b.G, a.B - b.B); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Int32Pixel operator /(Int32Pixel a, int b) + { + return new Int32Pixel(a.R / b, a.G / b, a.B / b); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static explicit operator Int32Pixel(Rgb24 pixel) + { + return new Int32Pixel(pixel.R, pixel.G, pixel.B); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static explicit operator Rgb24(Int32Pixel pixel) + { + return new Rgb24((byte)pixel.R, (byte)pixel.G, (byte)pixel.B); + } +} \ No newline at end of file diff --git a/Infra/StitchATon2.Infra.csproj b/Infra/StitchATon2.Infra.csproj new file mode 100644 index 0000000..6e6dca5 --- /dev/null +++ b/Infra/StitchATon2.Infra.csproj @@ -0,0 +1,14 @@ + + + + net8.0 + enable + enable + true + + + + + + + diff --git a/Infra/Synchronization/TaskHelper.cs b/Infra/Synchronization/TaskHelper.cs new file mode 100644 index 0000000..aa10870 --- /dev/null +++ b/Infra/Synchronization/TaskHelper.cs @@ -0,0 +1,12 @@ +namespace StitchATon2.Infra.Synchronization; + +public static class TaskHelper +{ + public static TaskFactory CreateTaskFactory() + { + return new TaskFactory( + TaskCreationOptions.AttachedToParent, + TaskContinuationOptions.ExecuteSynchronously + ); + } +} \ No newline at end of file diff --git a/Infra/Utils.cs b/Infra/Utils.cs new file mode 100644 index 0000000..a67462c --- /dev/null +++ b/Infra/Utils.cs @@ -0,0 +1,56 @@ +using System.IO.MemoryMappedFiles; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using StitchATon2.Infra.Buffers; + +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) + where T : unmanaged + { + uint sizeOfT = AlignedSizeOf(); + int n = count; + long spaceLeft = view.Capacity - position; + if (spaceLeft < 0) + { + n = 0; + } + else + { + ulong spaceNeeded = (ulong)(sizeOfT * count); + if ((ulong)spaceLeft < spaceNeeded) + { + n = (int)(spaceLeft / sizeOfT); + } + } + + var byteOffset = (ulong)(view.GetOffset() + position); + view.GetSafeBuffer().ReadSpan(byteOffset, span.Slice(offset, n)); + } + + public static ArrayOwner Clone(this ArrayOwner arrayOwner, int length) where T : unmanaged + { + var newArrayOwner = MemoryAllocator.AllocateArray(length); + 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/StitchATon2.sln b/StitchATon2.sln new file mode 100644 index 0000000..31e5c57 --- /dev/null +++ b/StitchATon2.sln @@ -0,0 +1,28 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StitchATon2.App", "App\StitchATon2.App.csproj", "{71732301-8708-4B76-BD37-0E736B58BA0B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StitchATon2.Domain", "Domain\StitchATon2.Domain.csproj", "{A6FF5EFD-E4EA-4E66-8EF6-A7668D64F0DC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StitchATon2.Infra", "Infra\StitchATon2.Infra.csproj", "{E602F3FC-6139-4B30-AC5A-75815E6340A4}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {71732301-8708-4B76-BD37-0E736B58BA0B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {71732301-8708-4B76-BD37-0E736B58BA0B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {71732301-8708-4B76-BD37-0E736B58BA0B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {71732301-8708-4B76-BD37-0E736B58BA0B}.Release|Any CPU.Build.0 = Release|Any CPU + {A6FF5EFD-E4EA-4E66-8EF6-A7668D64F0DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A6FF5EFD-E4EA-4E66-8EF6-A7668D64F0DC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A6FF5EFD-E4EA-4E66-8EF6-A7668D64F0DC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A6FF5EFD-E4EA-4E66-8EF6-A7668D64F0DC}.Release|Any CPU.Build.0 = Release|Any CPU + {E602F3FC-6139-4B30-AC5A-75815E6340A4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 + EndGlobalSection +EndGlobal diff --git a/global.json b/global.json new file mode 100644 index 0000000..2ddda36 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "8.0.0", + "rollForward": "latestMinor", + "allowPrerelease": false + } +} \ No newline at end of file