skip decimation for direct pairs and make it a toggle in settings

This commit is contained in:
2026-01-03 03:19:10 +09:00
parent 979443d9bb
commit 4b13dfe8d4
7 changed files with 126 additions and 43 deletions

View File

@@ -22,10 +22,12 @@ public class PlayerPerformanceConfig : ILightlessConfiguration
public int TextureDownscaleMaxDimension { get; set; } = 2048; public int TextureDownscaleMaxDimension { get; set; } = 2048;
public bool OnlyDownscaleUncompressedTextures { get; set; } = true; public bool OnlyDownscaleUncompressedTextures { get; set; } = true;
public bool KeepOriginalTextureFiles { get; set; } = false; public bool KeepOriginalTextureFiles { get; set; } = false;
public bool SkipTextureDownscaleForPreferredPairs { get; set; } = true;
public bool EnableModelDecimation { get; set; } = false; public bool EnableModelDecimation { get; set; } = false;
public int ModelDecimationTriangleThreshold { get; set; } = 50_000; public int ModelDecimationTriangleThreshold { get; set; } = 50_000;
public double ModelDecimationTargetRatio { get; set; } = 0.8; public double ModelDecimationTargetRatio { get; set; } = 0.8;
public bool KeepOriginalModelFiles { get; set; } = true; public bool KeepOriginalModelFiles { get; set; } = true;
public bool SkipModelDecimationForPreferredPairs { get; set; } = true;
public bool ModelDecimationAllowBody { get; set; } = false; public bool ModelDecimationAllowBody { get; set; } = false;
public bool ModelDecimationAllowFaceHead { get; set; } = false; public bool ModelDecimationAllowFaceHead { get; set; } = false;
public bool ModelDecimationAllowTail { get; set; } = false; public bool ModelDecimationAllowTail { get; set; } = false;

View File

@@ -146,7 +146,8 @@ public class PlayerDataFactory
fragment.FileReplacements = fragment.FileReplacements =
new HashSet<FileReplacement>(resolvedPaths.Select(c => new FileReplacement([.. c.Value], c.Key)), FileReplacementComparer.Instance) new HashSet<FileReplacement>(resolvedPaths.Select(c => new FileReplacement([.. c.Value], c.Key)), FileReplacementComparer.Instance)
.Where(p => p.HasFileReplacement).ToHashSet(); .Where(p => p.HasFileReplacement).ToHashSet();
fragment.FileReplacements.RemoveWhere(c => c.GamePaths.Any(g => !CacheMonitor.AllowedFileExtensions.Any(e => g.EndsWith(e, StringComparison.OrdinalIgnoreCase)))); var allowedExtensions = CacheMonitor.AllowedFileExtensions;
fragment.FileReplacements.RemoveWhere(c => c.GamePaths.Any(g => !allowedExtensions.Any(e => g.EndsWith(e, StringComparison.OrdinalIgnoreCase))));
ct.ThrowIfCancellationRequested(); ct.ThrowIfCancellationRequested();
@@ -194,7 +195,9 @@ public class PlayerDataFactory
// get all remaining paths and resolve them // get all remaining paths and resolve them
var transientPaths = ManageSemiTransientData(objectKind); var transientPaths = ManageSemiTransientData(objectKind);
var resolvedTransientPaths = await GetFileReplacementsFromPaths(playerRelatedObject, transientPaths, new HashSet<string>(StringComparer.Ordinal)).ConfigureAwait(false); var resolvedTransientPaths = transientPaths.Count == 0
? new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase).AsReadOnly()
: await GetFileReplacementsFromPaths(playerRelatedObject, transientPaths, new HashSet<string>(StringComparer.Ordinal)).ConfigureAwait(false);
if (logDebug) if (logDebug)
{ {
@@ -377,7 +380,15 @@ public class PlayerDataFactory
{ {
var forwardPaths = forwardResolve.ToArray(); var forwardPaths = forwardResolve.ToArray();
var reversePaths = reverseResolve.ToArray(); var reversePaths = reverseResolve.ToArray();
Dictionary<string, List<string>> resolvedPaths = new(StringComparer.Ordinal); if (forwardPaths.Length == 0 && reversePaths.Length == 0)
{
return new Dictionary<string, string[]>(StringComparer.OrdinalIgnoreCase).AsReadOnly();
}
var forwardPathsLower = forwardPaths.Length == 0 ? Array.Empty<string>() : forwardPaths.Select(p => p.ToLowerInvariant()).ToArray();
var reversePathsLower = reversePaths.Length == 0 ? Array.Empty<string>() : reversePaths.Select(p => p.ToLowerInvariant()).ToArray();
Dictionary<string, List<string>> resolvedPaths = new(forwardPaths.Length + reversePaths.Length, StringComparer.Ordinal);
if (handler.ObjectKind != ObjectKind.Player) if (handler.ObjectKind != ObjectKind.Player)
{ {
var (objectIndex, forwardResolved, reverseResolved) = await _dalamudUtil.RunOnFrameworkThread(() => var (objectIndex, forwardResolved, reverseResolved) = await _dalamudUtil.RunOnFrameworkThread(() =>
@@ -415,24 +426,29 @@ public class PlayerDataFactory
if (resolvedPaths.TryGetValue(filePath, out var list)) if (resolvedPaths.TryGetValue(filePath, out var list))
{ {
list.Add(forwardPaths[i].ToLowerInvariant()); list.Add(forwardPathsLower[i]);
} }
else else
{ {
resolvedPaths[filePath] = [forwardPaths[i].ToLowerInvariant()]; resolvedPaths[filePath] = [forwardPathsLower[i]];
} }
} }
for (int i = 0; i < reversePaths.Length; i++) for (int i = 0; i < reversePaths.Length; i++)
{ {
var filePath = reversePaths[i].ToLowerInvariant(); var filePath = reversePathsLower[i];
var reverseResolvedLower = new string[reverseResolved[i].Length];
for (var j = 0; j < reverseResolvedLower.Length; j++)
{
reverseResolvedLower[j] = reverseResolved[i][j].ToLowerInvariant();
}
if (resolvedPaths.TryGetValue(filePath, out var list)) if (resolvedPaths.TryGetValue(filePath, out var list))
{ {
list.AddRange(reverseResolved[i].Select(c => c.ToLowerInvariant())); list.AddRange(reverseResolvedLower);
} }
else else
{ {
resolvedPaths[filePath] = new List<string>(reverseResolved[i].Select(c => c.ToLowerInvariant()).ToList()); resolvedPaths[filePath] = new List<string>(reverseResolvedLower);
} }
} }
@@ -446,24 +462,29 @@ public class PlayerDataFactory
var filePath = forward[i].ToLowerInvariant(); var filePath = forward[i].ToLowerInvariant();
if (resolvedPaths.TryGetValue(filePath, out var list)) if (resolvedPaths.TryGetValue(filePath, out var list))
{ {
list.Add(forwardPaths[i].ToLowerInvariant()); list.Add(forwardPathsLower[i]);
} }
else else
{ {
resolvedPaths[filePath] = [forwardPaths[i].ToLowerInvariant()]; resolvedPaths[filePath] = [forwardPathsLower[i]];
} }
} }
for (int i = 0; i < reversePaths.Length; i++) for (int i = 0; i < reversePaths.Length; i++)
{ {
var filePath = reversePaths[i].ToLowerInvariant(); var filePath = reversePathsLower[i];
var reverseResolvedLower = new string[reverse[i].Length];
for (var j = 0; j < reverseResolvedLower.Length; j++)
{
reverseResolvedLower[j] = reverse[i][j].ToLowerInvariant();
}
if (resolvedPaths.TryGetValue(filePath, out var list)) if (resolvedPaths.TryGetValue(filePath, out var list))
{ {
list.AddRange(reverse[i].Select(c => c.ToLowerInvariant())); list.AddRange(reverseResolvedLower);
} }
else else
{ {
resolvedPaths[filePath] = new List<string>(reverse[i].Select(c => c.ToLowerInvariant()).ToList()); resolvedPaths[filePath] = new List<string>(reverseResolvedLower);
} }
} }

View File

@@ -6,6 +6,7 @@ using LightlessSync.API.Data.Enum;
using LightlessSync.API.Data.Extensions; using LightlessSync.API.Data.Extensions;
using LightlessSync.FileCache; using LightlessSync.FileCache;
using LightlessSync.Interop.Ipc; using LightlessSync.Interop.Ipc;
using LightlessSync.LightlessConfiguration;
using LightlessSync.PlayerData.Factories; using LightlessSync.PlayerData.Factories;
using LightlessSync.PlayerData.Handlers; using LightlessSync.PlayerData.Handlers;
using LightlessSync.Services; using LightlessSync.Services;
@@ -38,6 +39,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
private readonly ActorObjectService _actorObjectService; private readonly ActorObjectService _actorObjectService;
private readonly FileDownloadManager _downloadManager; private readonly FileDownloadManager _downloadManager;
private readonly FileCacheManager _fileDbManager; private readonly FileCacheManager _fileDbManager;
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
private readonly GameObjectHandlerFactory _gameObjectHandlerFactory; private readonly GameObjectHandlerFactory _gameObjectHandlerFactory;
private readonly IpcManager _ipcManager; private readonly IpcManager _ipcManager;
private readonly IHostApplicationLifetime _lifetime; private readonly IHostApplicationLifetime _lifetime;
@@ -197,6 +199,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
ActorObjectService actorObjectService, ActorObjectService actorObjectService,
IHostApplicationLifetime lifetime, IHostApplicationLifetime lifetime,
FileCacheManager fileDbManager, FileCacheManager fileDbManager,
PlayerPerformanceConfigService playerPerformanceConfigService,
PlayerPerformanceService playerPerformanceService, PlayerPerformanceService playerPerformanceService,
PairProcessingLimiter pairProcessingLimiter, PairProcessingLimiter pairProcessingLimiter,
ServerConfigurationManager serverConfigManager, ServerConfigurationManager serverConfigManager,
@@ -217,6 +220,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
_actorObjectService = actorObjectService; _actorObjectService = actorObjectService;
_lifetime = lifetime; _lifetime = lifetime;
_fileDbManager = fileDbManager; _fileDbManager = fileDbManager;
_playerPerformanceConfigService = playerPerformanceConfigService;
_playerPerformanceService = playerPerformanceService; _playerPerformanceService = playerPerformanceService;
_pairProcessingLimiter = pairProcessingLimiter; _pairProcessingLimiter = pairProcessingLimiter;
_serverConfigManager = serverConfigManager; _serverConfigManager = serverConfigManager;
@@ -522,11 +526,31 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
return GetCurrentPairs().Any(predicate); return GetCurrentPairs().Any(predicate);
} }
private bool ShouldSkipDownscale() private bool IsPreferredDirectPair()
{ {
return GetCurrentPairs().Any(p => p.IsDirectlyPaired && p.SelfToOtherPermissions.IsSticky()); return GetCurrentPairs().Any(p => p.IsDirectlyPaired && p.SelfToOtherPermissions.IsSticky());
} }
private bool ShouldSkipDownscale()
{
if (!_playerPerformanceConfigService.Current.SkipTextureDownscaleForPreferredPairs)
{
return false;
}
return IsPreferredDirectPair();
}
private bool ShouldSkipDecimation()
{
if (!_playerPerformanceConfigService.Current.SkipModelDecimationForPreferredPairs)
{
return false;
}
return IsPreferredDirectPair();
}
private bool IsPaused() private bool IsPaused()
{ {
var pairs = GetCurrentPairs(); var pairs = GetCurrentPairs();
@@ -1843,6 +1867,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
try try
{ {
bool skipDownscaleForPair = ShouldSkipDownscale(); bool skipDownscaleForPair = ShouldSkipDownscale();
bool skipDecimationForPair = ShouldSkipDecimation();
var user = GetPrimaryUserData(); var user = GetPrimaryUserData();
Dictionary<(string GamePath, string? Hash), string> moddedPaths; Dictionary<(string GamePath, string? Hash), string> moddedPaths;
List<FileReplacementData> missingReplacements = []; List<FileReplacementData> missingReplacements = [];
@@ -1881,7 +1906,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
} }
var handlerForDownload = _charaHandler; var handlerForDownload = _charaHandler;
_pairDownloadTask = Task.Run(async () => await _downloadManager.DownloadFiles(handlerForDownload, toDownloadReplacements, downloadToken, skipDownscaleForPair).ConfigureAwait(false)); _pairDownloadTask = Task.Run(async () => await _downloadManager.DownloadFiles(handlerForDownload, toDownloadReplacements, downloadToken, skipDownscaleForPair, skipDecimationForPair).ConfigureAwait(false));
await _pairDownloadTask.ConfigureAwait(false); await _pairDownloadTask.ConfigureAwait(false);
@@ -1904,7 +1929,10 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
{ {
await _textureDownscaleService.WaitForPendingJobsAsync(downloadedTextureHashes, downloadToken).ConfigureAwait(false); await _textureDownscaleService.WaitForPendingJobsAsync(downloadedTextureHashes, downloadToken).ConfigureAwait(false);
} }
}
if (!skipDecimationForPair)
{
var downloadedModelHashes = toDownloadReplacements var downloadedModelHashes = toDownloadReplacements
.Where(static replacement => replacement.GamePaths.Any(static path => path.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase))) .Where(static replacement => replacement.GamePaths.Any(static path => path.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase)))
.Select(static replacement => replacement.Hash) .Select(static replacement => replacement.Hash)
@@ -2388,6 +2416,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
ConcurrentDictionary<(string GamePath, string? Hash), string> outputDict = new(); ConcurrentDictionary<(string GamePath, string? Hash), string> outputDict = new();
bool hasMigrationChanges = false; bool hasMigrationChanges = false;
bool skipDownscaleForPair = ShouldSkipDownscale(); bool skipDownscaleForPair = ShouldSkipDownscale();
bool skipDecimationForPair = ShouldSkipDecimation();
try try
{ {
@@ -2419,17 +2448,14 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
foreach (var gamePath in item.GamePaths) foreach (var gamePath in item.GamePaths)
{ {
var preferredPath = fileCache.ResolvedFilepath; var preferredPath = fileCache.ResolvedFilepath;
if (!skipDownscaleForPair) if (!skipDownscaleForPair && gamePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase))
{
if (gamePath.EndsWith(".tex", StringComparison.OrdinalIgnoreCase))
{ {
preferredPath = _textureDownscaleService.GetPreferredPath(item.Hash, fileCache.ResolvedFilepath); preferredPath = _textureDownscaleService.GetPreferredPath(item.Hash, fileCache.ResolvedFilepath);
} }
else if (gamePath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase)) else if (!skipDecimationForPair && gamePath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase))
{ {
preferredPath = _modelDecimationService.GetPreferredPath(item.Hash, fileCache.ResolvedFilepath); preferredPath = _modelDecimationService.GetPreferredPath(item.Hash, fileCache.ResolvedFilepath);
} }
}
outputDict[(gamePath, item.Hash)] = preferredPath; outputDict[(gamePath, item.Hash)] = preferredPath;
} }
} }

View File

@@ -1,5 +1,6 @@
using LightlessSync.FileCache; using LightlessSync.FileCache;
using LightlessSync.Interop.Ipc; using LightlessSync.Interop.Ipc;
using LightlessSync.LightlessConfiguration;
using LightlessSync.PlayerData.Factories; using LightlessSync.PlayerData.Factories;
using LightlessSync.Services; using LightlessSync.Services;
using LightlessSync.Services.ActorTracking; using LightlessSync.Services.ActorTracking;
@@ -27,6 +28,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly IHostApplicationLifetime _lifetime; private readonly IHostApplicationLifetime _lifetime;
private readonly FileCacheManager _fileCacheManager; private readonly FileCacheManager _fileCacheManager;
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
private readonly PlayerPerformanceService _playerPerformanceService; private readonly PlayerPerformanceService _playerPerformanceService;
private readonly PairProcessingLimiter _pairProcessingLimiter; private readonly PairProcessingLimiter _pairProcessingLimiter;
private readonly ServerConfigurationManager _serverConfigManager; private readonly ServerConfigurationManager _serverConfigManager;
@@ -49,6 +51,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
IFramework framework, IFramework framework,
IHostApplicationLifetime lifetime, IHostApplicationLifetime lifetime,
FileCacheManager fileCacheManager, FileCacheManager fileCacheManager,
PlayerPerformanceConfigService playerPerformanceConfigService,
PlayerPerformanceService playerPerformanceService, PlayerPerformanceService playerPerformanceService,
PairProcessingLimiter pairProcessingLimiter, PairProcessingLimiter pairProcessingLimiter,
ServerConfigurationManager serverConfigManager, ServerConfigurationManager serverConfigManager,
@@ -69,6 +72,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
_framework = framework; _framework = framework;
_lifetime = lifetime; _lifetime = lifetime;
_fileCacheManager = fileCacheManager; _fileCacheManager = fileCacheManager;
_playerPerformanceConfigService = playerPerformanceConfigService;
_playerPerformanceService = playerPerformanceService; _playerPerformanceService = playerPerformanceService;
_pairProcessingLimiter = pairProcessingLimiter; _pairProcessingLimiter = pairProcessingLimiter;
_serverConfigManager = serverConfigManager; _serverConfigManager = serverConfigManager;
@@ -98,6 +102,7 @@ internal sealed class PairHandlerAdapterFactory : IPairHandlerAdapterFactory
actorObjectService, actorObjectService,
_lifetime, _lifetime,
_fileCacheManager, _fileCacheManager,
_playerPerformanceConfigService,
_playerPerformanceService, _playerPerformanceService,
_pairProcessingLimiter, _pairProcessingLimiter,
_serverConfigManager, _serverConfigManager,

View File

@@ -129,6 +129,8 @@ public class PlayerPerformanceService
.Distinct(StringComparer.OrdinalIgnoreCase) .Distinct(StringComparer.OrdinalIgnoreCase)
.ToList(); .ToList();
var skipDecimation = config.SkipModelDecimationForPreferredPairs && pairHandler.IsDirectlyPaired && pairHandler.HasStickyPermissions;
foreach (var hash in moddedModelHashes) foreach (var hash in moddedModelHashes)
{ {
var tris = await _xivDataAnalyzer.GetTrianglesByHash(hash).ConfigureAwait(false); var tris = await _xivDataAnalyzer.GetTrianglesByHash(hash).ConfigureAwait(false);
@@ -138,7 +140,12 @@ public class PlayerPerformanceService
var fileEntry = _fileCacheManager.GetFileCacheByHash(hash); var fileEntry = _fileCacheManager.GetFileCacheByHash(hash);
if (fileEntry != null) if (fileEntry != null)
{ {
var preferredPath = _modelDecimationService.GetPreferredPath(hash, fileEntry.ResolvedFilepath); var preferredPath = fileEntry.ResolvedFilepath;
if (!skipDecimation)
{
preferredPath = _modelDecimationService.GetPreferredPath(hash, fileEntry.ResolvedFilepath);
}
if (!string.Equals(preferredPath, fileEntry.ResolvedFilepath, StringComparison.OrdinalIgnoreCase)) if (!string.Equals(preferredPath, fileEntry.ResolvedFilepath, StringComparison.OrdinalIgnoreCase))
{ {
var decimatedTris = await _xivDataAnalyzer.GetEffectiveTrianglesByHash(hash, preferredPath).ConfigureAwait(false); var decimatedTris = await _xivDataAnalyzer.GetEffectiveTrianglesByHash(hash, preferredPath).ConfigureAwait(false);
@@ -192,7 +199,9 @@ public class PlayerPerformanceService
public bool ComputeAndAutoPauseOnVRAMUsageThresholds(IPairPerformanceSubject pairHandler, CharacterData charaData, List<DownloadFileTransfer> toDownloadFiles) public bool ComputeAndAutoPauseOnVRAMUsageThresholds(IPairPerformanceSubject pairHandler, CharacterData charaData, List<DownloadFileTransfer> toDownloadFiles)
{ {
var config = _playerPerformanceConfigService.Current; var config = _playerPerformanceConfigService.Current;
bool skipDownscale = pairHandler.IsDirectlyPaired && pairHandler.HasStickyPermissions; bool skipDownscale = config.SkipTextureDownscaleForPreferredPairs
&& pairHandler.IsDirectlyPaired
&& pairHandler.HasStickyPermissions;
long vramUsage = 0; long vramUsage = 0;
long effectiveVramUsage = 0; long effectiveVramUsage = 0;

View File

@@ -3590,6 +3590,14 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.SameLine(); ImGui.SameLine();
_uiShared.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), new SeStringUtils.RichTextEntry("If disabled, saved + effective VRAM usage information will not work.", UIColors.Get("LightlessYellow"))); _uiShared.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), new SeStringUtils.RichTextEntry("If disabled, saved + effective VRAM usage information will not work.", UIColors.Get("LightlessYellow")));
var skipPreferredDownscale = textureConfig.SkipTextureDownscaleForPreferredPairs;
if (ImGui.Checkbox("Skip downscale for preferred/direct pairs", ref skipPreferredDownscale))
{
textureConfig.SkipTextureDownscaleForPreferredPairs = skipPreferredDownscale;
_playerPerformanceConfigService.Save();
}
_uiShared.DrawHelpText("When enabled, textures for direct pairs with preferred permissions are left untouched.");
if (!textureConfig.EnableNonIndexTextureMipTrim && !textureConfig.EnableIndexTextureDownscale) if (!textureConfig.EnableNonIndexTextureMipTrim && !textureConfig.EnableIndexTextureDownscale)
{ {
UiSharedService.ColorTextWrapped("Both trimming and downscale are disabled. Lightless will keep original textures regardless of size.", UIColors.Get("DimRed")); UiSharedService.ColorTextWrapped("Both trimming and downscale are disabled. Lightless will keep original textures regardless of size.", UIColors.Get("DimRed"));
@@ -3649,6 +3657,14 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.SameLine(); ImGui.SameLine();
_uiShared.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), new SeStringUtils.RichTextEntry("If disabled, saved + effective triangle usage information will not work.", UIColors.Get("LightlessYellow"))); _uiShared.DrawNoteLine("! ", UIColors.Get("LightlessYellow"), new SeStringUtils.RichTextEntry("If disabled, saved + effective triangle usage information will not work.", UIColors.Get("LightlessYellow")));
var skipPreferredDecimation = performanceConfig.SkipModelDecimationForPreferredPairs;
if (ImGui.Checkbox("Skip decimation for preferred/direct pairs", ref skipPreferredDecimation))
{
performanceConfig.SkipModelDecimationForPreferredPairs = skipPreferredDecimation;
_playerPerformanceConfigService.Save();
}
_uiShared.DrawHelpText("When enabled, models for direct pairs with preferred permissions are left untouched.");
var triangleThreshold = performanceConfig.ModelDecimationTriangleThreshold; var triangleThreshold = performanceConfig.ModelDecimationTriangleThreshold;
ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale); ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale);
if (ImGui.SliderInt("Decimate models above", ref triangleThreshold, 10_000, 100_000)) if (ImGui.SliderInt("Decimate models above", ref triangleThreshold, 10_000, 100_000))

View File

@@ -89,12 +89,12 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
CurrentOwnerToken = null; CurrentOwnerToken = null;
} }
public async Task DownloadFiles(GameObjectHandler? gameObject, List<FileReplacementData> fileReplacementDto, CancellationToken ct, bool skipDownscale = false) public async Task DownloadFiles(GameObjectHandler? gameObject, List<FileReplacementData> fileReplacementDto, CancellationToken ct, bool skipDownscale = false, bool skipDecimation = false)
{ {
Mediator.Publish(new HaltScanMessage(nameof(DownloadFiles))); Mediator.Publish(new HaltScanMessage(nameof(DownloadFiles)));
try try
{ {
await DownloadFilesInternal(gameObject, fileReplacementDto, ct, skipDownscale).ConfigureAwait(false); await DownloadFilesInternal(gameObject, fileReplacementDto, ct, skipDownscale, skipDecimation).ConfigureAwait(false);
} }
catch catch
{ {
@@ -498,7 +498,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
IReadOnlyDictionary<string, long> rawSizeLookup, IReadOnlyDictionary<string, long> rawSizeLookup,
string downloadLabel, string downloadLabel,
CancellationToken ct, CancellationToken ct,
bool skipDownscale) bool skipDownscale,
bool skipDecimation)
{ {
SetStatus(downloadStatusKey, DownloadStatus.Decompressing); SetStatus(downloadStatusKey, DownloadStatus.Decompressing);
MarkTransferredFiles(downloadStatusKey, 1); MarkTransferredFiles(downloadStatusKey, 1);
@@ -552,7 +553,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
} }
await _fileCompactor.WriteAllBytesAsync(filePath, decompressed, ct).ConfigureAwait(false); await _fileCompactor.WriteAllBytesAsync(filePath, decompressed, ct).ConfigureAwait(false);
PersistFileToStorage(fileHash, filePath, repl.GamePath, skipDownscale); PersistFileToStorage(fileHash, filePath, repl.GamePath, skipDownscale, skipDecimation);
} }
catch (EndOfStreamException) catch (EndOfStreamException)
{ {
@@ -638,7 +639,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
yield return items.GetRange(i, Math.Min(chunkSize, items.Count - i)); yield return items.GetRange(i, Math.Min(chunkSize, items.Count - i));
} }
private async Task DownloadFilesInternal(GameObjectHandler? gameObjectHandler, List<FileReplacementData> fileReplacement, CancellationToken ct, bool skipDownscale) private async Task DownloadFilesInternal(GameObjectHandler? gameObjectHandler, List<FileReplacementData> fileReplacement, CancellationToken ct, bool skipDownscale, bool skipDecimation)
{ {
var objectName = gameObjectHandler?.Name ?? "Unknown"; var objectName = gameObjectHandler?.Name ?? "Unknown";
@@ -742,13 +743,13 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
Task batchTask = batchChunks.Length == 0 Task batchTask = batchChunks.Length == 0
? Task.CompletedTask ? Task.CompletedTask
: Parallel.ForEachAsync(batchChunks, new ParallelOptions { MaxDegreeOfParallelism = workerDop, CancellationToken = ct }, : Parallel.ForEachAsync(batchChunks, new ParallelOptions { MaxDegreeOfParallelism = workerDop, CancellationToken = ct },
async (chunk, token) => await ProcessBatchChunkAsync(chunk, replacementLookup, rawSizeLookup, token, skipDownscale).ConfigureAwait(false)); async (chunk, token) => await ProcessBatchChunkAsync(chunk, replacementLookup, rawSizeLookup, token, skipDownscale, skipDecimation).ConfigureAwait(false));
// direct downloads // direct downloads
Task directTask = directDownloads.Count == 0 Task directTask = directDownloads.Count == 0
? Task.CompletedTask ? Task.CompletedTask
: Parallel.ForEachAsync(directDownloads, new ParallelOptions { MaxDegreeOfParallelism = workerDop, CancellationToken = ct }, : Parallel.ForEachAsync(directDownloads, new ParallelOptions { MaxDegreeOfParallelism = workerDop, CancellationToken = ct },
async (d, token) => await ProcessDirectAsync(d, replacementLookup, rawSizeLookup, token, skipDownscale).ConfigureAwait(false)); async (d, token) => await ProcessDirectAsync(d, replacementLookup, rawSizeLookup, token, skipDownscale, skipDecimation).ConfigureAwait(false));
await Task.WhenAll(batchTask, directTask).ConfigureAwait(false); await Task.WhenAll(batchTask, directTask).ConfigureAwait(false);
@@ -761,7 +762,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
Dictionary<string, (string Extension, string GamePath)> replacementLookup, Dictionary<string, (string Extension, string GamePath)> replacementLookup,
IReadOnlyDictionary<string, long> rawSizeLookup, IReadOnlyDictionary<string, long> rawSizeLookup,
CancellationToken ct, CancellationToken ct,
bool skipDownscale) bool skipDownscale,
bool skipDecimation)
{ {
var statusKey = chunk.StatusKey; var statusKey = chunk.StatusKey;
@@ -795,7 +797,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
return; return;
} }
await DecompressBlockFileAsync(statusKey, blockFile, replacementLookup, rawSizeLookup, fi.Name, ct, skipDownscale).ConfigureAwait(false); await DecompressBlockFileAsync(statusKey, blockFile, replacementLookup, rawSizeLookup, fi.Name, ct, skipDownscale, skipDecimation).ConfigureAwait(false);
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
@@ -817,7 +819,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
Dictionary<string, (string Extension, string GamePath)> replacementLookup, Dictionary<string, (string Extension, string GamePath)> replacementLookup,
IReadOnlyDictionary<string, long> rawSizeLookup, IReadOnlyDictionary<string, long> rawSizeLookup,
CancellationToken ct, CancellationToken ct,
bool skipDownscale) bool skipDownscale,
bool skipDecimation)
{ {
var progress = CreateInlineProgress(bytes => var progress = CreateInlineProgress(bytes =>
{ {
@@ -827,7 +830,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
if (!ShouldUseDirectDownloads() || string.IsNullOrEmpty(directDownload.DirectDownloadUrl)) if (!ShouldUseDirectDownloads() || string.IsNullOrEmpty(directDownload.DirectDownloadUrl))
{ {
await ProcessDirectAsQueuedFallbackAsync(directDownload, replacementLookup, rawSizeLookup, progress, ct, skipDownscale).ConfigureAwait(false); await ProcessDirectAsQueuedFallbackAsync(directDownload, replacementLookup, rawSizeLookup, progress, ct, skipDownscale, skipDecimation).ConfigureAwait(false);
return; return;
} }
@@ -875,7 +878,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
} }
await _fileCompactor.WriteAllBytesAsync(finalFilename, decompressedBytes, ct).ConfigureAwait(false); await _fileCompactor.WriteAllBytesAsync(finalFilename, decompressedBytes, ct).ConfigureAwait(false);
PersistFileToStorage(directDownload.Hash, finalFilename, repl.GamePath, skipDownscale); PersistFileToStorage(directDownload.Hash, finalFilename, repl.GamePath, skipDownscale, skipDecimation);
MarkTransferredFiles(directDownload.DirectDownloadUrl!, 1); MarkTransferredFiles(directDownload.DirectDownloadUrl!, 1);
SetStatus(directDownload.DirectDownloadUrl!, DownloadStatus.Completed); SetStatus(directDownload.DirectDownloadUrl!, DownloadStatus.Completed);
@@ -902,7 +905,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
try try
{ {
await ProcessDirectAsQueuedFallbackAsync(directDownload, replacementLookup, rawSizeLookup, progress, ct, skipDownscale).ConfigureAwait(false); await ProcessDirectAsQueuedFallbackAsync(directDownload, replacementLookup, rawSizeLookup, progress, ct, skipDownscale, skipDecimation).ConfigureAwait(false);
if (!expectedDirectDownloadFailure && failureCount >= 3 && !_disableDirectDownloads) if (!expectedDirectDownloadFailure && failureCount >= 3 && !_disableDirectDownloads)
{ {
@@ -932,7 +935,8 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
IReadOnlyDictionary<string, long> rawSizeLookup, IReadOnlyDictionary<string, long> rawSizeLookup,
IProgress<long> progress, IProgress<long> progress,
CancellationToken ct, CancellationToken ct,
bool skipDownscale) bool skipDownscale,
bool skipDecimation)
{ {
if (string.IsNullOrEmpty(directDownload.DirectDownloadUrl)) if (string.IsNullOrEmpty(directDownload.DirectDownloadUrl))
throw new InvalidOperationException("Direct download fallback requested without a direct download URL."); throw new InvalidOperationException("Direct download fallback requested without a direct download URL.");
@@ -957,7 +961,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
if (!File.Exists(blockFile)) if (!File.Exists(blockFile))
throw new FileNotFoundException("Block file missing after direct download fallback.", blockFile); throw new FileNotFoundException("Block file missing after direct download fallback.", blockFile);
await DecompressBlockFileAsync(statusKey, blockFile, replacementLookup, rawSizeLookup, $"fallback-{directDownload.Hash}", ct, skipDownscale) await DecompressBlockFileAsync(statusKey, blockFile, replacementLookup, rawSizeLookup, $"fallback-{directDownload.Hash}", ct, skipDownscale, skipDecimation)
.ConfigureAwait(false); .ConfigureAwait(false);
} }
finally finally
@@ -986,7 +990,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
return await response.Content.ReadFromJsonAsync<List<DownloadFileDto>>(cancellationToken: ct).ConfigureAwait(false) ?? []; return await response.Content.ReadFromJsonAsync<List<DownloadFileDto>>(cancellationToken: ct).ConfigureAwait(false) ?? [];
} }
private void PersistFileToStorage(string fileHash, string filePath, string gamePath, bool skipDownscale) private void PersistFileToStorage(string fileHash, string filePath, string gamePath, bool skipDownscale, bool skipDecimation)
{ {
var fi = new FileInfo(filePath); var fi = new FileInfo(filePath);
@@ -1014,7 +1018,7 @@ public partial class FileDownloadManager : DisposableMediatorSubscriberBase
() => _textureMetadataHelper.DetermineMapKind(gamePath, filePath)); () => _textureMetadataHelper.DetermineMapKind(gamePath, filePath));
} }
if (!skipDownscale && _modelDecimationService.ShouldScheduleDecimation(fileHash, filePath, gamePath)) if (!skipDecimation && _modelDecimationService.ShouldScheduleDecimation(fileHash, filePath, gamePath))
{ {
_modelDecimationService.ScheduleDecimation(fileHash, filePath, gamePath); _modelDecimationService.ScheduleDecimation(fileHash, filePath, gamePath);
} }