optimize: use ReadOnlySpan instead of string
This commit is contained in:
parent
7c637eec3e
commit
1456503a3f
2 changed files with 136 additions and 36 deletions
|
|
@ -6,13 +6,21 @@
|
|||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<InvariantGlobalization>true</InvariantGlobalization>
|
||||
<PublishAot>true</PublishAot>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NetVips" Version="3.1.0" />
|
||||
<PackageReference Include="NetVips.Native.linux-x64" Version="8.17.1" />
|
||||
<PackageReference Include="Validation" Version="2.6.68" />
|
||||
<PackageReference Include="ZLogger" Version="2.5.10" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\vendor\NetVips\NetVips.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="Oh.My.Stitcher.Benchmark" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
// ReSharper disable ReplaceSliceWithRangeIndexer
|
||||
|
||||
using System.Buffers.Text;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using NetVips;
|
||||
using Oh.My.Stitcher.NetVips;
|
||||
using ZLogger;
|
||||
|
||||
namespace Oh.My.Stitcher;
|
||||
|
|
@ -73,6 +76,71 @@ public static class Tile
|
|||
return true;
|
||||
}
|
||||
|
||||
public static bool TryCreateFast(in Stitch request, string tilesDirectory, List<Image> images, ILogger logger,
|
||||
IMemoryCache cache, out Image? image, out string? cacheKey, out string? cacheFile)
|
||||
{
|
||||
if( !TryParseRect(request.CanvasRect, out int minRow, out int maxRow, out int minCol, out int maxCol) )
|
||||
throw new ArgumentException($"Invalid canvas_rect: '{request.CanvasRect}'");
|
||||
if( maxRow > byte.MaxValue || maxCol > byte.MaxValue )
|
||||
throw new NotSupportedException($"Unsupported rect row: {maxRow}, col: {maxCol}");
|
||||
|
||||
logger.ZLogDebug(
|
||||
$"rect: {request.CanvasRect}, minRow: {minRow}, maxRow: {maxRow}, minCol: {minCol}, maxCol: {maxCol}");
|
||||
int width = maxCol - minCol + 1;
|
||||
int height = maxRow - minRow + 1;
|
||||
|
||||
float cropOffsetX = request.CropOffset.X;
|
||||
float cropOffsetY = request.CropOffset.Y;
|
||||
float cropSizeW = request.CropSize.Width;
|
||||
float cropSizeH = request.CropSize.Height;
|
||||
float outputScale = request.OutputScale;
|
||||
|
||||
image = null;
|
||||
cacheKey = cacheFile = null;
|
||||
|
||||
// predicted size
|
||||
int predictedW = (int)( width * TILE_SIZE * cropSizeW * outputScale );
|
||||
int predictedH = (int)( height * TILE_SIZE * cropSizeH * outputScale );
|
||||
bool canBeCached = ( predictedW * predictedH ) <= MAX_CACHED_TILE_SIZE;
|
||||
if( canBeCached )
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[24];
|
||||
byte minRowByte = (byte)minRow;
|
||||
byte maxRowByte = (byte)maxRow;
|
||||
byte minColByte = (byte)minCol;
|
||||
byte maxColByte = (byte)maxCol;
|
||||
MemoryMarshal.Write(buffer, in minRowByte);
|
||||
MemoryMarshal.Write(buffer[1..], in maxRowByte);
|
||||
MemoryMarshal.Write(buffer[2..], in minColByte);
|
||||
MemoryMarshal.Write(buffer[3..], in maxColByte);
|
||||
MemoryMarshal.Write(buffer[4..], in cropOffsetX);
|
||||
MemoryMarshal.Write(buffer[8..], in cropOffsetY);
|
||||
MemoryMarshal.Write(buffer[12..], in cropSizeW);
|
||||
MemoryMarshal.Write(buffer[16..], in cropSizeH);
|
||||
MemoryMarshal.Write(buffer[20..], in outputScale);
|
||||
cacheKey = Convert.ToHexString(buffer).ToLowerInvariant();
|
||||
if( cache.TryGetValue(cacheKey, out cacheFile) && File.Exists(cacheFile) )
|
||||
return false;
|
||||
}
|
||||
|
||||
Span<byte> pathBuffer = stackalloc byte[512];
|
||||
for( int row = minRow; row <= maxRow; row++ )
|
||||
for( int col = minCol; col <= maxCol; col++ )
|
||||
{
|
||||
int length = FullPathFast(tilesDirectory, row, col, pathBuffer);
|
||||
images.Add(( OperationHacks.Call("pngload", pathBuffer.Slice(0, length + 1)) as Image )!);
|
||||
}
|
||||
|
||||
using var canvasImage = Image.Arrayjoin(images.ToArray(), width);
|
||||
int cropLeft = (int)( canvasImage.Width * cropOffsetX );
|
||||
int cropTop = (int)( canvasImage.Height * cropOffsetY );
|
||||
int cropWidth = (int)( canvasImage.Width * cropSizeW );
|
||||
int cropHeight = (int)( canvasImage.Height * cropSizeH );
|
||||
|
||||
image = canvasImage.Crop(cropLeft, cropTop, cropWidth, cropHeight).Resize(outputScale);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static Image CreateError(Exception e)
|
||||
{
|
||||
const int padding = 20;
|
||||
|
|
@ -80,7 +148,7 @@ public static class Tile
|
|||
return text.Embed(padding, padding, text.Width + ( 2 * padding ), text.Height + ( 2 * padding ));
|
||||
}
|
||||
|
||||
private static string FullPath(string directory, int row, int col)
|
||||
internal static string FullPath(string directory, int row, int col)
|
||||
{
|
||||
string letterPart = "";
|
||||
while( row > 0 )
|
||||
|
|
@ -89,19 +157,47 @@ public static class Tile
|
|||
letterPart = (char)( 'A' + remainder ) + letterPart;
|
||||
row = ( row - 1 ) / 26;
|
||||
}
|
||||
|
||||
string fileName = $"{letterPart}{col}.png";
|
||||
return Path.Combine(directory, fileName);
|
||||
}
|
||||
|
||||
private static bool TryParseRect(string rect, out int minRow, out int maxRow, out int minCol, out int maxCol)
|
||||
internal static int FullPathFast(string directory, int row, int col, Span<byte> buffer)
|
||||
{
|
||||
int p = 0;
|
||||
p += Encoding.UTF8.GetBytes(directory, buffer);
|
||||
buffer[p++] = (byte)Path.DirectorySeparatorChar;
|
||||
int letterStart = p;
|
||||
if( row > 0 )
|
||||
{
|
||||
while( row > 0 )
|
||||
{
|
||||
buffer[p++] = (byte)( 'A' + ( row - 1 ) % 26 );
|
||||
row = ( row - 1 ) / 26;
|
||||
}
|
||||
|
||||
buffer.Slice(letterStart, p - letterStart).Reverse();
|
||||
}
|
||||
|
||||
Utf8Formatter.TryFormat(col, buffer.Slice(p), out int numBytesWritten);
|
||||
p += numBytesWritten;
|
||||
|
||||
".png"u8.CopyTo(buffer.Slice(p));
|
||||
p += 4;
|
||||
|
||||
buffer[p] = 0; // null terminator
|
||||
return p;
|
||||
}
|
||||
|
||||
internal static bool TryParseRect(ReadOnlySpan<char> rect, out int minRow, out int maxRow, out int minCol,
|
||||
out int maxCol)
|
||||
{
|
||||
minRow = maxRow = minCol = maxCol = 0;
|
||||
string[] corners = rect.Split(':');
|
||||
switch( corners.Length )
|
||||
int colonIndex = rect.IndexOf(':');
|
||||
|
||||
if( colonIndex == -1 )
|
||||
{
|
||||
case 1:
|
||||
{
|
||||
if( !TryParseName(corners[0], out int r, out int c) )
|
||||
if( !TryParseName(rect, out int r, out int c) )
|
||||
return false;
|
||||
|
||||
minRow = maxRow = r;
|
||||
|
|
@ -109,9 +205,10 @@ public static class Tile
|
|||
return true;
|
||||
}
|
||||
|
||||
case 2:
|
||||
{
|
||||
if( !TryParseName(corners[0], out int r1, out int c1) || !TryParseName(corners[1], out int r2, out int c2) )
|
||||
ReadOnlySpan<char> left = rect.Slice(0, colonIndex);
|
||||
ReadOnlySpan<char> right = rect.Slice(colonIndex + 1);
|
||||
|
||||
if( !TryParseName(left, out int r1, out int c1) || !TryParseName(right, out int r2, out int c2) )
|
||||
return false;
|
||||
|
||||
minRow = Math.Min(r1, r2);
|
||||
|
|
@ -121,15 +218,10 @@ public static class Tile
|
|||
return true;
|
||||
}
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseName(string name, out int row, out int col)
|
||||
private static bool TryParseName(ReadOnlySpan<char> name, out int row, out int col)
|
||||
{
|
||||
row = col = 0;
|
||||
ReadOnlySpan<char> span = name.AsSpan().Trim();
|
||||
ReadOnlySpan<char> span = name.Trim();
|
||||
int splitIndex = span.IndexOfAnyInRange('0', '9');
|
||||
if( splitIndex <= 0 )
|
||||
return false;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue