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>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<InvariantGlobalization>true</InvariantGlobalization>
|
<InvariantGlobalization>true</InvariantGlobalization>
|
||||||
<PublishAot>true</PublishAot>
|
<PublishAot>true</PublishAot>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="NetVips" Version="3.1.0" />
|
|
||||||
<PackageReference Include="NetVips.Native.linux-x64" Version="8.17.1" />
|
<PackageReference Include="NetVips.Native.linux-x64" Version="8.17.1" />
|
||||||
<PackageReference Include="Validation" Version="2.6.68" />
|
<PackageReference Include="Validation" Version="2.6.68" />
|
||||||
<PackageReference Include="ZLogger" Version="2.5.10" />
|
<PackageReference Include="ZLogger" Version="2.5.10" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\vendor\NetVips\NetVips.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="Oh.My.Stitcher.Benchmark" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,11 @@
|
||||||
// ReSharper disable ReplaceSliceWithRangeIndexer
|
// ReSharper disable ReplaceSliceWithRangeIndexer
|
||||||
|
|
||||||
|
using System.Buffers.Text;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using NetVips;
|
using NetVips;
|
||||||
|
using Oh.My.Stitcher.NetVips;
|
||||||
using ZLogger;
|
using ZLogger;
|
||||||
|
|
||||||
namespace Oh.My.Stitcher;
|
namespace Oh.My.Stitcher;
|
||||||
|
|
@ -73,6 +76,71 @@ public static class Tile
|
||||||
return true;
|
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)
|
public static Image CreateError(Exception e)
|
||||||
{
|
{
|
||||||
const int padding = 20;
|
const int padding = 20;
|
||||||
|
|
@ -80,60 +148,84 @@ public static class Tile
|
||||||
return text.Embed(padding, padding, text.Width + ( 2 * padding ), text.Height + ( 2 * padding ));
|
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 = "";
|
string letterPart = "";
|
||||||
while (row > 0)
|
while( row > 0 )
|
||||||
{
|
{
|
||||||
int remainder = (row - 1) % 26;
|
int remainder = ( row - 1 ) % 26;
|
||||||
letterPart = (char)('A' + remainder) + letterPart;
|
letterPart = (char)( 'A' + remainder ) + letterPart;
|
||||||
row = (row - 1) / 26;
|
row = ( row - 1 ) / 26;
|
||||||
}
|
}
|
||||||
|
|
||||||
string fileName = $"{letterPart}{col}.png";
|
string fileName = $"{letterPart}{col}.png";
|
||||||
return Path.Combine(directory, fileName);
|
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)
|
||||||
{
|
{
|
||||||
minRow = maxRow = minCol = maxCol = 0;
|
int p = 0;
|
||||||
string[] corners = rect.Split(':');
|
p += Encoding.UTF8.GetBytes(directory, buffer);
|
||||||
switch( corners.Length )
|
buffer[p++] = (byte)Path.DirectorySeparatorChar;
|
||||||
|
int letterStart = p;
|
||||||
|
if( row > 0 )
|
||||||
{
|
{
|
||||||
case 1:
|
while( row > 0 )
|
||||||
{
|
{
|
||||||
if( !TryParseName(corners[0], out int r, out int c) )
|
buffer[p++] = (byte)( 'A' + ( row - 1 ) % 26 );
|
||||||
return false;
|
row = ( row - 1 ) / 26;
|
||||||
|
}
|
||||||
|
|
||||||
minRow = maxRow = r;
|
buffer.Slice(letterStart, p - letterStart).Reverse();
|
||||||
minCol = maxCol = c;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 2:
|
|
||||||
{
|
|
||||||
if( !TryParseName(corners[0], out int r1, out int c1) || !TryParseName(corners[1], out int r2, out int c2) )
|
|
||||||
return false;
|
|
||||||
|
|
||||||
minRow = Math.Min(r1, r2);
|
|
||||||
maxRow = Math.Max(r1, r2);
|
|
||||||
minCol = Math.Min(c1, c2);
|
|
||||||
maxCol = Math.Max(c1, c2);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryParseName(string name, out int row, out int col)
|
internal static bool TryParseRect(ReadOnlySpan<char> rect, out int minRow, out int maxRow, out int minCol,
|
||||||
|
out int maxCol)
|
||||||
|
{
|
||||||
|
minRow = maxRow = minCol = maxCol = 0;
|
||||||
|
int colonIndex = rect.IndexOf(':');
|
||||||
|
|
||||||
|
if( colonIndex == -1 )
|
||||||
|
{
|
||||||
|
if( !TryParseName(rect, out int r, out int c) )
|
||||||
|
return false;
|
||||||
|
|
||||||
|
minRow = maxRow = r;
|
||||||
|
minCol = maxCol = c;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
maxRow = Math.Max(r1, r2);
|
||||||
|
minCol = Math.Min(c1, c2);
|
||||||
|
maxCol = Math.Max(c1, c2);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseName(ReadOnlySpan<char> name, out int row, out int col)
|
||||||
{
|
{
|
||||||
row = col = 0;
|
row = col = 0;
|
||||||
ReadOnlySpan<char> span = name.AsSpan().Trim();
|
ReadOnlySpan<char> span = name.Trim();
|
||||||
int splitIndex = span.IndexOfAnyInRange('0', '9');
|
int splitIndex = span.IndexOfAnyInRange('0', '9');
|
||||||
if( splitIndex <= 0 )
|
if( splitIndex <= 0 )
|
||||||
return false;
|
return false;
|
||||||
if( !int.TryParse(span.Slice(splitIndex), out col))
|
if( !int.TryParse(span.Slice(splitIndex), out col) )
|
||||||
return false;
|
return false;
|
||||||
int letter = 0;
|
int letter = 0;
|
||||||
foreach( char c in span.Slice(0, splitIndex) )
|
foreach( char c in span.Slice(0, splitIndex) )
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue