reworked mesh decimation yes
This commit is contained in:
@@ -106,7 +106,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
||||
_baseAnalysisCts.Dispose();
|
||||
}
|
||||
|
||||
public async Task UpdateFileEntriesAsync(IEnumerable<string> filePaths, CancellationToken token)
|
||||
public async Task UpdateFileEntriesAsync(IEnumerable<string> filePaths, CancellationToken token, bool force = false)
|
||||
{
|
||||
var normalized = new HashSet<string>(
|
||||
filePaths.Where(path => !string.IsNullOrWhiteSpace(path)),
|
||||
@@ -115,6 +115,8 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var updated = false;
|
||||
foreach (var objectEntries in LastAnalysis.Values)
|
||||
{
|
||||
foreach (var entry in objectEntries.Values)
|
||||
@@ -124,9 +126,26 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
||||
continue;
|
||||
}
|
||||
token.ThrowIfCancellationRequested();
|
||||
await entry.ComputeSizes(_fileCacheManager, token).ConfigureAwait(false);
|
||||
await entry.ComputeSizes(_fileCacheManager, token, force).ConfigureAwait(false);
|
||||
|
||||
if (string.Equals(entry.FileType, "mdl", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var sourcePath = entry.FilePaths.FirstOrDefault(path => !string.IsNullOrWhiteSpace(path));
|
||||
if (!string.IsNullOrWhiteSpace(sourcePath))
|
||||
{
|
||||
entry.UpdateTriangles(_xivDataAnalyzer.RefreshTrianglesForPath(entry.Hash, sourcePath));
|
||||
}
|
||||
}
|
||||
|
||||
updated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (updated)
|
||||
{
|
||||
RecalculateSummary();
|
||||
Mediator.Publish(new CharacterDataAnalyzedMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private async Task BaseAnalysis(CharacterData charaData, CancellationToken token)
|
||||
@@ -311,6 +330,10 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
||||
var original = new FileInfo(path).Length;
|
||||
|
||||
var compressedLen = await fileCacheManager.GetCompressedSizeAsync(Hash, token).ConfigureAwait(false);
|
||||
if (compressedLen <= 0 && !string.Equals(FileType, "tex", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
compressedLen = original;
|
||||
}
|
||||
|
||||
fileCacheManager.SetSizeInfo(Hash, original, compressedLen);
|
||||
FileCacheManager.ApplySizesToEntries(CacheEntries, original, compressedLen);
|
||||
@@ -326,6 +349,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable
|
||||
private Lazy<string>? _format;
|
||||
|
||||
public void RefreshFormat() => _format = CreateFormatValue();
|
||||
public void UpdateTriangles(long triangles) => Triangles = triangles;
|
||||
|
||||
private Lazy<string> CreateFormatValue()
|
||||
=> new(() =>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
using LightlessSync.FileCache;
|
||||
using LightlessSync.LightlessConfiguration;
|
||||
using LightlessSync.LightlessConfiguration.Configurations;
|
||||
using LightlessSync.Utils;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Collections.Concurrent;
|
||||
@@ -71,11 +72,50 @@ public sealed class ModelDecimationService
|
||||
});
|
||||
}
|
||||
|
||||
public void ScheduleBatchDecimation(string hash, string filePath, ModelDecimationSettings settings)
|
||||
{
|
||||
if (!ShouldScheduleBatchDecimation(hash, filePath, settings))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_decimationDeduplicator.TryGetExisting(hash, out _))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_failedHashes.TryRemove(hash, out _);
|
||||
_decimatedPaths.TryRemove(hash, out _);
|
||||
|
||||
_logger.LogInformation("Queued batch model decimation for {Hash}", hash);
|
||||
|
||||
_decimationDeduplicator.GetOrStart(hash, async () =>
|
||||
{
|
||||
await _decimationSemaphore.WaitAsync().ConfigureAwait(false);
|
||||
try
|
||||
{
|
||||
await DecimateInternalAsync(hash, filePath, settings, allowExisting: false, destinationOverride: filePath, registerDecimatedPath: false).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_failedHashes[hash] = 1;
|
||||
_logger.LogWarning(ex, "Batch model decimation failed for {Hash}", hash);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_decimationSemaphore.Release();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public bool ShouldScheduleDecimation(string hash, string filePath, string? gamePath = null)
|
||||
=> IsDecimationEnabled()
|
||||
{
|
||||
var threshold = Math.Max(0, _performanceConfigService.Current.ModelDecimationTriangleThreshold);
|
||||
return IsDecimationEnabled()
|
||||
&& filePath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase)
|
||||
&& IsDecimationAllowed(gamePath)
|
||||
&& !ShouldSkipByTriangleCache(hash);
|
||||
&& !ShouldSkipByTriangleCache(hash, threshold);
|
||||
}
|
||||
|
||||
public string GetPreferredPath(string hash, string originalPath)
|
||||
{
|
||||
@@ -131,6 +171,23 @@ public sealed class ModelDecimationService
|
||||
}
|
||||
|
||||
private Task DecimateInternalAsync(string hash, string sourcePath)
|
||||
{
|
||||
if (!TryGetDecimationSettings(out var settings))
|
||||
{
|
||||
_logger.LogInformation("Model decimation disabled or invalid settings for {Hash}", hash);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
return DecimateInternalAsync(hash, sourcePath, settings, allowExisting: true);
|
||||
}
|
||||
|
||||
private Task DecimateInternalAsync(
|
||||
string hash,
|
||||
string sourcePath,
|
||||
ModelDecimationSettings settings,
|
||||
bool allowExisting,
|
||||
string? destinationOverride = null,
|
||||
bool registerDecimatedPath = true)
|
||||
{
|
||||
if (!File.Exists(sourcePath))
|
||||
{
|
||||
@@ -139,34 +196,47 @@ public sealed class ModelDecimationService
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (!TryGetDecimationSettings(out var triangleThreshold, out var targetRatio, out var normalizeTangents))
|
||||
if (!TryNormalizeSettings(settings, out var normalized))
|
||||
{
|
||||
_logger.LogInformation("Model decimation disabled or invalid settings for {Hash}", hash);
|
||||
_logger.LogInformation("Model decimation skipped for {Hash}; invalid settings.", hash);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
_logger.LogInformation(
|
||||
"Starting model decimation for {Hash} (threshold {Threshold}, ratio {Ratio:0.##}, normalize tangents {NormalizeTangents})",
|
||||
"Starting model decimation for {Hash} (threshold {Threshold}, ratio {Ratio:0.##}, normalize tangents {NormalizeTangents}, avoid body intersection {AvoidBodyIntersection})",
|
||||
hash,
|
||||
triangleThreshold,
|
||||
targetRatio,
|
||||
normalizeTangents);
|
||||
normalized.TriangleThreshold,
|
||||
normalized.TargetRatio,
|
||||
normalized.NormalizeTangents,
|
||||
normalized.AvoidBodyIntersection);
|
||||
|
||||
var destination = Path.Combine(GetDecimatedDirectory(), $"{hash}.mdl");
|
||||
if (File.Exists(destination))
|
||||
var destination = destinationOverride ?? Path.Combine(GetDecimatedDirectory(), $"{hash}.mdl");
|
||||
var inPlace = string.Equals(destination, sourcePath, StringComparison.OrdinalIgnoreCase);
|
||||
if (!inPlace && File.Exists(destination))
|
||||
{
|
||||
RegisterDecimatedModel(hash, sourcePath, destination);
|
||||
return Task.CompletedTask;
|
||||
if (allowExisting)
|
||||
{
|
||||
if (registerDecimatedPath)
|
||||
{
|
||||
RegisterDecimatedModel(hash, sourcePath, destination);
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
TryDelete(destination);
|
||||
}
|
||||
|
||||
if (!MdlDecimator.TryDecimate(sourcePath, destination, triangleThreshold, targetRatio, normalizeTangents, _logger))
|
||||
if (!MdlDecimator.TryDecimate(sourcePath, destination, normalized, _logger))
|
||||
{
|
||||
_failedHashes[hash] = 1;
|
||||
_logger.LogInformation("Model decimation skipped for {Hash}", hash);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
RegisterDecimatedModel(hash, sourcePath, destination);
|
||||
if (registerDecimatedPath)
|
||||
{
|
||||
RegisterDecimatedModel(hash, sourcePath, destination);
|
||||
}
|
||||
_logger.LogInformation("Decimated model {Hash} -> {Path}", hash, destination);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
@@ -255,7 +325,7 @@ public sealed class ModelDecimationService
|
||||
private bool IsDecimationEnabled()
|
||||
=> _performanceConfigService.Current.EnableModelDecimation;
|
||||
|
||||
private bool ShouldSkipByTriangleCache(string hash)
|
||||
private bool ShouldSkipByTriangleCache(string hash, int triangleThreshold)
|
||||
{
|
||||
if (string.IsNullOrEmpty(hash))
|
||||
{
|
||||
@@ -267,7 +337,7 @@ public sealed class ModelDecimationService
|
||||
return false;
|
||||
}
|
||||
|
||||
var threshold = Math.Max(0, _performanceConfigService.Current.ModelDecimationTriangleThreshold);
|
||||
var threshold = Math.Max(0, triangleThreshold);
|
||||
return threshold > 0 && cachedTris < threshold;
|
||||
}
|
||||
|
||||
@@ -318,11 +388,14 @@ public sealed class ModelDecimationService
|
||||
private static string NormalizeGamePath(string path)
|
||||
=> path.Replace('\\', '/').ToLowerInvariant();
|
||||
|
||||
private bool TryGetDecimationSettings(out int triangleThreshold, out double targetRatio, out bool normalizeTangents)
|
||||
private bool TryGetDecimationSettings(out ModelDecimationSettings settings)
|
||||
{
|
||||
triangleThreshold = 15_000;
|
||||
targetRatio = 0.8;
|
||||
normalizeTangents = true;
|
||||
settings = new ModelDecimationSettings(
|
||||
ModelDecimationDefaults.TriangleThreshold,
|
||||
ModelDecimationDefaults.TargetRatio,
|
||||
ModelDecimationDefaults.NormalizeTangents,
|
||||
ModelDecimationDefaults.AvoidBodyIntersection,
|
||||
new ModelDecimationAdvancedSettings());
|
||||
|
||||
var config = _performanceConfigService.Current;
|
||||
if (!config.EnableModelDecimation)
|
||||
@@ -330,15 +403,86 @@ public sealed class ModelDecimationService
|
||||
return false;
|
||||
}
|
||||
|
||||
triangleThreshold = Math.Max(0, config.ModelDecimationTriangleThreshold);
|
||||
targetRatio = config.ModelDecimationTargetRatio;
|
||||
normalizeTangents = config.ModelDecimationNormalizeTangents;
|
||||
if (double.IsNaN(targetRatio) || double.IsInfinity(targetRatio))
|
||||
var advanced = NormalizeAdvancedSettings(config.ModelDecimationAdvanced);
|
||||
settings = new ModelDecimationSettings(
|
||||
Math.Max(0, config.ModelDecimationTriangleThreshold),
|
||||
config.ModelDecimationTargetRatio,
|
||||
config.ModelDecimationNormalizeTangents,
|
||||
config.ModelDecimationAvoidBodyIntersection,
|
||||
advanced);
|
||||
|
||||
return TryNormalizeSettings(settings, out settings);
|
||||
}
|
||||
|
||||
private static bool TryNormalizeSettings(ModelDecimationSettings settings, out ModelDecimationSettings normalized)
|
||||
{
|
||||
var ratio = settings.TargetRatio;
|
||||
if (double.IsNaN(ratio) || double.IsInfinity(ratio))
|
||||
{
|
||||
normalized = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
ratio = Math.Clamp(ratio, MinTargetRatio, MaxTargetRatio);
|
||||
var advanced = NormalizeAdvancedSettings(settings.Advanced);
|
||||
normalized = new ModelDecimationSettings(
|
||||
Math.Max(0, settings.TriangleThreshold),
|
||||
ratio,
|
||||
settings.NormalizeTangents,
|
||||
settings.AvoidBodyIntersection,
|
||||
advanced);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static ModelDecimationAdvancedSettings NormalizeAdvancedSettings(ModelDecimationAdvancedSettings? settings)
|
||||
{
|
||||
var source = settings ?? new ModelDecimationAdvancedSettings();
|
||||
return new ModelDecimationAdvancedSettings
|
||||
{
|
||||
MinComponentTriangles = Math.Clamp(source.MinComponentTriangles, 0, 1000),
|
||||
MaxCollapseEdgeLengthFactor = ClampFloat(source.MaxCollapseEdgeLengthFactor, 0.1f, 10f, ModelDecimationAdvancedSettings.DefaultMaxCollapseEdgeLengthFactor),
|
||||
NormalSimilarityThresholdDegrees = ClampFloat(source.NormalSimilarityThresholdDegrees, 0f, 180f, ModelDecimationAdvancedSettings.DefaultNormalSimilarityThresholdDegrees),
|
||||
BoneWeightSimilarityThreshold = ClampFloat(source.BoneWeightSimilarityThreshold, 0f, 1f, ModelDecimationAdvancedSettings.DefaultBoneWeightSimilarityThreshold),
|
||||
UvSimilarityThreshold = ClampFloat(source.UvSimilarityThreshold, 0f, 1f, ModelDecimationAdvancedSettings.DefaultUvSimilarityThreshold),
|
||||
UvSeamAngleCos = ClampFloat(source.UvSeamAngleCos, -1f, 1f, ModelDecimationAdvancedSettings.DefaultUvSeamAngleCos),
|
||||
BlockUvSeamVertices = source.BlockUvSeamVertices,
|
||||
AllowBoundaryCollapses = source.AllowBoundaryCollapses,
|
||||
BodyCollisionDistanceFactor = ClampFloat(source.BodyCollisionDistanceFactor, 0f, 10f, ModelDecimationAdvancedSettings.DefaultBodyCollisionDistanceFactor),
|
||||
BodyCollisionNoOpDistanceFactor = ClampFloat(source.BodyCollisionNoOpDistanceFactor, 0f, 10f, ModelDecimationAdvancedSettings.DefaultBodyCollisionNoOpDistanceFactor),
|
||||
BodyCollisionAdaptiveRelaxFactor = ClampFloat(source.BodyCollisionAdaptiveRelaxFactor, 0f, 10f, ModelDecimationAdvancedSettings.DefaultBodyCollisionAdaptiveRelaxFactor),
|
||||
BodyCollisionAdaptiveNearRatio = ClampFloat(source.BodyCollisionAdaptiveNearRatio, 0f, 1f, ModelDecimationAdvancedSettings.DefaultBodyCollisionAdaptiveNearRatio),
|
||||
BodyCollisionAdaptiveUvThreshold = ClampFloat(source.BodyCollisionAdaptiveUvThreshold, 0f, 1f, ModelDecimationAdvancedSettings.DefaultBodyCollisionAdaptiveUvThreshold),
|
||||
BodyCollisionNoOpUvSeamAngleCos = ClampFloat(source.BodyCollisionNoOpUvSeamAngleCos, -1f, 1f, ModelDecimationAdvancedSettings.DefaultBodyCollisionNoOpUvSeamAngleCos),
|
||||
BodyCollisionProtectionFactor = ClampFloat(source.BodyCollisionProtectionFactor, 0f, 10f, ModelDecimationAdvancedSettings.DefaultBodyCollisionProtectionFactor),
|
||||
BodyProxyTargetRatioMin = ClampFloat(source.BodyProxyTargetRatioMin, 0f, 1f, ModelDecimationAdvancedSettings.DefaultBodyProxyTargetRatioMin),
|
||||
BodyCollisionProxyInflate = ClampFloat(source.BodyCollisionProxyInflate, 0f, 0.1f, ModelDecimationAdvancedSettings.DefaultBodyCollisionProxyInflate),
|
||||
BodyCollisionPenetrationFactor = ClampFloat(source.BodyCollisionPenetrationFactor, 0f, 1f, ModelDecimationAdvancedSettings.DefaultBodyCollisionPenetrationFactor),
|
||||
MinBodyCollisionDistance = ClampFloat(source.MinBodyCollisionDistance, 1e-6f, 1f, ModelDecimationAdvancedSettings.DefaultMinBodyCollisionDistance),
|
||||
MinBodyCollisionCellSize = ClampFloat(source.MinBodyCollisionCellSize, 1e-6f, 1f, ModelDecimationAdvancedSettings.DefaultMinBodyCollisionCellSize),
|
||||
};
|
||||
}
|
||||
|
||||
private static float ClampFloat(float value, float min, float max, float fallback)
|
||||
{
|
||||
if (float.IsNaN(value) || float.IsInfinity(value))
|
||||
{
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return Math.Clamp(value, min, max);
|
||||
}
|
||||
|
||||
private bool ShouldScheduleBatchDecimation(string hash, string filePath, ModelDecimationSettings settings)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filePath) || !filePath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
targetRatio = Math.Clamp(targetRatio, MinTargetRatio, MaxTargetRatio);
|
||||
if (!TryNormalizeSettings(settings, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
using LightlessSync.LightlessConfiguration.Configurations;
|
||||
|
||||
namespace LightlessSync.Services.ModelDecimation;
|
||||
|
||||
public readonly record struct ModelDecimationSettings(
|
||||
int TriangleThreshold,
|
||||
double TargetRatio,
|
||||
bool NormalizeTangents,
|
||||
bool AvoidBodyIntersection,
|
||||
ModelDecimationAdvancedSettings Advanced);
|
||||
@@ -666,6 +666,20 @@ public sealed partial class XivDataAnalyzer
|
||||
return CalculateTrianglesFromPath(hash, path.ResolvedFilepath, _configService.Current.TriangleDictionary, _failedCalculatedTris);
|
||||
}
|
||||
|
||||
public long RefreshTrianglesForPath(string hash, string filePath)
|
||||
{
|
||||
if (string.IsNullOrEmpty(filePath)
|
||||
|| !filePath.EndsWith(".mdl", StringComparison.OrdinalIgnoreCase)
|
||||
|| !File.Exists(filePath))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
_failedCalculatedTris.RemoveAll(entry => entry.Equals(hash, StringComparison.Ordinal));
|
||||
_configService.Current.TriangleDictionary.TryRemove(hash, out _);
|
||||
return CalculateTrianglesFromPath(hash, filePath, _configService.Current.TriangleDictionary, _failedCalculatedTris);
|
||||
}
|
||||
|
||||
public async Task<long> GetEffectiveTrianglesByHash(string hash, string filePath)
|
||||
{
|
||||
if (_configService.Current.EffectiveTriangleDictionary.TryGetValue(hash, out var cachedTris) && cachedTris > 0)
|
||||
|
||||
Reference in New Issue
Block a user