various 'improvements'

This commit is contained in:
2025-12-11 12:59:32 +09:00
parent 2e14fc2f8f
commit 6cf0e3daed
26 changed files with 3706 additions and 884 deletions

View File

@@ -1,8 +1,9 @@
using System;
using System.Collections.Concurrent;
using System.Buffers;
using System.Buffers.Binary;
using System.Globalization;
using System.IO;
using System.Runtime.InteropServices;
using OtterTex;
using OtterImage = OtterTex.Image;
using LightlessSync.LightlessConfiguration;
@@ -11,7 +12,6 @@ using Microsoft.Extensions.Logging;
using Lumina.Data.Files;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.PixelFormats;
using SixLabors.ImageSharp.Processing;
/*
* OtterTex made by Ottermandias
@@ -33,6 +33,7 @@ public sealed class TextureDownscaleService
private readonly ConcurrentDictionary<string, Task> _activeJobs = new(StringComparer.OrdinalIgnoreCase);
private readonly ConcurrentDictionary<string, string> _downscaledPaths = new(StringComparer.OrdinalIgnoreCase);
private readonly SemaphoreSlim _downscaleSemaphore = new(4);
private static readonly IReadOnlyDictionary<int, TextureCompressionTarget> BlockCompressedFormatMap =
new Dictionary<int, TextureCompressionTarget>
{
@@ -80,7 +81,10 @@ public sealed class TextureDownscaleService
if (!filePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase)) return;
if (_activeJobs.ContainsKey(hash)) return;
_activeJobs[hash] = Task.Run(() => DownscaleInternalAsync(hash, filePath, mapKind), CancellationToken.None);
_activeJobs[hash] = Task.Run(async () =>
{
await DownscaleInternalAsync(hash, filePath, mapKind).ConfigureAwait(false);
}, CancellationToken.None);
}
public string GetPreferredPath(string hash, string originalPath)
@@ -108,6 +112,7 @@ public sealed class TextureDownscaleService
bool onlyDownscaleUncompressed = false;
bool? isIndexTexture = null;
await _downscaleSemaphore.WaitAsync().ConfigureAwait(false);
try
{
if (!File.Exists(sourcePath))
@@ -157,6 +162,15 @@ public sealed class TextureDownscaleService
return;
}
if (headerInfo is { } headerValue &&
headerValue.Width <= targetMaxDimension &&
headerValue.Height <= targetMaxDimension)
{
_downscaledPaths[hash] = sourcePath;
_logger.LogTrace("Skipping downscale for index texture {Hash}; header dimensions {Width}x{Height} within target.", hash, headerValue.Width, headerValue.Height);
return;
}
if (onlyDownscaleUncompressed && headerInfo.HasValue && IsBlockCompressedFormat(headerInfo.Value.Format))
{
_downscaledPaths[hash] = sourcePath;
@@ -172,21 +186,20 @@ public sealed class TextureDownscaleService
var height = rgbaInfo.Meta.Height;
var requiredLength = width * height * bytesPerPixel;
var rgbaPixels = rgbaScratch.Pixels[..requiredLength].ToArray();
var rgbaPixels = rgbaScratch.Pixels.Slice(0, requiredLength);
using var originalImage = SixLabors.ImageSharp.Image.LoadPixelData<Rgba32>(rgbaPixels, width, height);
var targetSize = CalculateTargetSize(originalImage.Width, originalImage.Height, targetMaxDimension);
if (targetSize.width == originalImage.Width && targetSize.height == originalImage.Height)
{
_downscaledPaths[hash] = sourcePath;
_logger.LogTrace("Skipping downscale for index texture {Hash}; already within bounds.", hash);
return;
}
using var resized = IndexDownscaler.Downscale(originalImage, targetSize.width, targetSize.height, BlockMultiple);
var resizedPixels = new byte[targetSize.width * targetSize.height * 4];
resized.CopyPixelDataTo(resizedPixels);
using var resizedScratch = ScratchImage.FromRGBA(resizedPixels, targetSize.width, targetSize.height, out var creationInfo).ThrowIfError(creationInfo);
using var resizedScratch = CreateScratchImage(resized, targetSize.width, targetSize.height);
using var finalScratch = resizedScratch.Convert(DXGIFormat.B8G8R8A8UNorm);
TexFileHelper.Save(destination, finalScratch);
@@ -209,6 +222,7 @@ public sealed class TextureDownscaleService
}
finally
{
_downscaleSemaphore.Release();
_activeJobs.TryRemove(hash, out _);
}
}
@@ -227,6 +241,41 @@ public sealed class TextureDownscaleService
return (resultWidth, resultHeight);
}
private static ScratchImage CreateScratchImage(Image<Rgba32> image, int width, int height)
{
const int BytesPerPixel = 4;
var requiredLength = width * height * BytesPerPixel;
static ScratchImage Create(ReadOnlySpan<byte> pixels, int width, int height)
{
var scratchResult = ScratchImage.FromRGBA(pixels, width, height, out var creationInfo);
return scratchResult.ThrowIfError(creationInfo);
}
if (image.DangerousTryGetSinglePixelMemory(out var pixelMemory))
{
var byteSpan = MemoryMarshal.AsBytes(pixelMemory.Span);
if (byteSpan.Length < requiredLength)
{
throw new InvalidOperationException($"Image buffer shorter than expected ({byteSpan.Length} < {requiredLength}).");
}
return Create(byteSpan.Slice(0, requiredLength), width, height);
}
var rented = ArrayPool<byte>.Shared.Rent(requiredLength);
try
{
var rentedSpan = rented.AsSpan(0, requiredLength);
image.CopyPixelDataTo(rentedSpan);
return Create(rentedSpan, width, height);
}
finally
{
ArrayPool<byte>.Shared.Return(rented);
}
}
private static bool IsIndexMap(TextureMapKind kind)
=> kind is TextureMapKind.Mask
or TextureMapKind.Index;
@@ -420,21 +469,6 @@ public sealed class TextureDownscaleService
private static int ReduceDimension(int value)
=> value <= 1 ? 1 : Math.Max(1, value / 2);
private static Image<Rgba32> ReduceLinearTexture(Image<Rgba32> source, int targetWidth, int targetHeight)
{
var clone = source.Clone();
while (clone.Width > targetWidth || clone.Height > targetHeight)
{
var nextWidth = Math.Max(targetWidth, Math.Max(BlockMultiple, clone.Width / 2));
var nextHeight = Math.Max(targetHeight, Math.Max(BlockMultiple, clone.Height / 2));
clone.Mutate(ctx => ctx.Resize(nextWidth, nextHeight, KnownResamplers.Lanczos3));
}
return clone;
}
private static bool ShouldTrim(in TexMeta meta, int targetMaxDimension)
{
var depth = meta.Dimension == TexDimension.Tex3D ? Math.Max(1, meta.Depth) : 1;
@@ -443,12 +477,7 @@ public sealed class TextureDownscaleService
private static bool ShouldTrimDimensions(int width, int height, int depth, int targetMaxDimension)
{
if (width <= targetMaxDimension || height <= targetMaxDimension)
{
return false;
}
if (depth > 1 && depth <= targetMaxDimension)
if (width <= targetMaxDimension && height <= targetMaxDimension && depth <= targetMaxDimension)
{
return false;
}