solve 'edge' case and pass cancellation token
This commit is contained in:
parent
0472bfe58e
commit
d3dfdd6a74
15 changed files with 208 additions and 551 deletions
|
|
@ -30,7 +30,7 @@ public static class ImageController
|
||||||
|
|
||||||
await tileManager
|
await tileManager
|
||||||
.CreateSection(dto)
|
.CreateSection(dto)
|
||||||
.DangerousWriteToPipe(response.BodyWriter, dto.OutputScale, cancellationToken);
|
.WriteToPipe(response.BodyWriter, dto.OutputScale, cancellationToken);
|
||||||
|
|
||||||
await response.CompleteAsync();
|
await response.CompleteAsync();
|
||||||
}
|
}
|
||||||
|
|
@ -56,7 +56,7 @@ public static class ImageController
|
||||||
var scale = float.Clamp(480f / int.Max(section.Width, section.Height), 0.01f, 1f);
|
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.DangerousWriteToPipe(response.BodyWriter, scale, cancellationToken);
|
await section.WriteToPipe(response.BodyWriter, scale, cancellationToken);
|
||||||
await response.CompleteAsync();
|
await response.CompleteAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
14
App/Utils.cs
14
App/Utils.cs
|
|
@ -15,19 +15,23 @@ public static class Utils
|
||||||
dto.CropSize![0],
|
dto.CropSize![0],
|
||||||
dto.CropSize![1]);
|
dto.CropSize![1]);
|
||||||
|
|
||||||
public static async Task WriteToStream(this GridSection section, Stream stream, float? scale)
|
public static async Task WriteToStream(
|
||||||
|
this GridSection section,
|
||||||
|
Stream stream,
|
||||||
|
float? scale,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var imageCreator = new ImageCreator(section);
|
using var imageCreator = new DangerousImageCreator(section);
|
||||||
await imageCreator.WriteToStream(stream, scale!.Value);
|
await imageCreator.WriteToStream(stream, scale!.Value, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task DangerousWriteToPipe(
|
public static async Task WriteToPipe(
|
||||||
this GridSection section,
|
this GridSection section,
|
||||||
PipeWriter pipeWriter,
|
PipeWriter pipeWriter,
|
||||||
float? scale,
|
float? scale,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var imageCreator = new DangerousImageCreator(section);
|
using var imageCreator = new DangerousImageCreator(section);
|
||||||
await imageCreator.WriteToPipe(pipeWriter, scale!.Value, cancellationToken);
|
await imageCreator.WriteToPipe(pipeWriter, scale!.Value, cancellationToken);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -20,25 +20,20 @@ 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,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,34 +39,22 @@ public sealed class DangerousImageCreator : IDisposable
|
||||||
|
|
||||||
~DangerousImageCreator() => Dispose();
|
~DangerousImageCreator() => Dispose();
|
||||||
|
|
||||||
public async Task WriteToPipe2(PipeWriter outputPipe, float scale, CancellationToken cancellationToken = default)
|
private Task Create(float scale, Action<IBuffer<byte>> writeRowCallback, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var scaleFactor = MathF.ReciprocalEstimate(scale);
|
var scaleFactor = MathF.ReciprocalEstimate(scale);
|
||||||
var targetWidth = (int)(Width / scaleFactor);
|
var targetWidth = (int)(Width / scaleFactor);
|
||||||
var targetHeight = (int)(Height / scaleFactor);
|
var targetHeight = (int)(Height / scaleFactor);
|
||||||
if (targetHeight == 0 || targetWidth == 0)
|
if (targetHeight == 0 || targetWidth == 0)
|
||||||
return;
|
return Task.CompletedTask;
|
||||||
|
|
||||||
var encoder = new PngPipeEncoder(outputPipe, targetWidth, targetHeight);
|
using var xLookup = Utils.BoundsMatrix(scaleFactor, targetWidth, FullWidth, OffsetX);
|
||||||
encoder.WriteHeader();
|
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>();
|
var outputBufferSize = targetWidth * Unsafe.SizeOf<Rgb24>();
|
||||||
|
|
||||||
var xLookup = Utils.DoubleBoundsMatrix(scaleFactor, targetWidth, FullWidth, OffsetX);
|
|
||||||
var yLookup = Utils.DoubleBoundsMatrix(scaleFactor, targetHeight, FullHeight, OffsetY);
|
|
||||||
|
|
||||||
using var xLookup0 = xLookup.Item1;
|
|
||||||
using var xLookup1 = xLookup.Item2;
|
|
||||||
using var yLookup0 = yLookup.Item1;
|
|
||||||
using var yLookup1 = yLookup.Item2;
|
|
||||||
|
|
||||||
using var yStartMap0 = MemoryAllocator.Allocate<Int32Pixel>(targetWidth);
|
|
||||||
using var yStartMap1 = MemoryAllocator.Allocate<Int32Pixel>(targetWidth);
|
|
||||||
using var yEndMap0 = MemoryAllocator.Allocate<Int32Pixel>(targetWidth);
|
|
||||||
using var yEndMap1 = MemoryAllocator.Allocate<Int32Pixel>(targetWidth);
|
|
||||||
|
|
||||||
// var yStart = OffsetY;
|
|
||||||
|
|
||||||
// Use pixel referencing to eliminate type casting
|
// Use pixel referencing to eliminate type casting
|
||||||
var pxInt32 = Int32Pixel.Zero;
|
var pxInt32 = Int32Pixel.Zero;
|
||||||
ref var px = ref pxInt32;
|
ref var px = ref pxInt32;
|
||||||
|
|
@ -74,41 +62,37 @@ public sealed class DangerousImageCreator : IDisposable
|
||||||
ref var gChannel = ref Unsafe.Add(ref rChannel, 4);
|
ref var gChannel = ref Unsafe.Add(ref rChannel, 4);
|
||||||
ref var bChannel = ref Unsafe.Add(ref rChannel, 8);
|
ref var bChannel = ref Unsafe.Add(ref rChannel, 8);
|
||||||
|
|
||||||
var outputTaskQueue = TaskHelper.SynchronizedTaskFactory.StartNew(() => { }, cancellationToken);
|
var taskQueue = TaskHelper.SynchronizedTaskFactory.StartNew(() => { }, cancellationToken);
|
||||||
for (var y = 0; y < targetHeight; y++)
|
|
||||||
|
for (var y = 1; y <= targetHeight; y++)
|
||||||
{
|
{
|
||||||
var yStart = yLookup0[y];
|
var yStart = yLookup[y - 1];
|
||||||
var yEnd = yLookup1[y];
|
var yEnd = yLookup[y];
|
||||||
|
|
||||||
var (localRow0, localOffsetY0) = int.DivRem(yStart, TileHeight);
|
var (localRow0, localOffsetY0) = int.DivRem(yStart, TileHeight);
|
||||||
MapRow(localRow0, localOffsetY0, xLookup0, targetWidth, yStartMap0);
|
MapRow(localRow0, localOffsetY0, xLookup, targetWidth + 1, yStartMap);
|
||||||
MapRow(localRow0, localOffsetY0, xLookup1, targetWidth, yStartMap1);
|
|
||||||
|
|
||||||
var (localRow1, localOffsetY1) = int.DivRem(yEnd, TileHeight);
|
var (localRow1, localOffsetY1) = int.DivRem(yEnd, TileHeight);
|
||||||
MapRow(localRow1, localOffsetY1, xLookup0, targetWidth, yEndMap0);
|
MapRow(localRow1, localOffsetY1, xLookup, targetWidth + 1, yEndMap);
|
||||||
MapRow(localRow1, localOffsetY1, xLookup1, targetWidth, yEndMap1);
|
|
||||||
|
|
||||||
|
// Cross row
|
||||||
if (localRow0 != localRow1)
|
if (localRow0 != localRow1)
|
||||||
{
|
MapRow(localRow0, BottomPixelIndex, xLookup, targetWidth + 1, yEndMap, true);
|
||||||
MapRow(localRow0, BottomPixelIndex, xLookup0, targetWidth, yEndMap0, true);
|
|
||||||
MapRow(localRow0, BottomPixelIndex, xLookup1, targetWidth, yEndMap1, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// int xStart = OffsetX, x0 = 0;
|
|
||||||
|
|
||||||
var outputBuffer = MemoryAllocator.Allocate<byte>(outputBufferSize);
|
var outputBuffer = MemoryAllocator.Allocate<byte>(outputBufferSize);
|
||||||
ref var outputChannel = ref outputBuffer.Span[0];
|
ref var outputChannel = ref outputBuffer.Span[0];
|
||||||
var boxHeight = Math.Max(1, yEnd - yStart);
|
var boxHeight = yEnd - yStart;
|
||||||
for (int x = 0; x < targetWidth; x++)
|
|
||||||
{
|
|
||||||
var xStart = xLookup0[x];
|
|
||||||
var xEnd = xLookup1[x];
|
|
||||||
|
|
||||||
px = yEndMap1[x];
|
for (int x1 = 1, x0 = 0; x1 <= targetWidth; x0 = x1++)
|
||||||
px += yStartMap0[x];
|
{
|
||||||
px -= yEndMap0[x];
|
var xStart = xLookup[x1 - 1];
|
||||||
px -= yStartMap1[x];
|
var xEnd = xLookup[x1];
|
||||||
px /= Math.Max(1, xEnd - xStart) * boxHeight;
|
|
||||||
|
px = yEndMap[x1];
|
||||||
|
px += yStartMap[x0];
|
||||||
|
px -= yEndMap[x0];
|
||||||
|
px -= yStartMap[x1];
|
||||||
|
px /= Math.Max(1, (xEnd - xStart) * boxHeight);
|
||||||
|
|
||||||
outputChannel = rChannel;
|
outputChannel = rChannel;
|
||||||
outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1);
|
outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1);
|
||||||
|
|
@ -118,22 +102,24 @@ public sealed class DangerousImageCreator : IDisposable
|
||||||
|
|
||||||
outputChannel = bChannel;
|
outputChannel = bChannel;
|
||||||
outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1);
|
outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1);
|
||||||
|
|
||||||
// xStart = xEnd;
|
|
||||||
// x0 = x;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
outputTaskQueue = outputTaskQueue
|
if (cancellationToken.IsCancellationRequested)
|
||||||
.ContinueWith(async _ =>
|
{
|
||||||
{
|
outputBuffer.Dispose();
|
||||||
await encoder.WriteDataAsync(outputBuffer, cancellationToken: cancellationToken);
|
return Task.FromCanceled(cancellationToken);
|
||||||
}, cancellationToken);
|
}
|
||||||
|
|
||||||
yStart = yEnd;
|
cancellationToken.Register(outputBuffer.Dispose);
|
||||||
|
taskQueue = taskQueue
|
||||||
|
.ContinueWith(
|
||||||
|
_ => writeRowCallback.Invoke(outputBuffer),
|
||||||
|
cancellationToken,
|
||||||
|
TaskContinuationOptions.OnlyOnRanToCompletion,
|
||||||
|
TaskScheduler.Current);
|
||||||
}
|
}
|
||||||
|
|
||||||
await outputTaskQueue;
|
return taskQueue;
|
||||||
await encoder.WriteEndOfFileAsync(cancellationToken);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task WriteToPipe(PipeWriter outputPipe, float scale, CancellationToken cancellationToken = default)
|
public async Task WriteToPipe(PipeWriter outputPipe, float scale, CancellationToken cancellationToken = default)
|
||||||
|
|
@ -147,181 +133,32 @@ public sealed class DangerousImageCreator : IDisposable
|
||||||
var encoder = new PngPipeEncoder(outputPipe, targetWidth, targetHeight);
|
var encoder = new PngPipeEncoder(outputPipe, targetWidth, targetHeight);
|
||||||
encoder.WriteHeader();
|
encoder.WriteHeader();
|
||||||
|
|
||||||
Task outputTaskQueue;
|
await Create(scale,
|
||||||
|
output => encoder
|
||||||
|
.WriteDataAsync(output, cancellationToken: cancellationToken)
|
||||||
|
.Wait(cancellationToken),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
var outputBufferSize = targetWidth * Unsafe.SizeOf<Rgb24>();
|
await encoder.WriteEndOfFileAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
using var xLookup = Utils.BoundsMatrix(scaleFactor, targetWidth, FullWidth, OffsetX);
|
public async Task WriteToStream(Stream outputStream, float scale, CancellationToken cancellationToken = default)
|
||||||
using var yLookup = Utils.BoundsMatrix(scaleFactor, targetHeight, FullHeight, OffsetY);
|
{
|
||||||
|
var scaleFactor = MathF.ReciprocalEstimate(scale);
|
||||||
|
var targetWidth = (int)(Width / scaleFactor);
|
||||||
|
var targetHeight = (int)(Height / scaleFactor);
|
||||||
|
if (targetHeight == 0 || targetWidth == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
using var yStartMap = MemoryAllocator.Allocate<Int32Pixel>(targetWidth + 1);
|
var encoder = new PngStreamEncoder(outputStream, targetWidth, targetHeight);
|
||||||
using var yEndMap = MemoryAllocator.Allocate<Int32Pixel>(targetWidth + 1);
|
await encoder.WriteHeader(cancellationToken);
|
||||||
// OffsetX-(int)float.Ceiling(scaleFactor)
|
|
||||||
int yStart = OffsetY,
|
|
||||||
yEnd = yLookup[0],
|
|
||||||
xStart = OffsetX,
|
|
||||||
x0 = 0;
|
|
||||||
|
|
||||||
|
await Create(scale,
|
||||||
|
output => encoder
|
||||||
|
.WriteDataAsync(output, cancellationToken: cancellationToken)
|
||||||
|
.Wait(cancellationToken),
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
|
||||||
// Use pixel referencing to eliminate type casting
|
|
||||||
var pxInt32 = Int32Pixel.Zero;
|
|
||||||
ref var px = ref pxInt32;
|
|
||||||
ref var rChannel = ref Unsafe.As<Int32Pixel, byte>(ref px);
|
|
||||||
ref var gChannel = ref Unsafe.Add(ref rChannel, 4);
|
|
||||||
ref var bChannel = ref Unsafe.Add(ref rChannel, 8);
|
|
||||||
|
|
||||||
// First row
|
|
||||||
var (localRow0, localOffsetY0) = int.DivRem(yStart, TileHeight);
|
|
||||||
var (localRow1, localOffsetY1) = int.DivRem(yEnd, TileHeight);
|
|
||||||
{
|
|
||||||
switch (localOffsetY0)
|
|
||||||
{
|
|
||||||
// Cross row tile, no need to handle if it's first row tile for now
|
|
||||||
// the provided asset is bordered black anyway
|
|
||||||
case 0 when TileOrigin.Row > 1:
|
|
||||||
localOffsetY0 = BottomPixelIndex;
|
|
||||||
localRow0--;
|
|
||||||
break;
|
|
||||||
case > 0:
|
|
||||||
localOffsetY0--;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
MapRow(localRow0, localOffsetY0, xLookup, targetWidth, yStartMap);
|
|
||||||
MapRow(localRow1, localOffsetY1, xLookup, targetWidth, yEndMap);
|
|
||||||
|
|
||||||
// Cross row
|
|
||||||
if (localRow0 != localRow1)
|
|
||||||
MapRow(localRow0, BottomPixelIndex, xLookup, targetWidth, yEndMap, true);
|
|
||||||
|
|
||||||
var outputBuffer = MemoryAllocator.Allocate<byte>(outputBufferSize);
|
|
||||||
ref var outputChannel = ref outputBuffer.Span[0];
|
|
||||||
var boxHeight = yEnd - yStart;
|
|
||||||
|
|
||||||
// Render first pixel row
|
|
||||||
var xEnd = xLookup[0];
|
|
||||||
px = yEndMap[0];
|
|
||||||
px += yStartMap[^1];
|
|
||||||
px -= yEndMap[^1];
|
|
||||||
px -= yStartMap[0];
|
|
||||||
px /= Math.Max(1, (xEnd - xStart) * boxHeight);
|
|
||||||
|
|
||||||
outputChannel = rChannel;
|
|
||||||
outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1);
|
|
||||||
|
|
||||||
outputChannel = gChannel;
|
|
||||||
outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1);
|
|
||||||
|
|
||||||
outputChannel = bChannel;
|
|
||||||
outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1);
|
|
||||||
|
|
||||||
xStart = xEnd;
|
|
||||||
|
|
||||||
// Render entire pixel row
|
|
||||||
for (int x1 = 1; x1 < targetWidth; x1++)
|
|
||||||
{
|
|
||||||
xEnd = xLookup[x1];
|
|
||||||
|
|
||||||
px = yEndMap[x1];
|
|
||||||
px += yStartMap[x0];
|
|
||||||
px -= yEndMap[x0];
|
|
||||||
px -= yStartMap[x1];
|
|
||||||
px /= Math.Max(1, (xEnd - xStart) * boxHeight);
|
|
||||||
|
|
||||||
outputChannel = rChannel;
|
|
||||||
outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1);
|
|
||||||
|
|
||||||
outputChannel = gChannel;
|
|
||||||
outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1);
|
|
||||||
|
|
||||||
outputChannel = bChannel;
|
|
||||||
outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1);
|
|
||||||
|
|
||||||
xStart = xEnd;
|
|
||||||
x0 = x1;
|
|
||||||
}
|
|
||||||
|
|
||||||
outputTaskQueue = TaskHelper.SynchronizedTaskFactory.StartNew(async _ =>
|
|
||||||
{
|
|
||||||
await encoder.WriteDataAsync(outputBuffer, cancellationToken: cancellationToken);
|
|
||||||
}, null, cancellationToken);
|
|
||||||
|
|
||||||
yStart = yEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var y = 1; y < targetHeight; y++)
|
|
||||||
{
|
|
||||||
yEnd = yLookup[y];
|
|
||||||
|
|
||||||
(localRow0, localOffsetY0) = int.DivRem(yStart, TileHeight);
|
|
||||||
MapRow(localRow0, localOffsetY0, xLookup, targetWidth, yStartMap);
|
|
||||||
|
|
||||||
(localRow1, localOffsetY1) = int.DivRem(yEnd, TileHeight);
|
|
||||||
MapRow(localRow1, localOffsetY1, xLookup, targetWidth, yEndMap);
|
|
||||||
|
|
||||||
// Cross row
|
|
||||||
if (localRow0 != localRow1)
|
|
||||||
MapRow(localRow0, BottomPixelIndex, xLookup, targetWidth, yEndMap, true);
|
|
||||||
|
|
||||||
xStart = OffsetX;
|
|
||||||
x0 = 0;
|
|
||||||
|
|
||||||
var outputBuffer = MemoryAllocator.Allocate<byte>(outputBufferSize);
|
|
||||||
ref var outputChannel = ref outputBuffer.Span[0];
|
|
||||||
var boxHeight = yEnd - yStart;
|
|
||||||
|
|
||||||
var xEnd = xLookup[0];
|
|
||||||
px = yEndMap[0];
|
|
||||||
px += yStartMap[^1];
|
|
||||||
px -= yEndMap[^1];
|
|
||||||
px -= yStartMap[0];
|
|
||||||
px /= Math.Max(1, (xEnd - xStart) * boxHeight);
|
|
||||||
|
|
||||||
outputChannel = rChannel;
|
|
||||||
outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1);
|
|
||||||
|
|
||||||
outputChannel = gChannel;
|
|
||||||
outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1);
|
|
||||||
|
|
||||||
outputChannel = bChannel;
|
|
||||||
outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1);
|
|
||||||
|
|
||||||
xStart = xEnd;
|
|
||||||
|
|
||||||
for (int x1 = 1; x1 < targetWidth; x1++)
|
|
||||||
{
|
|
||||||
xEnd = xLookup[x1];
|
|
||||||
|
|
||||||
px = yEndMap[x1];
|
|
||||||
px += yStartMap[x0];
|
|
||||||
px -= yEndMap[x0];
|
|
||||||
px -= yStartMap[x1];
|
|
||||||
px /= Math.Max(1, (xEnd - xStart) * boxHeight);
|
|
||||||
|
|
||||||
outputChannel = rChannel;
|
|
||||||
outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1);
|
|
||||||
|
|
||||||
outputChannel = gChannel;
|
|
||||||
outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1);
|
|
||||||
|
|
||||||
outputChannel = bChannel;
|
|
||||||
outputChannel = ref Unsafe.AddByteOffset(ref outputChannel, 1);
|
|
||||||
|
|
||||||
xStart = xEnd;
|
|
||||||
x0 = x1;
|
|
||||||
}
|
|
||||||
|
|
||||||
outputTaskQueue = outputTaskQueue
|
|
||||||
.ContinueWith(async _ =>
|
|
||||||
{
|
|
||||||
await encoder.WriteDataAsync(outputBuffer, cancellationToken: cancellationToken);
|
|
||||||
}, cancellationToken);
|
|
||||||
|
|
||||||
yStart = yEnd;
|
|
||||||
}
|
|
||||||
|
|
||||||
await outputTaskQueue;
|
|
||||||
await encoder.WriteEndOfFileAsync(cancellationToken);
|
await encoder.WriteEndOfFileAsync(cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -333,33 +170,23 @@ public sealed class DangerousImageCreator : IDisposable
|
||||||
IBuffer<Int32Pixel> destination,
|
IBuffer<Int32Pixel> destination,
|
||||||
bool appendMode = false)
|
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 sourceMap = boundsMatrix.Span[..count];
|
||||||
var currentTile = TileManager.GetAdjacent(TileOrigin, 0, rowOffset);
|
|
||||||
var xAdder = Int32Pixel.Zero;
|
var xAdder = Int32Pixel.Zero;
|
||||||
var xOffset = 0;
|
var xOffset = 0;
|
||||||
var written = 0;
|
var written = 0;
|
||||||
var destinationSpan = destination.Span;
|
var destinationSpan = destination.Span;
|
||||||
var readBufferSpan = _mmfReadBuffer.Span;
|
var readBufferSpan = _mmfReadBuffer.Span;
|
||||||
|
|
||||||
var negative = sourceMap[0] - 1;
|
|
||||||
var negativePixel = appendMode ? destinationSpan[^1] : Int32Pixel.Zero;
|
|
||||||
if (negative >= 0)
|
|
||||||
{
|
|
||||||
negativePixel = readBufferSpan[negative];
|
|
||||||
}
|
|
||||||
else if(currentTile.Column > 1)
|
|
||||||
{
|
|
||||||
// Cross row tile, no need to handle if it's first column tile for now
|
|
||||||
// the provided asset is bordered black anyway
|
|
||||||
TileManager.GetAdjacent(currentTile, -1, 0)
|
|
||||||
.Integral
|
|
||||||
.Acquire(yOffset, readBufferSpan);
|
|
||||||
negativePixel = readBufferSpan[RightmostPixelIndex];
|
|
||||||
xAdder = readBufferSpan[RightmostPixelIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
destinationSpan[^1] = negativePixel;
|
|
||||||
|
|
||||||
while (true)
|
while (true)
|
||||||
{
|
{
|
||||||
currentTile.Integral.Acquire(yOffset, readBufferSpan);
|
currentTile.Integral.Acquire(yOffset, readBufferSpan);
|
||||||
|
|
@ -387,9 +214,15 @@ public sealed class DangerousImageCreator : IDisposable
|
||||||
if (written >= sourceMap.Length)
|
if (written >= sourceMap.Length)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
currentTile = TileManager.TryGetAdjacent(currentTile, 1, 0);
|
||||||
|
if (currentTile == null)
|
||||||
|
{
|
||||||
|
destinationSpan[written] = destinationSpan[written - 1];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
xAdder += readBufferSpan[RightmostPixelIndex];
|
xAdder += readBufferSpan[RightmostPixelIndex];
|
||||||
xOffset += TileWidth;
|
xOffset += TileWidth;
|
||||||
currentTile = TileManager.GetAdjacent(currentTile, 1, 0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,160 +0,0 @@
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using SixLabors.ImageSharp.PixelFormats;
|
|
||||||
using StitchATon2.Infra;
|
|
||||||
using StitchATon2.Infra.Buffers;
|
|
||||||
using StitchATon2.Infra.Encoders;
|
|
||||||
|
|
||||||
namespace StitchATon2.Domain.ImageCreators;
|
|
||||||
|
|
||||||
public class ImageCreator : IDisposable
|
|
||||||
{
|
|
||||||
private readonly GridSection _section;
|
|
||||||
|
|
||||||
private int FullWidth => _section.TileManager.Configuration.FullWidth;
|
|
||||||
private int FullHeight => _section.TileManager.Configuration.FullHeight;
|
|
||||||
|
|
||||||
private int OffsetX => _section.OffsetX;
|
|
||||||
private int OffsetY => _section.OffsetY;
|
|
||||||
|
|
||||||
private int Width => _section.Width;
|
|
||||||
private int Height => _section.Height;
|
|
||||||
|
|
||||||
private int TileWidth => _section.TileManager.Configuration.Width;
|
|
||||||
private int TileHeight => _section.TileManager.Configuration.Height;
|
|
||||||
private Tile TileOrigin => _section.Origin;
|
|
||||||
|
|
||||||
private int RightmostPixelIndex => _section.TileManager.Configuration.RightTileIndex;
|
|
||||||
private int BottomPixelIndex => _section.TileManager.Configuration.BottomTileIndex;
|
|
||||||
|
|
||||||
private TileManager TileManager => _section.TileManager;
|
|
||||||
|
|
||||||
private readonly ArrayOwner<Int32Pixel> _mmfReadBuffer;
|
|
||||||
|
|
||||||
public ImageCreator(GridSection section)
|
|
||||||
{
|
|
||||||
_section = section;
|
|
||||||
_mmfReadBuffer = MemoryAllocator.AllocateArray<Int32Pixel>(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.Array);
|
|
||||||
int localX;
|
|
||||||
while (written < sourceMap.Length && (localX = sourceMap[written] - xOffset) < TileWidth)
|
|
||||||
{
|
|
||||||
destination.Span[written] = _mmfReadBuffer[localX];
|
|
||||||
destination.Span[written] += xAdder;
|
|
||||||
written++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (written >= sourceMap.Length)
|
|
||||||
break;
|
|
||||||
|
|
||||||
xAdder += _mmfReadBuffer[RightmostPixelIndex];
|
|
||||||
xOffset += TileWidth;
|
|
||||||
currentTile = TileManager.GetAdjacent(currentTile, 1, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void MapRowAppend(int rowOffset, int yOffset, Span<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.Array);
|
|
||||||
int localX;
|
|
||||||
while (written < sourceMap.Length && (localX = sourceMap[written] - xOffset) < TileWidth)
|
|
||||||
{
|
|
||||||
destination.Span[written] += _mmfReadBuffer[localX];
|
|
||||||
destination.Span[written] += xAdder;
|
|
||||||
written++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (written >= sourceMap.Length)
|
|
||||||
break;
|
|
||||||
|
|
||||||
xAdder += _mmfReadBuffer[RightmostPixelIndex];
|
|
||||||
xOffset += TileWidth;
|
|
||||||
currentTile = TileManager.GetAdjacent(currentTile, 1, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
_mmfReadBuffer.Dispose();
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -24,7 +24,7 @@ public sealed class TileManager
|
||||||
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.GetSbsNotationRow(++row)}{++column}";
|
||||||
return new Tile
|
return new Tile
|
||||||
{
|
{
|
||||||
Id = id,
|
Id = id,
|
||||||
|
|
@ -51,7 +51,7 @@ public sealed class TileManager
|
||||||
|
|
||||||
public Tile GetTile(string coordinate)
|
public Tile GetTile(string coordinate)
|
||||||
{
|
{
|
||||||
var (column, row) = Utils.GetSBSCoordinate(coordinate);
|
var (column, row) = Utils.GetSbsNotationCoordinate(coordinate);
|
||||||
return GetTile(column, row);
|
return GetTile(column, row);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 GetSbsNotationRow(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)]);
|
||||||
|
|
@ -21,7 +21,7 @@ public static class Utils
|
||||||
}
|
}
|
||||||
|
|
||||||
[Pure]
|
[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]))
|
||||||
|
|
@ -43,18 +43,18 @@ public static class Utils
|
||||||
/// <param name="offset">The offset to apply before clamping.</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, Vector<float>.Count);
|
var vectorSize = DivCeil(length + 1, 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 - 1);
|
var vectorOffset = new Vector<int>(offset);
|
||||||
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 = Vector.CreateSequence(0f, 1f);
|
||||||
|
|
||||||
var seq = 0f;
|
var seq = -1f;
|
||||||
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;
|
||||||
|
|
@ -71,53 +71,6 @@ public static class Utils
|
||||||
resultSpan[i] = Vector.ClampNative(resultSpan[i], vectorMin, vectorMax);
|
resultSpan[i] = Vector.ClampNative(resultSpan[i], vectorMin, vectorMax);
|
||||||
}
|
}
|
||||||
|
|
||||||
var negative = float.Ceiling(scaleFactor);
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static (IBuffer<int>, IBuffer<int>) DoubleBoundsMatrix(float scaleFactor, int length, int max, int offset)
|
|
||||||
{
|
|
||||||
var vectorSize = DivCeil(length, Vector<float>.Count);
|
|
||||||
using var startBuffer = MemoryAllocator.Allocate<Vector<float>>(vectorSize);
|
|
||||||
using var endBuffer = MemoryAllocator.Allocate<Vector<float>>(vectorSize);
|
|
||||||
|
|
||||||
var startSpan = startBuffer.Span;
|
|
||||||
var endSpan = endBuffer.Span;
|
|
||||||
|
|
||||||
var vectorMin = Vector<int>.Zero;
|
|
||||||
var vectorOne = Vector<int>.One;
|
|
||||||
var vectorMax = Vector.Create(max);
|
|
||||||
var vectorScale = Vector.Create(scaleFactor);
|
|
||||||
var vectorOffset = new Vector<int>(offset - 1);
|
|
||||||
|
|
||||||
for (int i = 0, seq = 0; i < vectorSize; i++, seq += Vector<float>.Count)
|
|
||||||
{
|
|
||||||
startSpan[i] = Vector.CreateSequence(seq, 1f);
|
|
||||||
startSpan[i] = Vector.Multiply(startSpan[i], vectorScale);
|
|
||||||
endSpan[i] = Vector.Add(vectorScale, startSpan[i]);
|
|
||||||
endSpan[i] = Vector.Ceiling(endSpan[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
var resultStart = MemoryAllocator.Allocate<int>(vectorSize * Vector<int>.Count);
|
|
||||||
var resultEnd = MemoryAllocator.Allocate<int>(vectorSize * Vector<int>.Count);
|
|
||||||
|
|
||||||
var resultStartSpan = MemoryMarshal.Cast<int, Vector<int>>(resultStart.Span);
|
|
||||||
var resultEndSpan = MemoryMarshal.Cast<int, Vector<int>>(resultEnd.Span);
|
|
||||||
|
|
||||||
for (var i = 0; i < vectorSize; i++)
|
|
||||||
{
|
|
||||||
resultStartSpan[i] = Vector.ConvertToInt32(startSpan[i]);
|
|
||||||
resultStartSpan[i] = Vector.Subtract(resultStartSpan[i], vectorOne);
|
|
||||||
resultStartSpan[i] = Vector.Add(resultStartSpan[i], vectorOffset);
|
|
||||||
resultStartSpan[i] = Vector.Clamp(resultStartSpan[i], vectorMin, vectorMax);
|
|
||||||
|
|
||||||
resultEndSpan[i] = Vector.ConvertToInt32(endSpan[i]);
|
|
||||||
resultEndSpan[i] = Vector.Subtract(resultEndSpan[i], vectorOne);
|
|
||||||
resultEndSpan[i] = Vector.Add(resultEndSpan[i], vectorOffset);
|
|
||||||
resultEndSpan[i] = Vector.Clamp(resultEndSpan[i], vectorMin, vectorMax);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (resultStart, resultEnd);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -24,7 +24,8 @@ 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;
|
public Span<T> Span => _buffer.AsSpan(0, Length);
|
||||||
|
public Memory<T> Memory => _buffer.AsMemory(0, Length);
|
||||||
|
|
||||||
public T[] Array => _buffer;
|
public T[] Array => _buffer;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,5 +6,7 @@ public interface IBuffer<T> : IDisposable where T : unmanaged
|
||||||
|
|
||||||
Span<T> Span { get; }
|
Span<T> Span { get; }
|
||||||
|
|
||||||
|
Memory<T> Memory { get; }
|
||||||
|
|
||||||
int Length { get; }
|
int Length { get; }
|
||||||
}
|
}
|
||||||
|
|
@ -4,34 +4,48 @@ using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace StitchATon2.Infra.Buffers;
|
namespace StitchATon2.Infra.Buffers;
|
||||||
|
|
||||||
internal sealed unsafe class ImmovableMemory<T> : MemoryManager<T> where T : unmanaged
|
internal sealed unsafe class ImmovableMemory<T> : MemoryManager<T>, IBuffer<T> where T : unmanaged
|
||||||
{
|
{
|
||||||
private readonly T* _pointer;
|
internal readonly T* Pointer;
|
||||||
private readonly int _length;
|
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
public ImmovableMemory(int count)
|
public ImmovableMemory(int length)
|
||||||
{
|
{
|
||||||
_pointer = (T*)NativeMemory.Alloc((nuint)count, (nuint)Unsafe.SizeOf<T>());
|
Pointer = (T*)NativeMemory.Alloc((nuint)length, (nuint)Unsafe.SizeOf<T>());
|
||||||
_length = count;
|
Length = length;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void Dispose(bool disposing)
|
protected override void Dispose(bool disposing)
|
||||||
{
|
{
|
||||||
if (!_disposed)
|
if (_disposed) return;
|
||||||
{
|
NativeMemory.Free(Pointer);
|
||||||
NativeMemory.Free(_pointer);
|
_disposed = true;
|
||||||
_disposed = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override Span<T> GetSpan()
|
public override Span<T> GetSpan()
|
||||||
=> new(_pointer, _length);
|
=> _disposed
|
||||||
|
? throw new ObjectDisposedException(nameof(ImmovableMemory<T>))
|
||||||
|
: new Span<T>(Pointer, Length);
|
||||||
|
|
||||||
public override MemoryHandle Pin(int elementIndex = 0)
|
public override MemoryHandle Pin(int elementIndex = 0)
|
||||||
=> new(_pointer + elementIndex);
|
=> _disposed
|
||||||
|
? throw new ObjectDisposedException(nameof(ImmovableMemory<T>))
|
||||||
|
: new MemoryHandle(Pointer + elementIndex);
|
||||||
|
|
||||||
public override void Unpin()
|
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; }
|
||||||
}
|
}
|
||||||
|
|
@ -6,7 +6,7 @@ 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 UnmanagedMemory<T>(count);
|
=> new ImmovableMemory<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);
|
||||||
|
|
@ -14,31 +14,23 @@ public static class MemoryAllocator
|
||||||
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 MemoryManager<T> AllocateImmovable<T>(int count) where T : unmanaged
|
|
||||||
=> new ImmovableMemory<T>(count);
|
|
||||||
|
|
||||||
public static unsafe IBuffer<T> Clone<T>(this IBuffer<T> buffer) where T : unmanaged
|
public static unsafe IBuffer<T> Clone<T>(this IBuffer<T> buffer) where T : unmanaged
|
||||||
{
|
{
|
||||||
if (buffer is UnmanagedMemory<T> unmanagedMemory)
|
if (buffer is not ImmovableMemory<T> unmanagedMemory)
|
||||||
{
|
throw new NotSupportedException();
|
||||||
var newBuffer = new UnmanagedMemory<T>(buffer.Length);
|
|
||||||
var byteCount = (uint)(Unsafe.SizeOf<T>() * buffer.Length);
|
|
||||||
Unsafe.CopyBlock(newBuffer.Pointer, unmanagedMemory.Pointer, byteCount);
|
|
||||||
return newBuffer;
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
public static unsafe void Copy<T>(this IBuffer<T> source, IBuffer<T> destination, int count) where T : unmanaged
|
||||||
{
|
{
|
||||||
if (source is UnmanagedMemory<T> sourceBuffer && destination is UnmanagedMemory<T> destinationBuffer)
|
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);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new NotSupportedException();
|
var byteCount = (uint)(Unsafe.SizeOf<T>() * count);
|
||||||
|
Unsafe.CopyBlock(destinationBuffer.Pointer, sourceBuffer.Pointer, byteCount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -7,11 +7,13 @@ namespace StitchATon2.Infra.Buffers;
|
||||||
/// Provide non-thread safe anti GC contiguous memory.
|
/// Provide non-thread safe anti GC contiguous memory.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="T"></typeparam>
|
/// <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;
|
internal readonly T* Pointer;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
|
public Memory<T> Memory => throw new NotImplementedException();
|
||||||
public int Length { get; }
|
public int Length { get; }
|
||||||
|
|
||||||
public ref T this[int index] => ref Unsafe.AsRef<T>(Pointer + index);
|
public ref T this[int index] => ref Unsafe.AsRef<T>(Pointer + index);
|
||||||
|
|
|
||||||
|
|
@ -63,26 +63,32 @@ public class PngPipeEncoder : IDisposable
|
||||||
|
|
||||||
public async Task WriteDataAsync(IBuffer<byte> buffer, bool disposeBuffer = true, CancellationToken cancellationToken = default)
|
public async Task WriteDataAsync(IBuffer<byte> buffer, bool disposeBuffer = true, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
_zlibStream.Write([0]);
|
try
|
||||||
|
|
||||||
var offset = 0;
|
|
||||||
while (buffer.Length - offset > FlushThreshold)
|
|
||||||
{
|
{
|
||||||
_zlibStream.Write(buffer.Span.Slice(offset, FlushThreshold));
|
_zlibStream.Write([0]);
|
||||||
await _zlibStream.FlushAsync(cancellationToken);
|
|
||||||
offset += FlushThreshold;
|
|
||||||
if(_memoryStream.Length >= BufferSize)
|
|
||||||
await FlushAsync(cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (buffer.Length > offset)
|
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
|
||||||
{
|
{
|
||||||
_zlibStream.Write(buffer.Span[offset..]);
|
if(disposeBuffer)
|
||||||
await _zlibStream.FlushAsync(cancellationToken);
|
buffer.Dispose();
|
||||||
_shouldFlush = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if(disposeBuffer) buffer.Dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task FlushAsync(CancellationToken cancellationToken)
|
private async Task FlushAsync(CancellationToken cancellationToken)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
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;
|
||||||
|
|
||||||
|
|
@ -30,7 +31,7 @@ public class PngStreamEncoder : IDisposable, IAsyncDisposable
|
||||||
|
|
||||||
~PngStreamEncoder() => Dispose();
|
~PngStreamEncoder() => Dispose();
|
||||||
|
|
||||||
public async Task WriteHeader()
|
public async Task WriteHeader(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
byte[] headerBytes = [
|
byte[] headerBytes = [
|
||||||
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG Signature
|
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG Signature
|
||||||
|
|
@ -54,34 +55,42 @@ public class PngStreamEncoder : IDisposable, IAsyncDisposable
|
||||||
|
|
||||||
BinaryPrimitives.WriteUInt32BigEndian(headerBytes.AsSpan(29), crc);
|
BinaryPrimitives.WriteUInt32BigEndian(headerBytes.AsSpan(29), crc);
|
||||||
|
|
||||||
await _stream.WriteAsync(headerBytes);
|
await _stream.WriteAsync(headerBytes, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task WriteData(Memory<byte> data)
|
public async Task WriteDataAsync(IBuffer<byte> buffer, bool disposeBuffer = true, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
_zlibStream.Write([0]);
|
try
|
||||||
|
|
||||||
var dataSlice = data;
|
|
||||||
while (dataSlice.Length > FlushThreshold)
|
|
||||||
{
|
{
|
||||||
await _zlibStream.WriteAsync(dataSlice[..FlushThreshold]);
|
_zlibStream.Write([0]);
|
||||||
await _zlibStream.FlushAsync();
|
|
||||||
dataSlice = dataSlice[FlushThreshold..];
|
var dataSlice = buffer.Memory;
|
||||||
if(_memoryStream.Length >= BufferSize)
|
while (dataSlice.Length > FlushThreshold)
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
await _zlibStream.WriteAsync(dataSlice);
|
if(disposeBuffer)
|
||||||
await _zlibStream.FlushAsync();
|
buffer.Dispose();
|
||||||
_shouldFlush = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Flush()
|
private async Task FlushAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
await _zlibStream.FlushAsync();
|
await _zlibStream.FlushAsync(cancellationToken);
|
||||||
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);
|
||||||
|
|
@ -96,16 +105,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));
|
await _stream.WriteAsync(buffer.AsMemory(0, dataSize + 12), cancellationToken);
|
||||||
_memoryStream.SetLength(8);
|
_memoryStream.SetLength(8);
|
||||||
_memoryStream.Position = 8;
|
_memoryStream.Position = 8;
|
||||||
_shouldFlush = false;
|
_shouldFlush = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask WriteEndOfFile()
|
public async ValueTask WriteEndOfFileAsync(CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
if(_shouldFlush)
|
if(_shouldFlush)
|
||||||
await Flush();
|
await FlushAsync(cancellationToken);
|
||||||
|
|
||||||
var endChunk = new byte[] {
|
var endChunk = new byte[] {
|
||||||
0x00, 0x00, 0x00, 0x00, // Length
|
0x00, 0x00, 0x00, 0x00, // Length
|
||||||
|
|
@ -113,7 +122,7 @@ public class PngStreamEncoder : IDisposable, IAsyncDisposable
|
||||||
0xAE, 0x42, 0x60, 0x82, // Crc
|
0xAE, 0x42, 0x60, 0x82, // Crc
|
||||||
};
|
};
|
||||||
|
|
||||||
await _stream.WriteAsync(endChunk);
|
await _stream.WriteAsync(endChunk, cancellationToken);
|
||||||
await DisposeAsync();
|
await DisposeAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StitchATon2.Infra", "Infra\
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StitchATon2.Benchmark", "StitchATon2.Benchmark\StitchATon2.Benchmark.csproj", "{2F9B169C-C799-4489-B864-F912D69C5D3E}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StitchATon2.Benchmark", "StitchATon2.Benchmark\StitchATon2.Benchmark.csproj", "{2F9B169C-C799-4489-B864-F912D69C5D3E}"
|
||||||
EndProject
|
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
|
||||||
|
|
@ -30,5 +32,9 @@ Global
|
||||||
{2F9B169C-C799-4489-B864-F912D69C5D3E}.Debug|Any CPU.Build.0 = 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.ActiveCfg = Release|Any CPU
|
||||||
{2F9B169C-C799-4489-B864-F912D69C5D3E}.Release|Any CPU.Build.0 = 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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue