sigma update
This commit is contained in:
@@ -4,9 +4,11 @@ using System.Buffers.Binary;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using OtterTex;
|
||||
using OtterImage = OtterTex.Image;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.Utils;
|
||||
using LightlessSync.FileCache;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Lumina.Data.Files;
|
||||
@@ -30,10 +32,12 @@ public sealed class TextureDownscaleService
|
||||
private readonly LightlessConfigService _configService;
|
||||
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
|
||||
private readonly FileCacheManager _fileCacheManager;
|
||||
private readonly TextureCompressionService _textureCompressionService;
|
||||
|
||||
private readonly ConcurrentDictionary<string, Task> _activeJobs = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly TaskRegistry<string> _downscaleDeduplicator = new();
|
||||
private readonly ConcurrentDictionary<string, string> _downscaledPaths = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly SemaphoreSlim _downscaleSemaphore = new(4);
|
||||
private readonly SemaphoreSlim _compressionSemaphore = new(1);
|
||||
private static readonly IReadOnlyDictionary<int, TextureCompressionTarget> BlockCompressedFormatMap =
|
||||
new Dictionary<int, TextureCompressionTarget>
|
||||
{
|
||||
@@ -68,12 +72,14 @@ public sealed class TextureDownscaleService
|
||||
ILogger<TextureDownscaleService> logger,
|
||||
LightlessConfigService configService,
|
||||
PlayerPerformanceConfigService playerPerformanceConfigService,
|
||||
FileCacheManager fileCacheManager)
|
||||
FileCacheManager fileCacheManager,
|
||||
TextureCompressionService textureCompressionService)
|
||||
{
|
||||
_logger = logger;
|
||||
_configService = configService;
|
||||
_playerPerformanceConfigService = playerPerformanceConfigService;
|
||||
_fileCacheManager = fileCacheManager;
|
||||
_textureCompressionService = textureCompressionService;
|
||||
}
|
||||
|
||||
public void ScheduleDownscale(string hash, string filePath, TextureMapKind mapKind)
|
||||
@@ -82,9 +88,9 @@ public sealed class TextureDownscaleService
|
||||
public void ScheduleDownscale(string hash, string filePath, Func<TextureMapKind> mapKindFactory)
|
||||
{
|
||||
if (!filePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase)) return;
|
||||
if (_activeJobs.ContainsKey(hash)) return;
|
||||
if (_downscaleDeduplicator.TryGetExisting(hash, out _)) return;
|
||||
|
||||
_activeJobs[hash] = Task.Run(async () =>
|
||||
_downscaleDeduplicator.GetOrStart(hash, async () =>
|
||||
{
|
||||
TextureMapKind mapKind;
|
||||
try
|
||||
@@ -98,7 +104,7 @@ public sealed class TextureDownscaleService
|
||||
}
|
||||
|
||||
await DownscaleInternalAsync(hash, filePath, mapKind).ConfigureAwait(false);
|
||||
}, CancellationToken.None);
|
||||
});
|
||||
}
|
||||
|
||||
public bool ShouldScheduleDownscale(string filePath)
|
||||
@@ -107,7 +113,9 @@ public sealed class TextureDownscaleService
|
||||
return false;
|
||||
|
||||
var performanceConfig = _playerPerformanceConfigService.Current;
|
||||
return performanceConfig.EnableNonIndexTextureMipTrim || performanceConfig.EnableIndexTextureDownscale;
|
||||
return performanceConfig.EnableNonIndexTextureMipTrim
|
||||
|| performanceConfig.EnableIndexTextureDownscale
|
||||
|| performanceConfig.EnableUncompressedTextureCompression;
|
||||
}
|
||||
|
||||
public string GetPreferredPath(string hash, string originalPath)
|
||||
@@ -144,7 +152,7 @@ public sealed class TextureDownscaleService
|
||||
continue;
|
||||
}
|
||||
|
||||
if (_activeJobs.TryGetValue(hash, out var job))
|
||||
if (_downscaleDeduplicator.TryGetExisting(hash, out var job))
|
||||
{
|
||||
pending.Add(job);
|
||||
}
|
||||
@@ -182,10 +190,18 @@ public sealed class TextureDownscaleService
|
||||
targetMaxDimension = ResolveTargetMaxDimension();
|
||||
onlyDownscaleUncompressed = performanceConfig.OnlyDownscaleUncompressedTextures;
|
||||
|
||||
if (onlyDownscaleUncompressed && !headerInfo.HasValue)
|
||||
{
|
||||
_downscaledPaths[hash] = sourcePath;
|
||||
_logger.LogTrace("Skipping downscale for texture {Hash}; format unknown and only-uncompressed enabled.", hash);
|
||||
return;
|
||||
}
|
||||
|
||||
destination = Path.Combine(GetDownscaledDirectory(), $"{hash}.tex");
|
||||
if (File.Exists(destination))
|
||||
{
|
||||
RegisterDownscaledTexture(hash, sourcePath, destination);
|
||||
await TryAutoCompressAsync(hash, destination, mapKind, null).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -196,6 +212,7 @@ public sealed class TextureDownscaleService
|
||||
if (performanceConfig.EnableNonIndexTextureMipTrim
|
||||
&& await TryDropTopMipAsync(hash, sourcePath, destination, targetMaxDimension, onlyDownscaleUncompressed, headerInfo).ConfigureAwait(false))
|
||||
{
|
||||
await TryAutoCompressAsync(hash, destination, mapKind, null).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -206,6 +223,7 @@ public sealed class TextureDownscaleService
|
||||
|
||||
_downscaledPaths[hash] = sourcePath;
|
||||
_logger.LogTrace("Skipping downscale for non-index texture {Hash}; no mip reduction required.", hash);
|
||||
await TryAutoCompressAsync(hash, sourcePath, mapKind, headerInfo).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -213,6 +231,7 @@ public sealed class TextureDownscaleService
|
||||
{
|
||||
_downscaledPaths[hash] = sourcePath;
|
||||
_logger.LogTrace("Skipping downscale for index texture {Hash}; feature disabled.", hash);
|
||||
await TryAutoCompressAsync(hash, sourcePath, mapKind, headerInfo).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -222,6 +241,7 @@ public sealed class TextureDownscaleService
|
||||
{
|
||||
_downscaledPaths[hash] = sourcePath;
|
||||
_logger.LogTrace("Skipping downscale for index texture {Hash}; header dimensions {Width}x{Height} within target.", hash, headerValue.Width, headerValue.Height);
|
||||
await TryAutoCompressAsync(hash, sourcePath, mapKind, headerInfo).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -229,10 +249,12 @@ public sealed class TextureDownscaleService
|
||||
{
|
||||
_downscaledPaths[hash] = sourcePath;
|
||||
_logger.LogTrace("Skipping downscale for index texture {Hash}; block compressed format {Format}.", hash, headerInfo.Value.Format);
|
||||
await TryAutoCompressAsync(hash, sourcePath, mapKind, headerInfo).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
using var sourceScratch = TexFileHelper.Load(sourcePath);
|
||||
var sourceFormat = sourceScratch.Meta.Format;
|
||||
using var rgbaScratch = sourceScratch.GetRGBA(out var rgbaInfo).ThrowIfError(rgbaInfo);
|
||||
|
||||
var bytesPerPixel = rgbaInfo.Meta.Format.BitsPerPixel() / 8;
|
||||
@@ -248,16 +270,39 @@ public sealed class TextureDownscaleService
|
||||
{
|
||||
_downscaledPaths[hash] = sourcePath;
|
||||
_logger.LogTrace("Skipping downscale for index texture {Hash}; already within bounds.", hash);
|
||||
await TryAutoCompressAsync(hash, sourcePath, mapKind, headerInfo).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
using var resized = IndexDownscaler.Downscale(originalImage, targetSize.width, targetSize.height, BlockMultiple);
|
||||
|
||||
var canReencodeWithPenumbra = TryResolveCompressionTarget(headerInfo, sourceFormat, out var compressionTarget);
|
||||
using var resizedScratch = CreateScratchImage(resized, targetSize.width, targetSize.height);
|
||||
using var finalScratch = resizedScratch.Convert(DXGIFormat.B8G8R8A8UNorm);
|
||||
if (!TryConvertForSave(resizedScratch, sourceFormat, out var finalScratch, canReencodeWithPenumbra))
|
||||
{
|
||||
if (canReencodeWithPenumbra
|
||||
&& await TryReencodeWithPenumbraAsync(hash, sourcePath, destination, resizedScratch, compressionTarget).ConfigureAwait(false))
|
||||
{
|
||||
await TryAutoCompressAsync(hash, destination, mapKind, null).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
TexFileHelper.Save(destination, finalScratch);
|
||||
RegisterDownscaledTexture(hash, sourcePath, destination);
|
||||
_downscaledPaths[hash] = sourcePath;
|
||||
_logger.LogTrace(
|
||||
"Skipping downscale for index texture {Hash}; failed to re-encode to {Format}.",
|
||||
hash,
|
||||
sourceFormat);
|
||||
await TryAutoCompressAsync(hash, sourcePath, mapKind, headerInfo).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
using (finalScratch)
|
||||
{
|
||||
TexFileHelper.Save(destination, finalScratch);
|
||||
RegisterDownscaledTexture(hash, sourcePath, destination);
|
||||
}
|
||||
|
||||
await TryAutoCompressAsync(hash, destination, mapKind, null).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -277,7 +322,6 @@ public sealed class TextureDownscaleService
|
||||
finally
|
||||
{
|
||||
_downscaleSemaphore.Release();
|
||||
_activeJobs.TryRemove(hash, out _);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -330,6 +374,157 @@ public sealed class TextureDownscaleService
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryConvertForSave(
|
||||
ScratchImage source,
|
||||
DXGIFormat sourceFormat,
|
||||
out ScratchImage result,
|
||||
bool attemptPenumbraFallback)
|
||||
{
|
||||
var isCompressed = sourceFormat.IsCompressed();
|
||||
var targetFormat = isCompressed ? sourceFormat : DXGIFormat.B8G8R8A8UNorm;
|
||||
try
|
||||
{
|
||||
result = source.Convert(targetFormat);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var compressedFallback = attemptPenumbraFallback
|
||||
? " Attempting Penumbra re-encode."
|
||||
: " Skipping downscale.";
|
||||
_logger.LogWarning(
|
||||
ex,
|
||||
"Failed to convert downscaled texture to {Format}.{Fallback}",
|
||||
targetFormat,
|
||||
isCompressed ? compressedFallback : " Falling back to B8G8R8A8.");
|
||||
if (isCompressed)
|
||||
{
|
||||
result = default!;
|
||||
return false;
|
||||
}
|
||||
|
||||
result = source.Convert(DXGIFormat.B8G8R8A8UNorm);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryResolveCompressionTarget(TexHeaderInfo? headerInfo, DXGIFormat sourceFormat, out TextureCompressionTarget target)
|
||||
{
|
||||
if (headerInfo is { } info && TryGetCompressionTarget(info.Format, out target))
|
||||
{
|
||||
return _textureCompressionService.IsTargetSelectable(target);
|
||||
}
|
||||
|
||||
if (sourceFormat.IsCompressed() && BlockCompressedFormatMap.TryGetValue((int)sourceFormat, out target))
|
||||
{
|
||||
return _textureCompressionService.IsTargetSelectable(target);
|
||||
}
|
||||
|
||||
target = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private async Task<bool> TryReencodeWithPenumbraAsync(
|
||||
string hash,
|
||||
string sourcePath,
|
||||
string destination,
|
||||
ScratchImage resizedScratch,
|
||||
TextureCompressionTarget target)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var uncompressed = resizedScratch.Convert(DXGIFormat.B8G8R8A8UNorm);
|
||||
TexFileHelper.Save(destination, uncompressed);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to save uncompressed downscaled texture for {Hash}. Skipping downscale.", hash);
|
||||
TryDelete(destination);
|
||||
return false;
|
||||
}
|
||||
|
||||
await _compressionSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var request = new TextureCompressionRequest(destination, Array.Empty<string>(), target);
|
||||
await _textureCompressionService
|
||||
.ConvertTexturesAsync(new[] { request }, null, CancellationToken.None, requestRedraw: false)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to re-encode downscaled texture {Hash} to {Target}. Skipping downscale.", hash, target);
|
||||
TryDelete(destination);
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_compressionSemaphore.Release();
|
||||
}
|
||||
|
||||
RegisterDownscaledTexture(hash, sourcePath, destination);
|
||||
_logger.LogDebug("Downscaled texture {Hash} -> {Path} (re-encoded via Penumbra).", hash, destination);
|
||||
return true;
|
||||
}
|
||||
|
||||
private async Task TryAutoCompressAsync(string hash, string texturePath, TextureMapKind mapKind, TexHeaderInfo? headerInfo)
|
||||
{
|
||||
var performanceConfig = _playerPerformanceConfigService.Current;
|
||||
if (!performanceConfig.EnableUncompressedTextureCompression)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(texturePath) || !File.Exists(texturePath))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var info = headerInfo ?? (TryReadTexHeader(texturePath, out var header) ? header : (TexHeaderInfo?)null);
|
||||
if (!info.HasValue)
|
||||
{
|
||||
_logger.LogTrace("Skipping auto-compress for texture {Hash}; unable to read header.", hash);
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsBlockCompressedFormat(info.Value.Format))
|
||||
{
|
||||
_logger.LogTrace("Skipping auto-compress for texture {Hash}; already block-compressed.", hash);
|
||||
return;
|
||||
}
|
||||
|
||||
var suggestion = TextureMetadataHelper.GetSuggestedTarget(info.Value.Format.ToString(), mapKind, texturePath);
|
||||
if (suggestion is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var target = _textureCompressionService.NormalizeTarget(suggestion.Value.Target);
|
||||
if (!_textureCompressionService.IsTargetSelectable(target))
|
||||
{
|
||||
_logger.LogTrace("Skipping auto-compress for texture {Hash}; target {Target} not supported.", hash, target);
|
||||
return;
|
||||
}
|
||||
|
||||
await _compressionSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
var includeMipMaps = !performanceConfig.SkipUncompressedTextureCompressionMipMaps;
|
||||
var request = new TextureCompressionRequest(texturePath, Array.Empty<string>(), target);
|
||||
await _textureCompressionService
|
||||
.ConvertTexturesAsync(new[] { request }, null, CancellationToken.None, requestRedraw: false, includeMipMaps: includeMipMaps)
|
||||
.ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Auto-compress failed for texture {Hash} ({Path})", hash, texturePath);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_compressionSemaphore.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsIndexMap(TextureMapKind kind)
|
||||
=> kind is TextureMapKind.Mask
|
||||
or TextureMapKind.Index;
|
||||
|
||||
Reference in New Issue
Block a user