Compare commits

...

7 Commits

12 changed files with 259 additions and 24 deletions

View File

@@ -92,7 +92,7 @@ public sealed class PenumbraTexture : PenumbraBase
{ {
token.ThrowIfCancellationRequested(); 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); var convertTask = _convertTextureFile.Invoke(job.InputFile, job.OutputFile, job.TargetType, job.IncludeMipMaps);
await convertTask.ConfigureAwait(false); await convertTask.ConfigureAwait(false);

View File

@@ -182,14 +182,12 @@ public sealed class GameObjectHandler : DisposableMediatorSubscriberBase, IHighP
if (nextAddr != IntPtr.Zero && !PtrGuard.LooksLikePtr(nextAddr)) if (nextAddr != IntPtr.Zero && !PtrGuard.LooksLikePtr(nextAddr))
{ {
Logger.LogWarning("[{this}] _getAddress returned non-pointer: 0x{addr:X}", this, (ulong)nextAddr);
nextAddr = IntPtr.Zero; nextAddr = IntPtr.Zero;
} }
if (nextAddr != IntPtr.Zero && if (nextAddr != IntPtr.Zero &&
!PtrGuard.IsReadable(nextAddr, (nuint)sizeof(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject))) !PtrGuard.IsReadable(nextAddr, (nuint)sizeof(FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject)))
{ {
Logger.LogWarning("[{this}] Address not readable: 0x{addr:X}", this, (ulong)nextAddr);
nextAddr = IntPtr.Zero; nextAddr = IntPtr.Zero;
} }

View File

@@ -125,6 +125,8 @@ public sealed class Plugin : IDalamudPlugin
services.AddSingleton<FileTransferOrchestrator>(); services.AddSingleton<FileTransferOrchestrator>();
services.AddSingleton<LightlessPlugin>(); services.AddSingleton<LightlessPlugin>();
services.AddSingleton<LightlessProfileManager>(); services.AddSingleton<LightlessProfileManager>();
services.AddSingleton<TextureProcessingQueue>();
services.AddSingleton<ModelProcessingQueue>();
services.AddSingleton<TextureCompressionService>(); services.AddSingleton<TextureCompressionService>();
services.AddSingleton<TextureDownscaleService>(); services.AddSingleton<TextureDownscaleService>();
services.AddSingleton<ModelDecimationService>(); services.AddSingleton<ModelDecimationService>();

View File

@@ -0,0 +1,93 @@
using Microsoft.Extensions.Logging;
using System.Collections.Concurrent;
namespace LightlessSync.Services;
public sealed class AssetProcessingQueue : IDisposable
{
private readonly BlockingCollection<WorkItem> _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<CancellationToken, Task> work, CancellationToken token = default)
{
if (work is null)
{
throw new ArgumentNullException(nameof(work));
}
var completion = new TaskCompletionSource<object?>(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<CancellationToken, Task> Work,
CancellationToken Token,
TaskCompletionSource<object?> Completion);
}

View File

@@ -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); decoded = new DecodedMeshData(positions, normals, tangents, tangents2, colors, boneWeights, uvChannels, positionWs, normalWs, blendWeightEncoding);
return true; return true;
} }
@@ -3413,6 +3423,44 @@ internal static class MdlDecimator
return ToUShortNormalized(normalized); 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) private static void NormalizeWeights(float[] weights)
{ {
var sum = weights.Sum(); var sum = weights.Sum();

View File

@@ -1,6 +1,7 @@
using LightlessSync.FileCache; using LightlessSync.FileCache;
using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Configurations; using LightlessSync.LightlessConfiguration.Configurations;
using LightlessSync.Services;
using LightlessSync.Utils; using LightlessSync.Utils;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Collections.Concurrent; using System.Collections.Concurrent;
@@ -19,6 +20,7 @@ public sealed class ModelDecimationService
private readonly FileCacheManager _fileCacheManager; private readonly FileCacheManager _fileCacheManager;
private readonly PlayerPerformanceConfigService _performanceConfigService; private readonly PlayerPerformanceConfigService _performanceConfigService;
private readonly XivDataStorageService _xivDataStorageService; private readonly XivDataStorageService _xivDataStorageService;
private readonly ModelProcessingQueue _processingQueue;
private readonly SemaphoreSlim _decimationSemaphore = new(MaxConcurrentJobs); private readonly SemaphoreSlim _decimationSemaphore = new(MaxConcurrentJobs);
private readonly TaskRegistry<string> _decimationDeduplicator = new(); private readonly TaskRegistry<string> _decimationDeduplicator = new();
@@ -30,13 +32,15 @@ public sealed class ModelDecimationService
LightlessConfigService configService, LightlessConfigService configService,
FileCacheManager fileCacheManager, FileCacheManager fileCacheManager,
PlayerPerformanceConfigService performanceConfigService, PlayerPerformanceConfigService performanceConfigService,
XivDataStorageService xivDataStorageService) XivDataStorageService xivDataStorageService,
ModelProcessingQueue processingQueue)
{ {
_logger = logger; _logger = logger;
_configService = configService; _configService = configService;
_fileCacheManager = fileCacheManager; _fileCacheManager = fileCacheManager;
_performanceConfigService = performanceConfigService; _performanceConfigService = performanceConfigService;
_xivDataStorageService = xivDataStorageService; _xivDataStorageService = xivDataStorageService;
_processingQueue = processingQueue;
} }
public void ScheduleDecimation(string hash, string filePath, string? gamePath = null) 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); _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 try
{ {
await DecimateInternalAsync(hash, filePath).ConfigureAwait(false); await DecimateInternalAsync(hash, filePath).ConfigureAwait(false);
@@ -69,7 +73,7 @@ public sealed class ModelDecimationService
{ {
_decimationSemaphore.Release(); _decimationSemaphore.Release();
} }
}); }, CancellationToken.None));
} }
public void ScheduleBatchDecimation(string hash, string filePath, ModelDecimationSettings settings) 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); _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 try
{ {
await DecimateInternalAsync(hash, filePath, settings, allowExisting: false, destinationOverride: filePath, registerDecimatedPath: false).ConfigureAwait(false); await DecimateInternalAsync(hash, filePath, settings, allowExisting: false, destinationOverride: filePath, registerDecimatedPath: false).ConfigureAwait(false);
@@ -105,7 +109,7 @@ public sealed class ModelDecimationService
{ {
_decimationSemaphore.Release(); _decimationSemaphore.Release();
} }
}); }, CancellationToken.None));
} }
public bool ShouldScheduleDecimation(string hash, string filePath, string? gamePath = null) public bool ShouldScheduleDecimation(string hash, string filePath, string? gamePath = null)

View File

@@ -0,0 +1,19 @@
using Microsoft.Extensions.Logging;
namespace LightlessSync.Services;
public sealed class ModelProcessingQueue : IDisposable
{
private readonly AssetProcessingQueue _queue;
public ModelProcessingQueue(ILogger<ModelProcessingQueue> logger)
{
_queue = new AssetProcessingQueue(logger, "LightlessSync.ModelProcessing");
}
public Task Enqueue(Func<CancellationToken, Task> work, CancellationToken token = default)
=> _queue.Enqueue(work, token);
public void Dispose()
=> _queue.Dispose();
}

View File

@@ -8,6 +8,7 @@ using System.Threading;
using OtterTex; using OtterTex;
using OtterImage = OtterTex.Image; using OtterImage = OtterTex.Image;
using LightlessSync.LightlessConfiguration; using LightlessSync.LightlessConfiguration;
using LightlessSync.Services;
using LightlessSync.Utils; using LightlessSync.Utils;
using LightlessSync.FileCache; using LightlessSync.FileCache;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@@ -33,6 +34,7 @@ public sealed class TextureDownscaleService
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService; private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
private readonly FileCacheManager _fileCacheManager; private readonly FileCacheManager _fileCacheManager;
private readonly TextureCompressionService _textureCompressionService; private readonly TextureCompressionService _textureCompressionService;
private readonly TextureProcessingQueue _processingQueue;
private readonly TaskRegistry<string> _downscaleDeduplicator = new(); private readonly TaskRegistry<string> _downscaleDeduplicator = new();
private readonly ConcurrentDictionary<string, string> _downscaledPaths = new(StringComparer.OrdinalIgnoreCase); private readonly ConcurrentDictionary<string, string> _downscaledPaths = new(StringComparer.OrdinalIgnoreCase);
@@ -73,13 +75,15 @@ public sealed class TextureDownscaleService
LightlessConfigService configService, LightlessConfigService configService,
PlayerPerformanceConfigService playerPerformanceConfigService, PlayerPerformanceConfigService playerPerformanceConfigService,
FileCacheManager fileCacheManager, FileCacheManager fileCacheManager,
TextureCompressionService textureCompressionService) TextureCompressionService textureCompressionService,
TextureProcessingQueue processingQueue)
{ {
_logger = logger; _logger = logger;
_configService = configService; _configService = configService;
_playerPerformanceConfigService = playerPerformanceConfigService; _playerPerformanceConfigService = playerPerformanceConfigService;
_fileCacheManager = fileCacheManager; _fileCacheManager = fileCacheManager;
_textureCompressionService = textureCompressionService; _textureCompressionService = textureCompressionService;
_processingQueue = processingQueue;
} }
public void ScheduleDownscale(string hash, string filePath, TextureMapKind mapKind) 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 (!filePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase)) return;
if (_downscaleDeduplicator.TryGetExisting(hash, out _)) return; if (_downscaleDeduplicator.TryGetExisting(hash, out _)) return;
_downscaleDeduplicator.GetOrStart(hash, async () => _downscaleDeduplicator.GetOrStart(hash, () => _processingQueue.Enqueue(async token =>
{ {
TextureMapKind mapKind; TextureMapKind mapKind;
try try
@@ -104,7 +108,7 @@ public sealed class TextureDownscaleService
} }
await DownscaleInternalAsync(hash, filePath, mapKind).ConfigureAwait(false); await DownscaleInternalAsync(hash, filePath, mapKind).ConfigureAwait(false);
}); }, CancellationToken.None));
} }
public bool ShouldScheduleDownscale(string filePath) public bool ShouldScheduleDownscale(string filePath)
@@ -382,6 +386,12 @@ public sealed class TextureDownscaleService
{ {
var isCompressed = sourceFormat.IsCompressed(); var isCompressed = sourceFormat.IsCompressed();
var targetFormat = isCompressed ? sourceFormat : DXGIFormat.B8G8R8A8UNorm; var targetFormat = isCompressed ? sourceFormat : DXGIFormat.B8G8R8A8UNorm;
_logger.LogDebug(
"Downscale convert target {TargetFormat} (source {SourceFormat}, compressed {IsCompressed}, penumbraFallback {PenumbraFallback})",
targetFormat,
sourceFormat,
isCompressed,
attemptPenumbraFallback);
try try
{ {
result = source.Convert(targetFormat); result = source.Convert(targetFormat);
@@ -433,6 +443,7 @@ public sealed class TextureDownscaleService
{ {
try try
{ {
_logger.LogDebug("Downscale Penumbra re-encode target {Target} for {Hash}.", target, hash);
using var uncompressed = resizedScratch.Convert(DXGIFormat.B8G8R8A8UNorm); using var uncompressed = resizedScratch.Convert(DXGIFormat.B8G8R8A8UNorm);
TexFileHelper.Save(destination, uncompressed); TexFileHelper.Save(destination, uncompressed);
} }

View File

@@ -0,0 +1,19 @@
using Microsoft.Extensions.Logging;
namespace LightlessSync.Services;
public sealed class TextureProcessingQueue : IDisposable
{
private readonly AssetProcessingQueue _queue;
public TextureProcessingQueue(ILogger<TextureProcessingQueue> logger)
{
_queue = new AssetProcessingQueue(logger, "LightlessSync.TextureProcessing");
}
public Task Enqueue(Func<CancellationToken, Task> work, CancellationToken token = default)
=> _queue.Enqueue(work, token);
public void Dispose()
=> _queue.Dispose();
}

View File

@@ -56,6 +56,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
private readonly ModelDecimationService _modelDecimationService; private readonly ModelDecimationService _modelDecimationService;
private readonly TextureCompressionService _textureCompressionService; private readonly TextureCompressionService _textureCompressionService;
private readonly TextureMetadataHelper _textureMetadataHelper; private readonly TextureMetadataHelper _textureMetadataHelper;
private readonly TextureProcessingQueue _processingQueue;
private readonly List<TextureRow> _textureRows = new(); private readonly List<TextureRow> _textureRows = new();
private readonly Dictionary<string, TextureCompressionTarget> _textureSelections = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, TextureCompressionTarget> _textureSelections = new(StringComparer.OrdinalIgnoreCase);
@@ -137,7 +138,8 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
LightlessConfigService configService, LightlessConfigService configService,
PlayerPerformanceConfigService playerPerformanceConfig, TransientResourceManager transientResourceManager, PlayerPerformanceConfigService playerPerformanceConfig, TransientResourceManager transientResourceManager,
TransientConfigService transientConfigService, ModelDecimationService modelDecimationService, TransientConfigService transientConfigService, ModelDecimationService modelDecimationService,
TextureCompressionService textureCompressionService, TextureMetadataHelper textureMetadataHelper) TextureCompressionService textureCompressionService, TextureMetadataHelper textureMetadataHelper,
TextureProcessingQueue processingQueue)
: base(logger, mediator, "Lightless Character Data Analysis", performanceCollectorService) : base(logger, mediator, "Lightless Character Data Analysis", performanceCollectorService)
{ {
_characterAnalyzer = characterAnalyzer; _characterAnalyzer = characterAnalyzer;
@@ -150,6 +152,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
_modelDecimationService = modelDecimationService; _modelDecimationService = modelDecimationService;
_textureCompressionService = textureCompressionService; _textureCompressionService = textureCompressionService;
_textureMetadataHelper = textureMetadataHelper; _textureMetadataHelper = textureMetadataHelper;
_processingQueue = processingQueue;
Mediator.Subscribe<CharacterDataAnalyzedMessage>(this, (_) => Mediator.Subscribe<CharacterDataAnalyzedMessage>(this, (_) =>
{ {
_hasUpdate = true; _hasUpdate = true;
@@ -3716,7 +3719,10 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
_conversionCurrentFileProgress = 0; _conversionCurrentFileProgress = 0;
_conversionFailed = false; _conversionFailed = false;
_conversionTask = RunTextureConversionAsync(requests, _conversionCancellationTokenSource.Token); var conversionToken = _conversionCancellationTokenSource.Token;
_conversionTask = _processingQueue.Enqueue(
queueToken => RunTextureConversionAsync(requests, queueToken),
conversionToken);
_showModal = true; _showModal = true;
} }

View File

@@ -5,19 +5,35 @@ namespace LightlessSync.Utils
{ {
public static partial class PtrGuard 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 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; GetSystemInfo(out var si);
var u = (ulong)p; return si.lpMinimumApplicationAddress;
if (u < _minLikelyPtr) return false;
if (u > _maxUserPtr) return false;
if ((u & _aligmentPtr) != 0) return false;
return true;
} }
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) public static bool TryReadIntPtr(nint addr, out nint value)
{ {
value = 0; value = 0;

View File

@@ -32,5 +32,24 @@ namespace LightlessSync.Utils
[DllImport("kernel32.dll")] [DllImport("kernel32.dll")]
internal static extern nint GetCurrentProcess(); 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;
}
} }
} }