Compare commits

..

No commits in common. "main" and "v1.0.0" have entirely different histories.
main ... v1.0.0

31 changed files with 363 additions and 1091 deletions

View file

@ -6,11 +6,7 @@ namespace StitchATon2.App.Controllers;
public static class ImageController public static class ImageController
{ {
public static async Task GenerateImage( public static async Task GenerateImage(HttpResponse response, GenerateImageDto dto, TileManager tileManager)
HttpResponse response,
GenerateImageDto dto,
TileManager tileManager,
CancellationToken cancellationToken)
{ {
if (dto.GetErrors() is { Count: > 0 } errors) if (dto.GetErrors() is { Count: > 0 } errors)
{ {
@ -18,7 +14,7 @@ public static class ImageController
response.ContentType = "text/json"; response.ContentType = "text/json";
var errorBody = JsonSerializer.Serialize(errors, AppJsonSerializerContext.Default.DictionaryStringListString); var errorBody = JsonSerializer.Serialize(errors, AppJsonSerializerContext.Default.DictionaryStringListString);
response.ContentLength = errorBody.Length; response.ContentLength = errorBody.Length;
await response.WriteAsync(errorBody, cancellationToken: cancellationToken); await response.WriteAsync(errorBody);
await response.CompleteAsync(); await response.CompleteAsync();
return; return;
} }
@ -26,19 +22,14 @@ public static class ImageController
response.StatusCode = 200; response.StatusCode = 200;
response.ContentType = "image/png"; response.ContentType = "image/png";
Console.WriteLine($"Generate image for {dto}");
await tileManager await tileManager
.CreateSection(dto) .CreateSection(dto)
.WriteToPipe(response.BodyWriter, dto.OutputScale, cancellationToken); .WriteToStream(response.Body, dto.OutputScale);
await response.CompleteAsync(); await response.CompleteAsync();
} }
public static async Task GenerateRandomImage( public static async Task GenerateRandomImage(HttpResponse response, TileManager tileManager)
HttpResponse response,
TileManager tileManager,
CancellationToken cancellationToken)
{ {
response.StatusCode = 200; response.StatusCode = 200;
response.ContentType = "image/png"; response.ContentType = "image/png";
@ -56,7 +47,7 @@ public static class ImageController
var scale = float.Clamp(480f / int.Max(section.Width, section.Height), 0.01f, 1f); var scale = float.Clamp(480f / int.Max(section.Width, section.Height), 0.01f, 1f);
Console.WriteLine($"Generate random image for {coordinatePair} scale: {scale}"); Console.WriteLine($"Generate random image for {coordinatePair} scale: {scale}");
await section.WriteToPipe(response.BodyWriter, scale, cancellationToken); await section.WriteToStream(response.Body, scale);
await response.CompleteAsync(); await response.CompleteAsync();
} }
} }

View file

@ -69,9 +69,4 @@ public class GenerateImageDto
yield return (fieldName, $"{fieldName} must be less than or equal to {max}."); 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}";
}
} }

View file

@ -5,7 +5,7 @@ using StitchATon2.Domain;
var builder = WebApplication.CreateSlimBuilder(args); var builder = WebApplication.CreateSlimBuilder(args);
var tileManager = new TileManager(Configuration.Default); using var tileManager = new TileManager(Configuration.Default);
builder.Services.AddSingleton(tileManager); builder.Services.AddSingleton(tileManager);
builder.Services.ConfigureHttpJsonOptions(options => builder.Services.ConfigureHttpJsonOptions(options =>

View file

@ -1,12 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk.Web"> <Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<InvariantGlobalization>true</InvariantGlobalization> <InvariantGlobalization>true</InvariantGlobalization>
<RootNamespace>StitchATon2.App</RootNamespace>
<PublishAot>true</PublishAot> <PublishAot>true</PublishAot>
<RootNamespace>StitchATon2.App</RootNamespace>
<!-- <EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>-->
<!-- <CompilerGeneratedFilesOutputPath>Generated</CompilerGeneratedFilesOutputPath>-->
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View file

@ -1,4 +1,3 @@
using System.IO.Pipelines;
using StitchATon2.App.Models; using StitchATon2.App.Models;
using StitchATon2.Domain; using StitchATon2.Domain;
using StitchATon2.Domain.ImageCreators; using StitchATon2.Domain.ImageCreators;
@ -15,23 +14,9 @@ public static class Utils
dto.CropSize![0], dto.CropSize![0],
dto.CropSize![1]); dto.CropSize![1]);
public static async Task WriteToStream( public static async Task WriteToStream(this GridSection section, Stream stream, float? scale)
this GridSection section,
Stream stream,
float? scale,
CancellationToken cancellationToken = default)
{ {
using var imageCreator = new DangerousImageCreator(section); var imageCreator = new ImageCreator(section);
await imageCreator.WriteToStream(stream, scale!.Value, cancellationToken); await imageCreator.WriteToStream(stream, scale!.Value);
}
public static async Task WriteToPipe(
this GridSection section,
PipeWriter pipeWriter,
float? scale,
CancellationToken cancellationToken = default)
{
using var imageCreator = new DangerousImageCreator(section);
await imageCreator.WriteToPipe(pipeWriter, scale!.Value, cancellationToken);
} }
} }

View file

@ -20,20 +20,25 @@ public class Configuration
public int TileCount => Columns * Rows; public int TileCount => Columns * Rows;
public required int ImageCacheCapacity { get; init; }
public required int IntegralCacheCapacity { get; init; }
public static Configuration Default public static Configuration Default
{ {
get 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"); var cachePath = Path.Combine(Path.GetTempPath(), "d42df2a2-60ac-4dc3-a6b9-d4c04f2e08e6");
return new Configuration return new Configuration
{ {
AssetPath = assetPath!, AssetPath = assetPath,
CachePath = cachePath, CachePath = cachePath,
Columns = 55, Columns = 55,
Rows = 31, Rows = 31,
Width = 720, Width = 720,
Height = 720, Height = 720,
ImageCacheCapacity = 5,
IntegralCacheCapacity = 10,
}; };
} }
} }

View file

@ -47,10 +47,5 @@ public class GridSection
(var rowOffset, OffsetY) = Math.DivRem(y0, config.Height); (var rowOffset, OffsetY) = Math.DivRem(y0, config.Height);
Origin = tileManager.GetTile(col0 + columnOffset, row0 + rowOffset); 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();
} }
} }

View file

@ -1,234 +0,0 @@
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<Int32Pixel> _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<Int32Pixel>(TileWidth);
}
~DangerousImageCreator() => Dispose();
private Task Create(float scale, Action<IBuffer<byte>> writeRowCallback, CancellationToken cancellationToken)
{
var scaleFactor = MathF.ReciprocalEstimate(scale);
var targetWidth = (int)(Width / scaleFactor);
var targetHeight = (int)(Height / scaleFactor);
if (targetHeight == 0 || targetWidth == 0)
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<Int32Pixel>(targetWidth + 1);
using var yEndMap = MemoryAllocator.Allocate<Int32Pixel>(targetWidth + 1);
var outputBufferSize = targetWidth * Unsafe.SizeOf<Rgb24>();
// Use pixel referencing to eliminate type casting
var pxInt32 = Int32Pixel.Zero;
ref var px = ref pxInt32;
ref var rChannel = ref Unsafe.As<Int32Pixel, byte>(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);
for (var y = 1; y <= targetHeight; y++)
{
var yStart = yLookup[y - 1];
var yEnd = yLookup[y];
var (localRow0, localOffsetY0) = int.DivRem(yStart, TileHeight);
MapRow(localRow0, localOffsetY0, xLookup, targetWidth + 1, yStartMap);
var (localRow1, localOffsetY1) = int.DivRem(yEnd, TileHeight);
MapRow(localRow1, localOffsetY1, xLookup, targetWidth + 1, yEndMap);
// Cross row
if (localRow0 != localRow1)
MapRow(localRow0, BottomPixelIndex, xLookup, targetWidth + 1, yEndMap, true);
var outputBuffer = MemoryAllocator.Allocate<byte>(outputBufferSize);
ref var outputChannel = ref outputBuffer.Span[0];
var boxHeight = yEnd - yStart;
for (int x1 = 1, x0 = 0; x1 <= targetWidth; x0 = x1++)
{
var xStart = xLookup[x1 - 1];
var 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);
}
if (cancellationToken.IsCancellationRequested)
{
outputBuffer.Dispose();
return Task.FromCanceled(cancellationToken);
}
cancellationToken.Register(outputBuffer.Dispose);
taskQueue = taskQueue
.ContinueWith(
_ => writeRowCallback.Invoke(outputBuffer),
cancellationToken,
TaskContinuationOptions.OnlyOnRanToCompletion,
TaskScheduler.Current);
}
return taskQueue;
}
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();
await Create(scale,
output => encoder
.WriteDataAsync(output, cancellationToken: cancellationToken)
.Wait(cancellationToken),
cancellationToken);
await encoder.WriteEndOfFileAsync(cancellationToken);
}
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);
await encoder.WriteEndOfFileAsync(cancellationToken);
}
private void MapRow(
int rowOffset,
int yOffset,
IBuffer<int> boundsMatrix,
int count,
IBuffer<Int32Pixel> 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 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;
currentTile = TileManager.TryGetAdjacent(currentTile, 1, 0);
if (currentTile == null)
{
destinationSpan[written] = destinationSpan[written - 1];
break;
}
xAdder += readBufferSpan[RightmostPixelIndex];
xOffset += TileWidth;
}
}
public void Dispose()
{
_mmfReadBuffer.Dispose();
GC.SuppressFinalize(this);
}
}

View file

@ -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<Int32Pixel>.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<Rgb24>();
using var outputBuffer = MemoryAllocator.AllocateManaged<byte>(outputBufferSize);
using var xLookup = Utils.BoundsMatrix(scaleFactor, targetWidth, FullWidth, OffsetX);
using var yLookup = Utils.BoundsMatrix(scaleFactor, targetHeight, FullHeight, OffsetY);
using var yStartMap = MemoryAllocator.Allocate<Int32Pixel>(targetWidth);
using var yEndMap = MemoryAllocator.Allocate<Int32Pixel>(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<int> sourceMap, IBuffer<Int32Pixel> 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<int> sourceMap, IBuffer<Int32Pixel> 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);
}
}
}

View file

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
</PropertyGroup> </PropertyGroup>

View file

@ -5,26 +5,29 @@ using StitchATon2.Infra.Buffers;
namespace StitchATon2.Domain; namespace StitchATon2.Domain;
public sealed class TileManager public sealed class TileManager : IDisposable
{ {
private readonly Tile[] _tiles; private readonly IMemoryOwner<Tile> _tiles;
public Configuration Configuration { get; } public Configuration Configuration { get; }
public TileManager(Configuration config) public TileManager(Configuration config)
{ {
Configuration = config; Configuration = config;
_tiles = new Tile[Configuration.TileCount]; _tiles = MemoryAllocator.AllocateManaged<Tile>(config.TileCount);
var tilesSpan = _tiles.Memory.Span;
for (var id = 0; id < config.TileCount; id++) for (var id = 0; id < config.TileCount; id++)
_tiles[id] = CreateTile(id); tilesSpan[id] = CreateTile(id);
Console.WriteLine("Tile manager created"); Console.WriteLine("Tile manager created");
} }
~TileManager() => Dispose();
private Tile CreateTile(int id) private Tile CreateTile(int id)
{ {
var (row, column) = int.DivRem(id, Configuration.Columns); var (row, column) = int.DivRem(id, Configuration.Columns);
var coordinate = $"{Utils.GetSbsNotationRow(++row)}{++column}"; var coordinate = $"{Utils.GetSBSNotation(++row)}{++column}";
return new Tile return new Tile
{ {
Id = id, Id = id,
@ -44,14 +47,14 @@ public sealed class TileManager
private int GetId(int column, int row) => column - 1 + (row - 1) * Configuration.Columns; private int GetId(int column, int row) => column - 1 + (row - 1) * Configuration.Columns;
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public Tile GetTile(int id) => _tiles[id]; public Tile GetTile(int id) => _tiles.Memory.Span[id];
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public Tile GetTile(int column, int row) => GetTile(GetId(column, row)); public Tile GetTile(int column, int row) => GetTile(GetId(column, row));
public Tile GetTile(string coordinate) public Tile GetTile(string coordinate)
{ {
var (column, row) = Utils.GetSbsNotationCoordinate(coordinate); var (column, row) = Utils.GetSBSCoordinate(coordinate);
return GetTile(column, row); return GetTile(column, row);
} }
@ -96,4 +99,10 @@ public sealed class TileManager
cropY, cropY,
cropWidth, cropWidth,
cropHeight); cropHeight);
public void Dispose()
{
_tiles.Dispose();
GC.SuppressFinalize(this);
}
} }

View file

@ -9,7 +9,7 @@ namespace StitchATon2.Domain;
public static class Utils public static class Utils
{ {
[Pure] [Pure]
public static string GetSbsNotationRow(int row) public static string GetSBSNotation(int row)
=> row <= 26 => row <= 26
? new string([(char)(row + 'A' - 1)]) ? new string([(char)(row + 'A' - 1)])
: new string(['A', (char)(row + 'A' - 27)]); : new string(['A', (char)(row + 'A' - 27)]);
@ -20,8 +20,7 @@ public static class Utils
return (a + b - 1) / b; return (a + b - 1) / b;
} }
[Pure] public static (int Column, int Row) GetSBSCoordinate(string coordinate)
public static (int Column, int Row) GetSbsNotationCoordinate(string coordinate)
{ {
var column = coordinate[^1] - '0'; var column = coordinate[^1] - '0';
if(char.IsDigit(coordinate[^2])) if(char.IsDigit(coordinate[^2]))
@ -34,31 +33,26 @@ public static class Utils
return (column, row); return (column, row);
} }
/// <summary>
/// Performs a SIMD-accelerated calculation that generates a buffer of bounded, scaled indices.
/// </summary>
/// <param name="scaleFactor">The amount by which to scale the sequence values.</param>
/// <param name="length">The total number of scalar values to generate.</param>
/// <param name="max">Upper limit (exclusive) for clamping values.</param>
/// <param name="offset">The offset to apply before clamping.</param>
public static IBuffer<int> BoundsMatrix(float scaleFactor, int length, int max, int offset) public static IBuffer<int> BoundsMatrix(float scaleFactor, int length, int max, int offset)
{ {
var vectorSize = DivCeil(length + 1, Vector<float>.Count); var vectorSize = DivCeil(length, Vector<float>.Count);
using var buffer = MemoryAllocator.Allocate<Vector<float>>(vectorSize); using var buffer = MemoryAllocator.Allocate<Vector<float>>(vectorSize);
var span = buffer.Span; var span = buffer.Span;
var vectorMin = Vector<int>.Zero; var vectorMin = Vector<int>.Zero;
var vectorOffset = new Vector<int>(offset); var vectorOffset = new Vector<int>(offset - 1);
var vectorMax = new Vector<int>(max - 1); var vectorMax = new Vector<int>(max - 1);
var vectorScale = new Vector<float>(scaleFactor); var vectorScale = new Vector<float>(scaleFactor);
var vectorSequence = Vector.CreateSequence(0f, 1f); var vectorSequence = SequenceVector(0f, 1f);
var seq = -1f; var seq = 0f;
for (var i = 0; i < vectorSize; i++, seq += Vector<float>.Count) for (var i = 0; i < vectorSize; i++, seq += Vector<float>.Count)
{ {
var sequence = new Vector<float>(seq) + vectorSequence; var sequence = new Vector<float>(seq) + vectorSequence;
span[i] = Vector.FusedMultiplyAdd(sequence, vectorScale, vectorScale); span[i] = Vector.Multiply(sequence, vectorScale);
span[i] = Vector.Add(span[i], vectorScale);
span[i] = Vector.Ceiling(span[i]); span[i] = Vector.Ceiling(span[i]);
} }
@ -68,9 +62,23 @@ public static class Utils
{ {
resultSpan[i] = Vector.ConvertToInt32(span[i]); resultSpan[i] = Vector.ConvertToInt32(span[i]);
resultSpan[i] = Vector.Add(resultSpan[i], vectorOffset); resultSpan[i] = Vector.Add(resultSpan[i], vectorOffset);
resultSpan[i] = Vector.ClampNative(resultSpan[i], vectorMin, vectorMax); resultSpan[i] = Vector.Min(resultSpan[i], vectorMax);
resultSpan[i] = Vector.Max(resultSpan[i], vectorMin);
} }
return result; return result;
} }
private static Vector<float> SequenceVector(float start, float step)
{
var vector = Vector<float>.Zero;
ref var reference = ref Unsafe.As<Vector<float>, float>(ref vector);
for (var i = 0; i < Vector<float>.Count; i++)
{
ref var current = ref Unsafe.Add(ref reference, i);
current = start + step * i;
}
return vector;
}
} }

View file

@ -7,11 +7,10 @@ public class ArrayOwner<T> : IBuffer<T> where T : unmanaged
private readonly ArrayPool<T> _owner; private readonly ArrayPool<T> _owner;
private readonly T[] _buffer; private readonly T[] _buffer;
public ArrayOwner(ArrayPool<T> owner, int length) public ArrayOwner(ArrayPool<T> owner, int size)
{ {
_owner = owner; _owner = owner;
_buffer = owner.Rent(length); _buffer = owner.Rent(size);
Length = length;
} }
~ArrayOwner() => Dispose(); ~ArrayOwner() => Dispose();
@ -24,10 +23,7 @@ public class ArrayOwner<T> : IBuffer<T> where T : unmanaged
public ref T this[int index] => ref _buffer[index]; public ref T this[int index] => ref _buffer[index];
public Span<T> Span => _buffer.AsSpan(0, Length); public Span<T> Span => _buffer;
public Memory<T> Memory => _buffer.AsMemory(0, Length);
public T[] Array => _buffer; public T[] Array => _buffer;
public int Length { get; }
} }

View file

@ -5,8 +5,4 @@ public interface IBuffer<T> : IDisposable where T : unmanaged
ref T this[int index] { get; } ref T this[int index] { get; }
Span<T> Span { get; } Span<T> Span { get; }
Memory<T> Memory { get; }
int Length { get; }
} }

View file

@ -1,51 +0,0 @@
using System.Buffers;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace StitchATon2.Infra.Buffers;
internal sealed unsafe class ImmovableMemory<T> : MemoryManager<T>, IBuffer<T> where T : unmanaged
{
internal readonly T* Pointer;
private bool _disposed;
public ImmovableMemory(int length)
{
Pointer = (T*)NativeMemory.Alloc((nuint)length, (nuint)Unsafe.SizeOf<T>());
Length = length;
}
protected override void Dispose(bool disposing)
{
if (_disposed) return;
NativeMemory.Free(Pointer);
_disposed = true;
}
public override Span<T> GetSpan()
=> _disposed
? throw new ObjectDisposedException(nameof(ImmovableMemory<T>))
: new Span<T>(Pointer, Length);
public override MemoryHandle Pin(int elementIndex = 0)
=> _disposed
? throw new ObjectDisposedException(nameof(ImmovableMemory<T>))
: new MemoryHandle(Pointer + elementIndex);
public override void Unpin()
{
}
public ref T this[int index]
{
get
{
if (_disposed) throw new ObjectDisposedException(nameof(ImmovableMemory<T>));
return ref Unsafe.AsRef<T>(Pointer + index);
}
}
public Span<T> Span => GetSpan();
public int Length { get; }
}

View file

@ -1,36 +1,15 @@
using System.Buffers; using System.Buffers;
using System.Runtime.CompilerServices;
namespace StitchATon2.Infra.Buffers; namespace StitchATon2.Infra.Buffers;
public static class MemoryAllocator public static class MemoryAllocator
{ {
public static IBuffer<T> Allocate<T>(int count) where T : unmanaged public static IBuffer<T> Allocate<T>(int count) where T : unmanaged
=> new ImmovableMemory<T>(count); => new UnmanagedMemory<T>(count);
public static IMemoryOwner<T> AllocateManaged<T>(int count) public static IMemoryOwner<T> AllocateManaged<T>(int count)
=> MemoryPool<T>.Shared.Rent(count); => MemoryPool<T>.Shared.Rent(count);
public static ArrayOwner<T> AllocateArray<T>(int count) where T : unmanaged public static ArrayOwner<T> AllocateArray<T>(int count) where T : unmanaged
=> new(ArrayPool<T>.Shared, count); => new(ArrayPool<T>.Shared, count);
public static unsafe IBuffer<T> Clone<T>(this IBuffer<T> buffer) where T : unmanaged
{
if (buffer is not ImmovableMemory<T> unmanagedMemory)
throw new NotSupportedException();
var newBuffer = new ImmovableMemory<T>(buffer.Length);
var byteCount = (uint)(Unsafe.SizeOf<T>() * buffer.Length);
Unsafe.CopyBlock(newBuffer.Pointer, unmanagedMemory.Pointer, byteCount);
return newBuffer;
}
public static unsafe void Copy<T>(this IBuffer<T> source, IBuffer<T> destination, int count) where T : unmanaged
{
if (source is not ImmovableMemory<T> sourceBuffer || destination is not ImmovableMemory<T> destinationBuffer)
throw new NotSupportedException();
var byteCount = (uint)(Unsafe.SizeOf<T>() * count);
Unsafe.CopyBlock(destinationBuffer.Pointer, sourceBuffer.Pointer, byteCount);
}
} }

View file

@ -1,102 +0,0 @@
using System.Buffers;
namespace StitchATon2.Infra.Buffers;
public class PooledMemoryStream : Stream
{
private byte[] _buffer;
private int _length;
private int _position;
private readonly ArrayPool<byte> _pool;
private bool _disposed;
public PooledMemoryStream(int initialCapacity = 1024, ArrayPool<byte>? pool = null)
{
_pool = pool ?? ArrayPool<byte>.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<byte> 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<byte>();
_disposed = true;
}
base.Dispose(disposing);
}
}

View file

@ -3,38 +3,26 @@ using System.Runtime.InteropServices;
namespace StitchATon2.Infra.Buffers; namespace StitchATon2.Infra.Buffers;
/// <summary>
/// Provide non-thread safe anti GC contiguous memory.
/// </summary>
/// <typeparam name="T"></typeparam>
[Obsolete("Use immovable memory instead")]
internal sealed unsafe class UnmanagedMemory<T> : IBuffer<T> where T : unmanaged internal sealed unsafe class UnmanagedMemory<T> : IBuffer<T> where T : unmanaged
{ {
internal readonly T* Pointer; private readonly void* _pointer;
private bool _disposed; private readonly int _count;
public Memory<T> Memory => throw new NotImplementedException(); public ref T this[int index] => ref Unsafe.AsRef<T>((T*)_pointer + index); // *((T*)_pointer + index);
public int Length { get; }
public ref T this[int index] => ref Unsafe.AsRef<T>(Pointer + index); public Span<T> Span => new(_pointer, _count);
public Span<T> Span => new(Pointer, Length); public UnmanagedMemory(int count)
public UnmanagedMemory(int length)
{ {
Pointer = (T*)NativeMemory.Alloc((nuint)length, (nuint)Unsafe.SizeOf<T>()); _pointer = NativeMemory.Alloc((nuint)count, (nuint)Unsafe.SizeOf<T>());
Length = length; _count = count;
} }
~UnmanagedMemory() => Dispose(); ~UnmanagedMemory() => Dispose();
public void Dispose() public void Dispose()
{ {
if (!_disposed) NativeMemory.Free(_pointer);
{ GC.SuppressFinalize(this);
NativeMemory.Free(Pointer);
GC.SuppressFinalize(this);
_disposed = true;
}
} }
} }

View file

@ -15,17 +15,6 @@ public static class Crc32
return ~crc; 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() private static uint[] GenerateTable()
{ {
const uint poly = 0xEDB88320; const uint poly = 0xEDB88320;

View file

@ -1,144 +0,0 @@
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 const int PipeChunkThreshold = 16 * 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<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[16..], _width);
BinaryPrimitives.WriteInt32BigEndian(headerBytes[20..], _height);
var crc = Crc32.Compute(headerBytes.Slice(12, 17));
BinaryPrimitives.WriteUInt32BigEndian(headerBytes[29..], crc);
_outputPipe.Write(headerBytes);
}
public async Task WriteDataAsync(IBuffer<byte> buffer, bool disposeBuffer = true, CancellationToken cancellationToken = default)
{
try
{
_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;
}
}
finally
{
if(disposeBuffer)
buffer.Dispose();
}
}
private async Task FlushAsync(CancellationToken cancellationToken)
{
await _zlibStream.FlushAsync(cancellationToken);
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));
await _outputPipe.FlushAsync(cancellationToken);
_memoryStream.SetLength(8);
_memoryStream.Position = 8;
_shouldFlush = false;
}
public async Task WriteEndOfFileAsync(CancellationToken cancellationToken = default)
{
if(_shouldFlush)
await FlushAsync(cancellationToken);
Span<byte> 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);
}
}
}

View file

@ -1,6 +1,5 @@
using System.Buffers.Binary; using System.Buffers.Binary;
using System.IO.Compression; using System.IO.Compression;
using StitchATon2.Infra.Buffers;
namespace StitchATon2.Infra.Encoders; namespace StitchATon2.Infra.Encoders;
@ -31,7 +30,7 @@ public class PngStreamEncoder : IDisposable, IAsyncDisposable
~PngStreamEncoder() => Dispose(); ~PngStreamEncoder() => Dispose();
public async Task WriteHeader(CancellationToken cancellationToken = default) public async Task WriteHeader()
{ {
byte[] headerBytes = [ byte[] headerBytes = [
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG Signature 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG Signature
@ -55,42 +54,34 @@ public class PngStreamEncoder : IDisposable, IAsyncDisposable
BinaryPrimitives.WriteUInt32BigEndian(headerBytes.AsSpan(29), crc); BinaryPrimitives.WriteUInt32BigEndian(headerBytes.AsSpan(29), crc);
await _stream.WriteAsync(headerBytes, cancellationToken); await _stream.WriteAsync(headerBytes);
} }
public async Task WriteDataAsync(IBuffer<byte> buffer, bool disposeBuffer = true, CancellationToken cancellationToken = default) public async Task WriteData(Memory<byte> data)
{ {
try _zlibStream.Write([0]);
var dataSlice = data;
while (dataSlice.Length > FlushThreshold)
{ {
_zlibStream.Write([0]); await _zlibStream.WriteAsync(dataSlice[..FlushThreshold]);
await _zlibStream.FlushAsync();
var dataSlice = buffer.Memory; dataSlice = dataSlice[FlushThreshold..];
while (dataSlice.Length > FlushThreshold) if(_memoryStream.Length >= BufferSize)
{ await Flush();
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;
}
} }
finally
if (dataSlice.Length > 0)
{ {
if(disposeBuffer) await _zlibStream.WriteAsync(dataSlice);
buffer.Dispose(); await _zlibStream.FlushAsync();
_shouldFlush = true;
} }
} }
private async Task FlushAsync(CancellationToken cancellationToken) private async Task Flush()
{ {
await _zlibStream.FlushAsync(cancellationToken); await _zlibStream.FlushAsync();
var dataSize = (int)(_memoryStream.Length - 8); var dataSize = (int)(_memoryStream.Length - 8);
_memoryStream.Write("\0\0\0\0"u8); _memoryStream.Write("\0\0\0\0"u8);
@ -105,16 +96,16 @@ public class PngStreamEncoder : IDisposable, IAsyncDisposable
var crc = Crc32.Compute(buffer.AsSpan(4, dataSize + 4)); var crc = Crc32.Compute(buffer.AsSpan(4, dataSize + 4));
BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(dataSize + 8), crc); BinaryPrimitives.WriteUInt32BigEndian(buffer.AsSpan(dataSize + 8), crc);
await _stream.WriteAsync(buffer.AsMemory(0, dataSize + 12), cancellationToken); await _stream.WriteAsync(buffer.AsMemory(0, dataSize + 12));
_memoryStream.SetLength(8); _memoryStream.SetLength(8);
_memoryStream.Position = 8; _memoryStream.Position = 8;
_shouldFlush = false; _shouldFlush = false;
} }
public async ValueTask WriteEndOfFileAsync(CancellationToken cancellationToken = default) public async ValueTask WriteEndOfFile()
{ {
if(_shouldFlush) if(_shouldFlush)
await FlushAsync(cancellationToken); await Flush();
var endChunk = new byte[] { var endChunk = new byte[] {
0x00, 0x00, 0x00, 0x00, // Length 0x00, 0x00, 0x00, 0x00, // Length
@ -122,7 +113,7 @@ public class PngStreamEncoder : IDisposable, IAsyncDisposable
0xAE, 0x42, 0x60, 0x82, // Crc 0xAE, 0x42, 0x60, 0x82, // Crc
}; };
await _stream.WriteAsync(endChunk, cancellationToken); await _stream.WriteAsync(endChunk);
await DisposeAsync(); await DisposeAsync();
} }

View file

@ -1,166 +0,0 @@
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<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[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<byte> 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<byte> 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<byte> 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);
}
}
}

View file

@ -1,3 +1,4 @@
using System.Buffers;
using System.IO.MemoryMappedFiles; using System.IO.MemoryMappedFiles;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@ -5,7 +6,6 @@ using SixLabors.ImageSharp.Formats;
using SixLabors.ImageSharp.Formats.Png; using SixLabors.ImageSharp.Formats.Png;
using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.PixelFormats;
using StitchATon2.Infra.Buffers; using StitchATon2.Infra.Buffers;
using StitchATon2.Infra.Synchronization;
namespace StitchATon2.Infra; namespace StitchATon2.Infra;
@ -18,9 +18,9 @@ public class ImageIntegral : IDisposable
private readonly int _width; private readonly int _width;
private readonly int _height; private readonly int _height;
private ManualResetEventSlim[]? _rowLocks; private IMemoryOwner<ManualResetEventSlim>? _rowLocks;
private MemoryMappedFile? _memoryMappedFile; private MemoryMappedFile? _memoryMappedFile;
private readonly Lock _lock = new(); private readonly object _lock = new();
private readonly ManualResetEventSlim _queueLock = new(true); private readonly ManualResetEventSlim _queueLock = new(true);
private readonly ManualResetEventSlim _initializationLock = new(false); private readonly ManualResetEventSlim _initializationLock = new(false);
@ -58,14 +58,14 @@ public class ImageIntegral : IDisposable
{ {
if (_memoryMappedFile is null) if (_memoryMappedFile is null)
{ {
Task.Run(() => Initialize(cancellationToken), cancellationToken); Task.Factory.StartNew(() => Initialize(cancellationToken), cancellationToken);
_initializationLock.Wait(cancellationToken); _initializationLock.Wait(cancellationToken);
_initializationLock.Dispose(); _initializationLock.Dispose();
} }
} }
} }
_rowLocks?[row].Wait(cancellationToken); _rowLocks?.Memory.Span[row].Wait(cancellationToken);
} }
private void Initialize(CancellationToken cancellationToken) private void Initialize(CancellationToken cancellationToken)
@ -86,8 +86,8 @@ public class ImageIntegral : IDisposable
} }
var taskQueue = backedFileStream == null var taskQueue = backedFileStream == null
? TaskHelper.SynchronizedTaskFactory.StartNew(() => { }, cancellationToken) ? Task.CompletedTask
: AllocateBackedFile(backedFileStream, header, cancellationToken); : AllocateBackedFile(backedFileStream, header);
taskQueue = taskQueue.ContinueWith( taskQueue = taskQueue.ContinueWith(
_ => _ =>
@ -100,11 +100,12 @@ public class ImageIntegral : IDisposable
// initialize resource gating, all rows is expected to be locked // initialize resource gating, all rows is expected to be locked
// if the backed file require to allocate, it should be safe to do this // if the backed file require to allocate, it should be safe to do this
// asynchronously // asynchronously
var rowLocks = new ManualResetEventSlim[_height]; var rowLocks = MemoryAllocator.AllocateManaged<ManualResetEventSlim>(_height);
var rowLocksSpan = rowLocks.Memory.Span;
for (int i = 0; i < _height; i++) for (int i = 0; i < _height; i++)
{ {
var isOpen = i < header.ProcessedRows; var isOpen = i < header.ProcessedRows;
rowLocks[i] = new ManualResetEventSlim(isOpen); rowLocksSpan[i] = new ManualResetEventSlim(isOpen);
} }
_rowLocks = rowLocks; _rowLocks = rowLocks;
@ -128,7 +129,7 @@ public class ImageIntegral : IDisposable
var imageBuffer = image.Frames.RootFrame.PixelBuffer; var imageBuffer = image.Frames.RootFrame.PixelBuffer;
var accumulator = Int32Pixel.Zero; var accumulator = Int32Pixel.Zero;
var buffer = MemoryAllocator.Allocate<Int32Pixel>(_width); var buffer = MemoryAllocator.AllocateArray<Int32Pixel>(_width);
var processedRows = _processedRows; var processedRows = _processedRows;
Interlocked.Exchange(ref _queueCounter, 0); Interlocked.Exchange(ref _queueCounter, 0);
@ -142,55 +143,50 @@ public class ImageIntegral : IDisposable
buffer[x] = accumulator; buffer[x] = accumulator;
} }
taskQueue = QueueWriterTask(taskQueue, 0, buffer.Clone(), cancellationToken); taskQueue = QueueWriterTask(taskQueue, 0, buffer.Clone(_width), cancellationToken);
processedRows++; processedRows++;
} }
else else
{ {
ReadRow(processedRows - 1, buffer.Span); ReadRow(processedRows - 1, buffer);
} }
if(cancellationToken.IsCancellationRequested) if(cancellationToken.IsCancellationRequested)
return; return;
var prevBuffer = buffer; var prevBuffer = buffer;
buffer = MemoryAllocator.Allocate<Int32Pixel>(_width); buffer = MemoryAllocator.AllocateArray<Int32Pixel>(_width);
try
for (int y = processedRows; y < image.Height; y++)
{ {
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++)
{ {
var sourceRow = imageBuffer.DangerousGetRowSpan(y); accumulator.Accumulate(sourceRow[x]);
accumulator = (Int32Pixel)sourceRow[0]; buffer[x] = accumulator + prevBuffer[x];
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<Int32Pixel>(_width);
} }
}
finally if (_queueCounter >= MaxProcessingQueue)
{ {
buffer.Dispose(); _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<Int32Pixel>(_width);
} }
buffer.Dispose();
if(cancellationToken.IsCancellationRequested) if(cancellationToken.IsCancellationRequested)
return; return;
@ -209,7 +205,7 @@ public class ImageIntegral : IDisposable
private Task QueueWriterTask( private Task QueueWriterTask(
Task taskQueue, Task taskQueue,
int row, int row,
IBuffer<Int32Pixel> writeBuffer, ArrayOwner<Int32Pixel> writeBuffer,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
Interlocked.Increment(ref _queueCounter); Interlocked.Increment(ref _queueCounter);
@ -217,10 +213,10 @@ public class ImageIntegral : IDisposable
return taskQueue.ContinueWith(_ => return taskQueue.ContinueWith(_ =>
{ {
using (var view = AcquireView(row, MemoryMappedFileAccess.Write)) using (var view = AcquireView(row, MemoryMappedFileAccess.Write))
view.DangerousWriteSpan(0, writeBuffer.Span, 0, _width); view.WriteArray(0, writeBuffer.Array, 0, _width);
writeBuffer.Dispose(); writeBuffer.Dispose();
_rowLocks![row].Set(); _rowLocks!.Memory.Span[row].Set();
Interlocked.Increment(ref _processedRows); Interlocked.Increment(ref _processedRows);
using (var view = AcquireHeaderView(MemoryMappedFileAccess.Write)) using (var view = AcquireHeaderView(MemoryMappedFileAccess.Write))
@ -253,6 +249,12 @@ public class ImageIntegral : IDisposable
view.DangerousReadSpan(0, buffer, 0, _width); view.DangerousReadSpan(0, buffer, 0, _width);
} }
private void ReadRow(int row, IBuffer<Int32Pixel> buffer)
{
using var view = AcquireView(row, MemoryMappedFileAccess.Read);
view.DangerousReadSpan(0, buffer.Span, 0, _width);
}
private FileStream? InitializeBackedFile(string path, out Header header) private FileStream? InitializeBackedFile(string path, out Header header)
{ {
var expectedHeader = Header.CreateInitial(_width, _height); var expectedHeader = Header.CreateInitial(_width, _height);
@ -309,35 +311,33 @@ public class ImageIntegral : IDisposable
return fs; return fs;
} }
private static Task AllocateBackedFile(FileStream fileStream, Header header, CancellationToken cancellationToken) private static async Task AllocateBackedFile(FileStream fileStream, Header header)
{ {
return TaskHelper.SynchronizedTaskFactory.StartNew(() => // The input filestream is expected to be empty with
{ // initial cursor at the beginning of the file and the content
// The input filestream is expected to be empty with // is pre-allocated for at least Header.Length bytes
// initial cursor at the beginning of the file and the content // No other process should be accessed the file while being
// is pre-allocated for at least Header.Length bytes // allocated.
// No other process should be accessed the file while being // Allocated bytes is not necessary to be zeroed.
// allocated.
// Allocated bytes is not necessary to be zeroed.
// const int writeBufferSize = 4 * 1024; // const int writeBufferSize = 4 * 1024;
// using var writeBuffer = MemoryPool<byte>.Shared.Rent(writeBufferSize); // using var writeBuffer = MemoryPool<byte>.Shared.Rent(writeBufferSize);
// //
// var written = 0; // var written = 0;
// while (written + writeBufferSize < header.Length) // while (written + writeBufferSize < header.Length)
// { // {
// await fileStream.WriteAsync(writeBuffer.Memory, cancellationToken); // await fileStream.WriteAsync(writeBuffer.Memory, cancellationToken);
// written += writeBufferSize; // written += writeBufferSize;
// } // }
// //
// if (written < header.Length) // if (written < header.Length)
// { // {
// await fileStream.WriteAsync(writeBuffer.Memory[..(header.Length - written)], cancellationToken); // await fileStream.WriteAsync(writeBuffer.Memory[..(header.Length - written)], cancellationToken);
// } // }
fileStream.SetLength(header.Length + Header.Size); fileStream.SetLength(header.Length + Header.Size);
fileStream.Dispose();
}, cancellationToken); await fileStream.DisposeAsync();
} }
[StructLayout(LayoutKind.Sequential)] [StructLayout(LayoutKind.Sequential)]
@ -368,8 +368,11 @@ public class ImageIntegral : IDisposable
if (_rowLocks is { } locks) if (_rowLocks is { } locks)
{ {
_rowLocks = null; _rowLocks = null;
var lockSpan = locks.Memory.Span;
for(int i = 0; i < _height; i++) for(int i = 0; i < _height; i++)
locks[i].Dispose(); lockSpan[i].Dispose();
locks.Dispose();
} }
} }

View file

@ -68,12 +68,6 @@ public record struct Int32Pixel
return new Int32Pixel(a.R / b, a.G / b, a.B / b); 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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static explicit operator Int32Pixel(Rgb24 pixel) public static explicit operator Int32Pixel(Rgb24 pixel)
{ {

View file

@ -1,14 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks> <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.11" /> <PackageReference Include="SixLabors.ImageSharp" Version="3.1.10" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -2,7 +2,11 @@ namespace StitchATon2.Infra.Synchronization;
public static class TaskHelper public static class TaskHelper
{ {
public static readonly TaskFactory SynchronizedTaskFactory = new( public static TaskFactory CreateTaskFactory()
TaskCreationOptions.LongRunning, {
TaskContinuationOptions.ExecuteSynchronously); return new TaskFactory(
TaskCreationOptions.AttachedToParent,
TaskContinuationOptions.ExecuteSynchronously
);
}
} }

View file

@ -1,16 +1,25 @@
using System.IO.MemoryMappedFiles; using System.IO.MemoryMappedFiles;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using StitchATon2.Infra.Buffers;
namespace StitchATon2.Infra; namespace StitchATon2.Infra;
internal static class Utils 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<T>() where T : unmanaged private static unsafe uint AlignedSizeOf<T>() where T : unmanaged
{ {
uint size = (uint)sizeof(T); 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<T>(this MemoryMappedViewAccessor view, long position, Span<T> span, int offset, int count) public static void DangerousReadSpan<T>(this UnmanagedMemoryAccessor view, long position, Span<T> span, int offset, int count)
where T : unmanaged where T : unmanaged
{ {
uint sizeOfT = AlignedSizeOf<T>(); uint sizeOfT = AlignedSizeOf<T>();
@ -29,14 +38,19 @@ internal static class Utils
} }
} }
var byteOffset = (ulong)(view.PointerOffset + position); var byteOffset = (ulong)(view.GetOffset() + position);
view.SafeMemoryMappedViewHandle.ReadSpan(byteOffset, span.Slice(offset, n)); view.GetSafeBuffer().ReadSpan(byteOffset, span.Slice(offset, n));
} }
internal static void DangerousWriteSpan<T>(this MemoryMappedViewAccessor view, long position, Span<T> span, int offset, int count) public static ArrayOwner<T> Clone<T>(this ArrayOwner<T> arrayOwner, int length) where T : unmanaged
where T : unmanaged
{ {
var byteOffset = (ulong)(view.PointerOffset + position); var newArrayOwner = MemoryAllocator.AllocateArray<T>(length);
view.SafeMemoryMappedViewHandle.WriteSpan<T>(byteOffset, span.Slice(offset, count)); Array.Copy(arrayOwner.Array, 0, newArrayOwner.Array, 0, length);
return newArrayOwner;
}
public static void CopyTo<T>(this ArrayOwner<T> arrayOwner, ArrayOwner<T> target, int length) where T : unmanaged
{
Array.Copy(arrayOwner.Array, 0, target.Array, 0, length);
} }
} }

View file

@ -1,42 +0,0 @@
# Stitch-a-ton Contest Submission
Repository moved to: https://github.com/denniskematian/GigATon
## Installation
This project is build using .NET 9, with SixLabors.ImageSharp as the only external library.
Please refer to [install dotnet SDK on Debian](https://learn.microsoft.com/en-us/dotnet/core/install/linux-debian?tabs=dotnet9)
for details.
### 1. Build
Install dependencies:
```
wget https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
sudo apt update
sudo apt install -y dotnet-sdk-9.0
```
Build the project in `App` folder using command:
```
dotnet publish -c Release -r linux-arm64 --self-contained true -o ./publish
```
### 2. Running the app
After publishing, enter the folder `/publish` then run
```
./StitchATon2.App
```
## Ideas
Since we only serve a static assets and the output is downscale only, I think it's good to pre-process each image to [image integral](https://en.wikipedia.org/wiki/Summed-area_table) first and then save it to another directory (eg. temp).
Later, we can apply Box Filter averaging on the fly to output Stream (HttpResponse).
## Approach
This project is heavily depends on Memory Mapped File to reduce processing overhead and memory pressure to allow HTTP serve more request.
The MMF is used to store the Image Integral of provided asset.

View file

@ -1,77 +0,0 @@
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));
}
}

View file

@ -6,10 +6,6 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StitchATon2.Domain", "Domai
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StitchATon2.Infra", "Infra\StitchATon2.Infra.csproj", "{E602F3FC-6139-4B30-AC5A-75815E6340A4}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StitchATon2.Infra", "Infra\StitchATon2.Infra.csproj", "{E602F3FC-6139-4B30-AC5A-75815E6340A4}"
EndProject 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 Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -28,13 +24,5 @@ Global
{E602F3FC-6139-4B30-AC5A-75815E6340A4}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
{E602F3FC-6139-4B30-AC5A-75815E6340A4}.Release|Any CPU.Build.0 = 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
{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 EndGlobalSection
EndGlobal EndGlobal

View file

@ -1,6 +1,6 @@
{ {
"sdk": { "sdk": {
"version": "9.0.0", "version": "8.0.0",
"rollForward": "latestMinor", "rollForward": "latestMinor",
"allowPrerelease": false "allowPrerelease": false
} }