diff --git a/LightlessSync/Interop/Ipc/Penumbra/PenumbraTexture.cs b/LightlessSync/Interop/Ipc/Penumbra/PenumbraTexture.cs index 453d211..30ac3a5 100644 --- a/LightlessSync/Interop/Ipc/Penumbra/PenumbraTexture.cs +++ b/LightlessSync/Interop/Ipc/Penumbra/PenumbraTexture.cs @@ -92,7 +92,7 @@ public sealed class PenumbraTexture : PenumbraBase { token.ThrowIfCancellationRequested(); - logger.LogInformation("Converting texture {Input} -> {Output} ({Target})", job.InputFile, job.OutputFile, job.TargetType); + logger.LogDebug("Converting texture {Input} -> {Output} ({Target})", job.InputFile, job.OutputFile, job.TargetType); var convertTask = _convertTextureFile.Invoke(job.InputFile, job.OutputFile, job.TargetType, job.IncludeMipMaps); await convertTask.ConfigureAwait(false); diff --git a/LightlessSync/Plugin.cs b/LightlessSync/Plugin.cs index 5affe17..fa04cf5 100644 --- a/LightlessSync/Plugin.cs +++ b/LightlessSync/Plugin.cs @@ -125,6 +125,8 @@ public sealed class Plugin : IDalamudPlugin services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/LightlessSync/Services/AssetProcessingQueue.cs b/LightlessSync/Services/AssetProcessingQueue.cs new file mode 100644 index 0000000..4e4654b --- /dev/null +++ b/LightlessSync/Services/AssetProcessingQueue.cs @@ -0,0 +1,93 @@ +using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; + +namespace LightlessSync.Services; + +public sealed class AssetProcessingQueue : IDisposable +{ + private readonly BlockingCollection _queue = new(); + private readonly Thread _worker; + private readonly ILogger _logger; + private bool _disposed; + + public AssetProcessingQueue(ILogger logger, string name) + { + _logger = logger; + _worker = new Thread(Run) + { + IsBackground = true, + Name = string.IsNullOrWhiteSpace(name) ? "LightlessSync.AssetProcessing" : name + }; + _worker.Start(); + } + + public Task Enqueue(Func work, CancellationToken token = default) + { + if (work is null) + { + throw new ArgumentNullException(nameof(work)); + } + + var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + if (token.IsCancellationRequested) + { + completion.TrySetCanceled(token); + return completion.Task; + } + + if (_queue.IsAddingCompleted || _disposed) + { + completion.TrySetException(new ObjectDisposedException(nameof(AssetProcessingQueue))); + return completion.Task; + } + + _queue.Add(new WorkItem(work, token, completion)); + return completion.Task; + } + + private void Run() + { + foreach (var item in _queue.GetConsumingEnumerable()) + { + if (item.Token.IsCancellationRequested) + { + item.Completion.TrySetCanceled(item.Token); + continue; + } + + try + { + item.Work(item.Token).GetAwaiter().GetResult(); + item.Completion.TrySetResult(null); + } + catch (OperationCanceledException ex) + { + var token = ex.CancellationToken.IsCancellationRequested ? ex.CancellationToken : item.Token; + item.Completion.TrySetCanceled(token); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Asset processing job failed."); + item.Completion.TrySetException(ex); + } + } + } + + public void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + _queue.CompleteAdding(); + _worker.Join(TimeSpan.FromSeconds(2)); + _queue.Dispose(); + } + + private readonly record struct WorkItem( + Func Work, + CancellationToken Token, + TaskCompletionSource Completion); +} diff --git a/LightlessSync/Services/ModelDecimation/MdlDecimator.cs b/LightlessSync/Services/ModelDecimation/MdlDecimator.cs index 4fcdbc0..48eb343 100644 --- a/LightlessSync/Services/ModelDecimation/MdlDecimator.cs +++ b/LightlessSync/Services/ModelDecimation/MdlDecimator.cs @@ -2104,6 +2104,16 @@ internal static class MdlDecimator } } + if (boneWeights != null + && blendWeightEncoding == BlendWeightEncoding.Default + && format.BlendWeightsElement is { } blendWeightsElement + && (MdlFile.VertexType)blendWeightsElement.Type == MdlFile.VertexType.UShort4 + && ShouldTreatWeightsAsByteNormalized(boneWeights)) + { + RescaleUShortAsByteWeights(boneWeights); + blendWeightEncoding = BlendWeightEncoding.UShortAsByte; + } + decoded = new DecodedMeshData(positions, normals, tangents, tangents2, colors, boneWeights, uvChannels, positionWs, normalWs, blendWeightEncoding); return true; } @@ -3413,6 +3423,44 @@ internal static class MdlDecimator return ToUShortNormalized(normalized); } + private static bool ShouldTreatWeightsAsByteNormalized(BoneWeight[] weights) + { + const float maxByteUnorm = byte.MaxValue / (float)ushort.MaxValue; + var maxWeight = 0f; + for (var i = 0; i < weights.Length; i++) + { + var weight = weights[i]; + maxWeight = Math.Max(maxWeight, weight.weight0); + maxWeight = Math.Max(maxWeight, weight.weight1); + maxWeight = Math.Max(maxWeight, weight.weight2); + maxWeight = Math.Max(maxWeight, weight.weight3); + if (maxWeight > maxByteUnorm) + { + return false; + } + } + + return maxWeight > 0f; + } + + private static void RescaleUShortAsByteWeights(BoneWeight[] weights) + { + var scale = ushort.MaxValue / (float)byte.MaxValue; + for (var i = 0; i < weights.Length; i++) + { + var weight = weights[i]; + weights[i] = new BoneWeight( + weight.index0, + weight.index1, + weight.index2, + weight.index3, + weight.weight0 * scale, + weight.weight1 * scale, + weight.weight2 * scale, + weight.weight3 * scale); + } + } + private static void NormalizeWeights(float[] weights) { var sum = weights.Sum(); diff --git a/LightlessSync/Services/ModelDecimation/ModelDecimationService.cs b/LightlessSync/Services/ModelDecimation/ModelDecimationService.cs index 3caa070..664eb10 100644 --- a/LightlessSync/Services/ModelDecimation/ModelDecimationService.cs +++ b/LightlessSync/Services/ModelDecimation/ModelDecimationService.cs @@ -1,6 +1,7 @@ using LightlessSync.FileCache; using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration.Configurations; +using LightlessSync.Services; using LightlessSync.Utils; using Microsoft.Extensions.Logging; using System.Collections.Concurrent; @@ -19,6 +20,7 @@ public sealed class ModelDecimationService private readonly FileCacheManager _fileCacheManager; private readonly PlayerPerformanceConfigService _performanceConfigService; private readonly XivDataStorageService _xivDataStorageService; + private readonly ModelProcessingQueue _processingQueue; private readonly SemaphoreSlim _decimationSemaphore = new(MaxConcurrentJobs); private readonly TaskRegistry _decimationDeduplicator = new(); @@ -30,13 +32,15 @@ public sealed class ModelDecimationService LightlessConfigService configService, FileCacheManager fileCacheManager, PlayerPerformanceConfigService performanceConfigService, - XivDataStorageService xivDataStorageService) + XivDataStorageService xivDataStorageService, + ModelProcessingQueue processingQueue) { _logger = logger; _configService = configService; _fileCacheManager = fileCacheManager; _performanceConfigService = performanceConfigService; _xivDataStorageService = xivDataStorageService; + _processingQueue = processingQueue; } public void ScheduleDecimation(string hash, string filePath, string? gamePath = null) @@ -53,9 +57,9 @@ public sealed class ModelDecimationService _logger.LogDebug("Queued model decimation for {Hash}", hash); - _decimationDeduplicator.GetOrStart(hash, async () => + _decimationDeduplicator.GetOrStart(hash, () => _processingQueue.Enqueue(async token => { - await _decimationSemaphore.WaitAsync().ConfigureAwait(false); + await _decimationSemaphore.WaitAsync(token).ConfigureAwait(false); try { await DecimateInternalAsync(hash, filePath).ConfigureAwait(false); @@ -69,7 +73,7 @@ public sealed class ModelDecimationService { _decimationSemaphore.Release(); } - }); + }, CancellationToken.None)); } public void ScheduleBatchDecimation(string hash, string filePath, ModelDecimationSettings settings) @@ -89,9 +93,9 @@ public sealed class ModelDecimationService _logger.LogInformation("Queued batch model decimation for {Hash}", hash); - _decimationDeduplicator.GetOrStart(hash, async () => + _decimationDeduplicator.GetOrStart(hash, () => _processingQueue.Enqueue(async token => { - await _decimationSemaphore.WaitAsync().ConfigureAwait(false); + await _decimationSemaphore.WaitAsync(token).ConfigureAwait(false); try { await DecimateInternalAsync(hash, filePath, settings, allowExisting: false, destinationOverride: filePath, registerDecimatedPath: false).ConfigureAwait(false); @@ -105,7 +109,7 @@ public sealed class ModelDecimationService { _decimationSemaphore.Release(); } - }); + }, CancellationToken.None)); } public bool ShouldScheduleDecimation(string hash, string filePath, string? gamePath = null) diff --git a/LightlessSync/Services/ModelProcessingQueue.cs b/LightlessSync/Services/ModelProcessingQueue.cs new file mode 100644 index 0000000..f2978ca --- /dev/null +++ b/LightlessSync/Services/ModelProcessingQueue.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Logging; + +namespace LightlessSync.Services; + +public sealed class ModelProcessingQueue : IDisposable +{ + private readonly AssetProcessingQueue _queue; + + public ModelProcessingQueue(ILogger logger) + { + _queue = new AssetProcessingQueue(logger, "LightlessSync.ModelProcessing"); + } + + public Task Enqueue(Func work, CancellationToken token = default) + => _queue.Enqueue(work, token); + + public void Dispose() + => _queue.Dispose(); +} diff --git a/LightlessSync/Services/TextureCompression/TextureDownscaleService.cs b/LightlessSync/Services/TextureCompression/TextureDownscaleService.cs index b5d677c..e06641a 100644 --- a/LightlessSync/Services/TextureCompression/TextureDownscaleService.cs +++ b/LightlessSync/Services/TextureCompression/TextureDownscaleService.cs @@ -8,6 +8,7 @@ using System.Threading; using OtterTex; using OtterImage = OtterTex.Image; using LightlessSync.LightlessConfiguration; +using LightlessSync.Services; using LightlessSync.Utils; using LightlessSync.FileCache; using Microsoft.Extensions.Logging; @@ -33,6 +34,7 @@ public sealed class TextureDownscaleService private readonly PlayerPerformanceConfigService _playerPerformanceConfigService; private readonly FileCacheManager _fileCacheManager; private readonly TextureCompressionService _textureCompressionService; + private readonly TextureProcessingQueue _processingQueue; private readonly TaskRegistry _downscaleDeduplicator = new(); private readonly ConcurrentDictionary _downscaledPaths = new(StringComparer.OrdinalIgnoreCase); @@ -73,13 +75,15 @@ public sealed class TextureDownscaleService LightlessConfigService configService, PlayerPerformanceConfigService playerPerformanceConfigService, FileCacheManager fileCacheManager, - TextureCompressionService textureCompressionService) + TextureCompressionService textureCompressionService, + TextureProcessingQueue processingQueue) { _logger = logger; _configService = configService; _playerPerformanceConfigService = playerPerformanceConfigService; _fileCacheManager = fileCacheManager; _textureCompressionService = textureCompressionService; + _processingQueue = processingQueue; } public void ScheduleDownscale(string hash, string filePath, TextureMapKind mapKind) @@ -90,7 +94,7 @@ public sealed class TextureDownscaleService if (!filePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase)) return; if (_downscaleDeduplicator.TryGetExisting(hash, out _)) return; - _downscaleDeduplicator.GetOrStart(hash, async () => + _downscaleDeduplicator.GetOrStart(hash, () => _processingQueue.Enqueue(async token => { TextureMapKind mapKind; try @@ -104,7 +108,7 @@ public sealed class TextureDownscaleService } await DownscaleInternalAsync(hash, filePath, mapKind).ConfigureAwait(false); - }); + }, CancellationToken.None)); } public bool ShouldScheduleDownscale(string filePath) @@ -382,6 +386,12 @@ public sealed class TextureDownscaleService { var isCompressed = sourceFormat.IsCompressed(); var targetFormat = isCompressed ? sourceFormat : DXGIFormat.B8G8R8A8UNorm; + _logger.LogDebug( + "Downscale convert target {TargetFormat} (source {SourceFormat}, compressed {IsCompressed}, penumbraFallback {PenumbraFallback})", + targetFormat, + sourceFormat, + isCompressed, + attemptPenumbraFallback); try { result = source.Convert(targetFormat); @@ -433,6 +443,7 @@ public sealed class TextureDownscaleService { try { + _logger.LogDebug("Downscale Penumbra re-encode target {Target} for {Hash}.", target, hash); using var uncompressed = resizedScratch.Convert(DXGIFormat.B8G8R8A8UNorm); TexFileHelper.Save(destination, uncompressed); } diff --git a/LightlessSync/Services/TextureProcessingQueue.cs b/LightlessSync/Services/TextureProcessingQueue.cs new file mode 100644 index 0000000..00ebd3c --- /dev/null +++ b/LightlessSync/Services/TextureProcessingQueue.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Logging; + +namespace LightlessSync.Services; + +public sealed class TextureProcessingQueue : IDisposable +{ + private readonly AssetProcessingQueue _queue; + + public TextureProcessingQueue(ILogger logger) + { + _queue = new AssetProcessingQueue(logger, "LightlessSync.TextureProcessing"); + } + + public Task Enqueue(Func work, CancellationToken token = default) + => _queue.Enqueue(work, token); + + public void Dispose() + => _queue.Dispose(); +} diff --git a/LightlessSync/UI/DataAnalysisUi.cs b/LightlessSync/UI/DataAnalysisUi.cs index 4ed7c30..1bdbae0 100644 --- a/LightlessSync/UI/DataAnalysisUi.cs +++ b/LightlessSync/UI/DataAnalysisUi.cs @@ -56,6 +56,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase private readonly ModelDecimationService _modelDecimationService; private readonly TextureCompressionService _textureCompressionService; private readonly TextureMetadataHelper _textureMetadataHelper; + private readonly TextureProcessingQueue _processingQueue; private readonly List _textureRows = new(); private readonly Dictionary _textureSelections = new(StringComparer.OrdinalIgnoreCase); @@ -137,7 +138,8 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase LightlessConfigService configService, PlayerPerformanceConfigService playerPerformanceConfig, TransientResourceManager transientResourceManager, TransientConfigService transientConfigService, ModelDecimationService modelDecimationService, - TextureCompressionService textureCompressionService, TextureMetadataHelper textureMetadataHelper) + TextureCompressionService textureCompressionService, TextureMetadataHelper textureMetadataHelper, + TextureProcessingQueue processingQueue) : base(logger, mediator, "Lightless Character Data Analysis", performanceCollectorService) { _characterAnalyzer = characterAnalyzer; @@ -150,6 +152,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase _modelDecimationService = modelDecimationService; _textureCompressionService = textureCompressionService; _textureMetadataHelper = textureMetadataHelper; + _processingQueue = processingQueue; Mediator.Subscribe(this, (_) => { _hasUpdate = true; @@ -3716,7 +3719,10 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase _conversionCurrentFileProgress = 0; _conversionFailed = false; - _conversionTask = RunTextureConversionAsync(requests, _conversionCancellationTokenSource.Token); + var conversionToken = _conversionCancellationTokenSource.Token; + _conversionTask = _processingQueue.Enqueue( + queueToken => RunTextureConversionAsync(requests, queueToken), + conversionToken); _showModal = true; } diff --git a/LightlessSync/Utils/PtrGuard.cs b/LightlessSync/Utils/PtrGuard.cs index 2b08169..430a317 100644 --- a/LightlessSync/Utils/PtrGuard.cs +++ b/LightlessSync/Utils/PtrGuard.cs @@ -5,19 +5,35 @@ namespace LightlessSync.Utils { public static partial class PtrGuard { - private const ulong _minLikelyPtr = 0x0000_0001_0000_0000UL; - private const ulong _maxUserPtr = 0x0000_7FFF_FFFF_FFFFUL; private const ulong _aligmentPtr = 0x7UL; + private static readonly nuint _minAppAddr = (nuint)GetMinAppAddr(); + private static readonly nuint _maxAppAddr = (nuint)GetMaxAppAddr(); - public static bool LooksLikePtr(nint p) + private static nint GetMinAppAddr() { - if (p == 0) return false; - var u = (ulong)p; - if (u < _minLikelyPtr) return false; - if (u > _maxUserPtr) return false; - if ((u & _aligmentPtr) != 0) return false; - return true; + GetSystemInfo(out var si); + return si.lpMinimumApplicationAddress; } + + private static nint GetMaxAppAddr() + { + GetSystemInfo(out var si); + return si.lpMaximumApplicationAddress; + } + + public static bool LooksLikePtr(nint p) + { + if (p == 0) return false; + nuint u = (nuint)p; + + if (u < _minAppAddr) return false; + if (u > _maxAppAddr) return false; + if ((u & _aligmentPtr) != 0) return false; + if ((uint)u == 0x12345679u) return false; + + return true; + } + public static bool TryReadIntPtr(nint addr, out nint value) { value = 0; diff --git a/LightlessSync/Utils/PtrGuardMemory.cs b/LightlessSync/Utils/PtrGuardMemory.cs index ff29c4f..ccb5b79 100644 --- a/LightlessSync/Utils/PtrGuardMemory.cs +++ b/LightlessSync/Utils/PtrGuardMemory.cs @@ -32,5 +32,24 @@ namespace LightlessSync.Utils [DllImport("kernel32.dll")] internal static extern nint GetCurrentProcess(); + + [DllImport("kernel32.dll")] + internal static extern void GetSystemInfo(out SYSTEM_INFO lpSystemInfo); + + [StructLayout(LayoutKind.Sequential)] + internal struct SYSTEM_INFO + { + public ushort wProcessorArchitecture; + public ushort wReserved; + public uint dwPageSize; + public nint lpMinimumApplicationAddress; + public nint lpMaximumApplicationAddress; + public nint dwActiveProcessorMask; + public uint dwNumberOfProcessors; + public uint dwProcessorType; + public uint dwAllocationGranularity; + public ushort wProcessorLevel; + public ushort wProcessorRevision; + } } } \ No newline at end of file