This commit is contained in:
2025-12-28 05:24:12 +09:00
parent 1632258c4f
commit 8f32b375dd
27 changed files with 3040 additions and 482 deletions

View File

@@ -14,6 +14,7 @@ using LightlessSync.Services.TextureCompression;
using LightlessSync.Utils;
using Microsoft.Extensions.Logging;
using OtterTex;
using System.Buffers.Binary;
using System.Globalization;
using System.Numerics;
using SixLabors.ImageSharp;
@@ -49,6 +50,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
private readonly Dictionary<string, TextureCompressionTarget> _textureSelections = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _selectedTextureKeys = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, TexturePreviewState> _texturePreviews = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, TextureResolutionInfo?> _textureResolutionCache = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<ObjectKind, TextureWorkspaceTab> _textureWorkspaceTabs = new();
private readonly List<string> _storedPathsToRemove = [];
private readonly Dictionary<string, string> _filePathResolve = [];
@@ -88,6 +90,9 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
private bool _showAlreadyAddedTransients = false;
private bool _acknowledgeReview = false;
private Task<TextureRowBuildResult>? _textureRowsBuildTask;
private CancellationTokenSource? _textureRowsBuildCts;
private ObjectKind _selectedObjectTab;
private TextureUsageCategory? _textureCategoryFilter = null;
@@ -204,9 +209,9 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
return;
}
_cachedAnalysis = _characterAnalyzer.LastAnalysis.DeepClone();
_cachedAnalysis = CloneAnalysis(_characterAnalyzer.LastAnalysis);
_hasUpdate = false;
_textureRowsDirty = true;
InvalidateTextureRows();
}
private void DrawContentTabs()
@@ -750,7 +755,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
_selectedTextureKeys.Clear();
_textureSelections.Clear();
ResetTextureFilters();
_textureRowsDirty = true;
InvalidateTextureRows();
_conversionFailed = false;
}
@@ -762,6 +767,8 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
preview.Texture?.Dispose();
}
_texturePreviews.Clear();
_textureRowsBuildCts?.Cancel();
_textureRowsBuildCts?.Dispose();
_conversionProgress.ProgressChanged -= ConversionProgress_ProgressChanged;
}
@@ -775,18 +782,108 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
private void EnsureTextureRows()
{
if (!_textureRowsDirty || _cachedAnalysis == null)
if (_cachedAnalysis == null)
{
return;
}
if (_textureRowsDirty && _textureRowsBuildTask == null)
{
_textureRowsBuildCts?.Dispose();
_textureRowsBuildCts = new();
var snapshot = _cachedAnalysis;
_textureRowsBuildTask = Task.Run(() => BuildTextureRows(snapshot, _textureRowsBuildCts.Token), _textureRowsBuildCts.Token);
}
if (_textureRowsBuildTask == null || !_textureRowsBuildTask.IsCompleted)
{
return;
}
var completedTask = _textureRowsBuildTask;
_textureRowsBuildTask = null;
_textureRowsBuildCts?.Dispose();
_textureRowsBuildCts = null;
if (completedTask.IsCanceled)
{
return;
}
if (completedTask.IsFaulted)
{
_logger.LogWarning(completedTask.Exception, "Failed to build texture rows.");
_textureRowsDirty = false;
return;
}
ApplyTextureRowBuild(completedTask.Result);
_textureRowsDirty = false;
}
private void ApplyTextureRowBuild(TextureRowBuildResult result)
{
_textureRows.Clear();
_textureRows.AddRange(result.Rows);
foreach (var row in _textureRows)
{
if (row.IsAlreadyCompressed)
{
_selectedTextureKeys.Remove(row.Key);
_textureSelections.Remove(row.Key);
}
}
_selectedTextureKeys.RemoveWhere(key => !result.ValidKeys.Contains(key));
foreach (var key in _texturePreviews.Keys.ToArray())
{
if (!result.ValidKeys.Contains(key) && _texturePreviews.TryGetValue(key, out var preview))
{
preview.Texture?.Dispose();
_texturePreviews.Remove(key);
}
}
foreach (var key in _textureResolutionCache.Keys.ToArray())
{
if (!result.ValidKeys.Contains(key))
{
_textureResolutionCache.Remove(key);
}
}
foreach (var key in _textureSelections.Keys.ToArray())
{
if (!result.ValidKeys.Contains(key))
{
_textureSelections.Remove(key);
continue;
}
_textureSelections[key] = _textureCompressionService.NormalizeTarget(_textureSelections[key]);
}
if (!string.IsNullOrEmpty(_selectedTextureKey) && !result.ValidKeys.Contains(_selectedTextureKey))
{
_selectedTextureKey = string.Empty;
}
}
private TextureRowBuildResult BuildTextureRows(
Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>> analysis,
CancellationToken token)
{
var rows = new List<TextureRow>();
HashSet<string> validKeys = new(StringComparer.OrdinalIgnoreCase);
foreach (var (objectKind, entries) in _cachedAnalysis)
foreach (var (objectKind, entries) in analysis)
{
foreach (var entry in entries.Values)
{
token.ThrowIfCancellationRequested();
if (!string.Equals(entry.FileType, "tex", StringComparison.Ordinal))
{
continue;
@@ -828,17 +925,11 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
suggestion?.Reason);
validKeys.Add(row.Key);
_textureRows.Add(row);
if (row.IsAlreadyCompressed)
{
_selectedTextureKeys.Remove(row.Key);
_textureSelections.Remove(row.Key);
}
rows.Add(row);
}
}
_textureRows.Sort((a, b) =>
rows.Sort((a, b) =>
{
var comp = a.ObjectKind.CompareTo(b.ObjectKind);
if (comp != 0)
@@ -851,34 +942,14 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
return string.Compare(a.DisplayName, b.DisplayName, StringComparison.OrdinalIgnoreCase);
});
_selectedTextureKeys.RemoveWhere(key => !validKeys.Contains(key));
return new TextureRowBuildResult(rows, validKeys);
}
foreach (var key in _texturePreviews.Keys.ToArray())
{
if (!validKeys.Contains(key) && _texturePreviews.TryGetValue(key, out var preview))
{
preview.Texture?.Dispose();
_texturePreviews.Remove(key);
}
}
foreach (var key in _textureSelections.Keys.ToArray())
{
if (!validKeys.Contains(key))
{
_textureSelections.Remove(key);
continue;
}
_textureSelections[key] = _textureCompressionService.NormalizeTarget(_textureSelections[key]);
}
if (!string.IsNullOrEmpty(_selectedTextureKey) && !validKeys.Contains(_selectedTextureKey))
{
_selectedTextureKey = string.Empty;
}
_textureRowsDirty = false;
private void InvalidateTextureRows()
{
_textureRowsDirty = true;
_textureRowsBuildCts?.Cancel();
_textureResolutionCache.Clear();
}
private static string MakeTextureKey(ObjectKind objectKind, string primaryFilePath) =>
@@ -893,6 +964,30 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
_textureSearch = string.Empty;
}
private static Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>> CloneAnalysis(
Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>> source)
{
var clone = new Dictionary<ObjectKind, Dictionary<string, CharacterAnalyzer.FileDataEntry>>(source.Count);
foreach (var (objectKind, entries) in source)
{
var entryClone = new Dictionary<string, CharacterAnalyzer.FileDataEntry>(entries.Count, entries.Comparer);
foreach (var (hash, entry) in entries)
{
entryClone[hash] = new CharacterAnalyzer.FileDataEntry(
hash,
entry.FileType,
entry.GamePaths.ToList(),
entry.FilePaths.ToList(),
entry.OriginalSize,
entry.CompressedSize,
entry.Triangles);
}
clone[objectKind] = entryClone;
}
return clone;
}
private void DrawAnalysisOverview(int totalFiles, long totalActualSize, long totalCompressedSize, long totalTriangles, string breakdownTooltip)
{
var scale = ImGuiHelpers.GlobalScale;
@@ -1091,6 +1186,10 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
public bool IsAlreadyCompressed => CurrentTarget.HasValue;
}
private sealed record TextureRowBuildResult(
List<TextureRow> Rows,
HashSet<string> ValidKeys);
private sealed class TexturePreviewState
{
public Task? LoadTask { get; set; }
@@ -1099,6 +1198,22 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
public DateTime LastAccessUtc { get; set; } = DateTime.UtcNow;
}
private readonly struct TextureResolutionInfo
{
public TextureResolutionInfo(ushort width, ushort height, ushort depth, ushort mipLevels)
{
Width = width;
Height = height;
Depth = depth;
MipLevels = mipLevels;
}
public ushort Width { get; }
public ushort Height { get; }
public ushort Depth { get; }
public ushort MipLevels { get; }
}
private void DrawTextureWorkspace(ObjectKind objectKind, IReadOnlyList<IGrouping<string, CharacterAnalyzer.FileDataEntry>> otherFileGroups)
{
if (!_textureWorkspaceTabs.ContainsKey(objectKind))
@@ -1143,6 +1258,11 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
private void DrawTextureTabContent(ObjectKind objectKind)
{
var scale = ImGuiHelpers.GlobalScale;
if (_textureRowsBuildTask != null && !_textureRowsBuildTask.IsCompleted && _textureRows.Count == 0)
{
UiSharedService.ColorText("Building texture list.", ImGuiColors.DalamudGrey);
return;
}
var objectRows = _textureRows.Where(row => row.ObjectKind == objectKind).ToList();
var hasAnyTextureRows = objectRows.Count > 0;
var availableCategories = objectRows.Select(row => row.Category)
@@ -1404,6 +1524,24 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
{
ResetTextureFilters();
}
ImGuiHelpers.ScaledDummy(6);
ImGui.Separator();
ImGuiHelpers.ScaledDummy(4);
UiSharedService.ColorText("Texture row colors", UIColors.Get("LightlessPurple"));
DrawTextureRowLegendItem("Selected", UIColors.Get("LightlessYellow"), "This row is selected in the texture table.");
DrawTextureRowLegendItem("Already compressed", UIColors.Get("LightlessGreenDefault"), "Texture is already stored in a compressed format.");
DrawTextureRowLegendItem("Missing analysis data", UIColors.Get("DimRed"), "File size data has not been computed yet.");
}
private static void DrawTextureRowLegendItem(string label, Vector4 color, string description)
{
var scale = ImGuiHelpers.GlobalScale;
var swatchSize = new Vector2(12f * scale, 12f * scale);
ImGui.ColorButton($"##textureRowLegend{label}", color, ImGuiColorEditFlags.NoTooltip | ImGuiColorEditFlags.NoDragDrop, swatchSize);
ImGui.SameLine(0f, 6f * scale);
var wrapPos = ImGui.GetCursorPosX() + ImGui.GetContentRegionAvail().X;
UiSharedService.TextWrapped($"{label}: {description}", wrapPos);
}
private static void DrawEnumFilterCombo<T>(
@@ -1810,7 +1948,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
ImGui.SameLine();
if (_uiSharedService.IconTextButton(FontAwesomeIcon.Sync, "Refresh", 130f * scale))
{
_textureRowsDirty = true;
InvalidateTextureRows();
}
TextureRow? lastSelected = null;
@@ -1976,7 +2114,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
{
_selectedTextureKeys.Clear();
_textureSelections.Clear();
_textureRowsDirty = true;
InvalidateTextureRows();
}
}
@@ -2197,6 +2335,68 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
}
}
private TextureResolutionInfo? GetTextureResolution(TextureRow row)
{
if (_textureResolutionCache.TryGetValue(row.Key, out var cached))
{
return cached;
}
var info = TryReadTextureResolution(row.PrimaryFilePath, out var resolved)
? resolved
: (TextureResolutionInfo?)null;
_textureResolutionCache[row.Key] = info;
return info;
}
private static bool TryReadTextureResolution(string path, out TextureResolutionInfo info)
{
info = default;
try
{
Span<byte> buffer = stackalloc byte[16];
using var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
var read = stream.Read(buffer);
if (read < buffer.Length)
{
return false;
}
var width = BinaryPrimitives.ReadUInt16LittleEndian(buffer[8..10]);
var height = BinaryPrimitives.ReadUInt16LittleEndian(buffer[10..12]);
var depth = BinaryPrimitives.ReadUInt16LittleEndian(buffer[12..14]);
var mipLevels = BinaryPrimitives.ReadUInt16LittleEndian(buffer[14..16]);
if (width == 0 || height == 0)
{
return false;
}
if (depth == 0)
{
depth = 1;
}
if (mipLevels == 0)
{
mipLevels = 1;
}
info = new TextureResolutionInfo(width, height, depth, mipLevels);
return true;
}
catch
{
return false;
}
}
private static string FormatTextureResolution(TextureResolutionInfo info)
=> info.Depth > 1
? $"{info.Width} x {info.Height} x {info.Depth}"
: $"{info.Width} x {info.Height}";
private void DrawTextureRow(TextureRow row, IReadOnlyList<TextureCompressionTarget> targets, int index)
{
var key = row.Key;
@@ -2465,6 +2665,9 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase
MetaRow(FontAwesomeIcon.LayerGroup, "Map Type", row.MapKind.ToString());
MetaRow(FontAwesomeIcon.Fingerprint, "Hash", row.Hash, UIColors.Get("LightlessBlue"));
MetaRow(FontAwesomeIcon.InfoCircle, "Current Format", row.Format);
var resolution = GetTextureResolution(row);
var resolutionLabel = resolution.HasValue ? FormatTextureResolution(resolution.Value) : "Unknown";
MetaRow(FontAwesomeIcon.Images, "Resolution", resolutionLabel);
var selectedLabel = hasSelectedInfo ? selectedInfo!.Title : selectedTarget.ToString();
var selectionColor = hasSelectedInfo ? UIColors.Get("LightlessYellow") : UIColors.Get("LightlessGreen");