From 1456503a3fca091e5f2533abcc72415f89fb53e9 Mon Sep 17 00:00:00 2001 From: Renjaya Raga Zenta Date: Thu, 31 Jul 2025 00:31:51 +0700 Subject: [PATCH] optimize: use ReadOnlySpan instead of string --- src/Oh.My.Stitcher/Oh.My.Stitcher.csproj | 10 +- src/Oh.My.Stitcher/Tile.cs | 162 ++++++++++++++++++----- 2 files changed, 136 insertions(+), 36 deletions(-) diff --git a/src/Oh.My.Stitcher/Oh.My.Stitcher.csproj b/src/Oh.My.Stitcher/Oh.My.Stitcher.csproj index ca91574..b82d246 100644 --- a/src/Oh.My.Stitcher/Oh.My.Stitcher.csproj +++ b/src/Oh.My.Stitcher/Oh.My.Stitcher.csproj @@ -6,13 +6,21 @@ enable true true + true - + + + + + + + + diff --git a/src/Oh.My.Stitcher/Tile.cs b/src/Oh.My.Stitcher/Tile.cs index 7d613c7..72ba464 100644 --- a/src/Oh.My.Stitcher/Tile.cs +++ b/src/Oh.My.Stitcher/Tile.cs @@ -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 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 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 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,60 +148,84 @@ 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) + while( row > 0 ) { - int remainder = (row - 1) % 26; - letterPart = (char)('A' + remainder) + letterPart; - row = (row - 1) / 26; + int remainder = ( row - 1 ) % 26; + 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 buffer) { - minRow = maxRow = minCol = maxCol = 0; - string[] corners = rect.Split(':'); - switch( corners.Length ) + int p = 0; + p += Encoding.UTF8.GetBytes(directory, buffer); + buffer[p++] = (byte)Path.DirectorySeparatorChar; + int letterStart = p; + if( row > 0 ) { - case 1: - { - if( !TryParseName(corners[0], out int r, out int c) ) - return false; + while( row > 0 ) + { + buffer[p++] = (byte)( 'A' + ( row - 1 ) % 26 ); + row = ( row - 1 ) / 26; + } - minRow = maxRow = r; - 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; + 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; } - private static bool TryParseName(string name, out int row, out int col) + internal static bool TryParseRect(ReadOnlySpan 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 left = rect.Slice(0, colonIndex); + ReadOnlySpan 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 name, out int row, out int col) { row = col = 0; - ReadOnlySpan span = name.AsSpan().Trim(); + ReadOnlySpan span = name.Trim(); int splitIndex = span.IndexOfAnyInRange('0', '9'); if( splitIndex <= 0 ) return false; - if( !int.TryParse(span.Slice(splitIndex), out col)) + if( !int.TryParse(span.Slice(splitIndex), out col) ) return false; int letter = 0; foreach( char c in span.Slice(0, splitIndex) )