From d50d9cdf0f7f78c6419bc750ce511f71df665213 Mon Sep 17 00:00:00 2001 From: rootdarkarchon Date: Sat, 19 Aug 2023 00:21:51 +0200 Subject: [PATCH] add bc7 texture conversion, add syncshell note in profile display --- MareSynchronos/Interop/IpcManager.cs | 41 ++++- MareSynchronos/MareSynchronos.csproj | 10 +- MareSynchronos/Services/CharacterAnalyzer.cs | 11 +- MareSynchronos/UI/DataAnalysisUi.cs | 150 +++++++++++++++++-- MareSynchronos/UI/PopoutProfileUi.cs | 6 +- 5 files changed, 190 insertions(+), 28 deletions(-) diff --git a/MareSynchronos/Interop/IpcManager.cs b/MareSynchronos/Interop/IpcManager.cs index 6e2b0b6..6e2d6ff 100644 --- a/MareSynchronos/Interop/IpcManager.cs +++ b/MareSynchronos/Interop/IpcManager.cs @@ -57,6 +57,7 @@ public sealed class IpcManager : DisposableMediatorSubscriberBase private readonly FuncSubscriber _penumbraGetMetaManipulations; private readonly EventSubscriber _penumbraInit; private readonly EventSubscriber _penumbraModSettingChanged; + private readonly FuncSubscriber _penumbraConvertTextureFile; private readonly EventSubscriber _penumbraObjectIsRedrawn; private readonly ActionSubscriber _penumbraRedraw; private readonly ActionSubscriber _penumbraRedrawObject; @@ -101,6 +102,7 @@ public sealed class IpcManager : DisposableMediatorSubscriberBase if (change == ModSettingChange.EnableState) Mediator.Publish(new PenumbraModSettingChangedMessage()); }); + _penumbraConvertTextureFile = Penumbra.Api.Ipc.ConvertTextureFile.Subscriber(pi); _penumbraGameObjectResourcePathResolved = Penumbra.Api.Ipc.GameObjectResourcePathResolved.Subscriber(pi, ResourceLoaded); @@ -553,6 +555,43 @@ public sealed class IpcManager : DisposableMediatorSubscriberBase }).ConfigureAwait(false); } + public async Task PenumbraConvertTextureFiles(ILogger logger, Dictionary textures, IProgress<(string, int)> progress, CancellationToken token) + { + if (!CheckPenumbraApi()) return; + + Mediator.Publish(new HaltScanMessage("TextureConversion")); + int currentTexture = 0; + foreach (var texture in textures) + { + if (token.IsCancellationRequested) break; + + progress.Report((texture.Key, ++currentTexture)); + + logger.LogInformation("Converting Texture {path} to {type}", texture.Key, TextureType.Bc7Tex); + var convertTask = _penumbraConvertTextureFile.Invoke(texture.Key, texture.Key, TextureType.Bc7Tex, true); + await convertTask.ConfigureAwait(false); + if (convertTask.IsCompletedSuccessfully && texture.Value.Any()) + { + foreach (var duplicatedTexture in texture.Value) + { + logger.LogInformation("Migrating duplicate {dup}", duplicatedTexture); + try + { + File.Copy(texture.Key, duplicatedTexture, true); + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to copy duplicate {dup}", duplicatedTexture); + } + } + } + } + Mediator.Publish(new ResumeScanMessage("TextureConversion")); + + var gameObject = await _dalamudUtil.CreateGameObjectAsync(await _dalamudUtil.GetPlayerPointerAsync()); + _penumbraRedrawObject.Invoke(gameObject!, RedrawType.Redraw); + } + protected override void Dispose(bool disposing) { base.Dispose(disposing); @@ -668,7 +707,7 @@ public sealed class IpcManager : DisposableMediatorSubscriberBase bool apiAvailable = false; try { - apiAvailable = _penumbraApiVersion.Invoke() is { Item1: 4, Item2: >= 19 } && _penumbraEnabled.Invoke(); + apiAvailable = _penumbraApiVersion.Invoke() is { Item1: 4, Item2: >= 21 } && _penumbraEnabled.Invoke(); _shownPenumbraUnavailable = _shownPenumbraUnavailable && !apiAvailable; return apiAvailable; } diff --git a/MareSynchronos/MareSynchronos.csproj b/MareSynchronos/MareSynchronos.csproj index ee89063..b026276 100644 --- a/MareSynchronos/MareSynchronos.csproj +++ b/MareSynchronos/MareSynchronos.csproj @@ -29,17 +29,17 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - + + - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/MareSynchronos/Services/CharacterAnalyzer.cs b/MareSynchronos/Services/CharacterAnalyzer.cs index ed27c3b..5088155 100644 --- a/MareSynchronos/Services/CharacterAnalyzer.cs +++ b/MareSynchronos/Services/CharacterAnalyzer.cs @@ -37,7 +37,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable _analysisCts = null; } - public async Task ComputeAnalysis(bool print = true) + public async Task ComputeAnalysis(bool print = true, bool recalculate = false) { Logger.LogDebug("=== Calculating Character Analysis ==="); @@ -46,9 +46,9 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable var cancelToken = _analysisCts.Token; var allFiles = LastAnalysis.SelectMany(v => v.Value.Select(d => d.Value)).ToList(); - if (allFiles.Exists(c => !c.IsComputed)) + if (allFiles.Exists(c => !c.IsComputed || recalculate)) { - var remaining = allFiles.Where(c => !c.IsComputed).ToList(); + var remaining = allFiles.Where(c => !c.IsComputed || recalculate).ToList(); TotalFiles = remaining.Count; CurrentFile = 1; Logger.LogDebug("=== Computing {amount} remaining files ===", remaining.Count); @@ -57,6 +57,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable foreach (var file in remaining) { + Logger.LogDebug("Computing file {file}", file.FilePaths[0]); await file.ComputeSizes(_fileCacheManager, cancelToken).ConfigureAwait(false); CurrentFile++; } @@ -96,7 +97,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable { data[fileEntry.Hash] = new FileDataEntry(fileEntry.Hash, ext, fileEntry.GamePaths.ToList(), - fileCacheEntries.Select(c => c.ResolvedFilepath).ToList(), + fileCacheEntries.Select(c => c.ResolvedFilepath).Distinct().ToList(), entry.Size > 0 ? entry.Size.Value : 0, entry.CompressedSize > 0 ? entry.CompressedSize.Value : 0); } } @@ -197,7 +198,7 @@ public sealed class CharacterAnalyzer : MediatorSubscriberBase, IDisposable { return "Unknown"; } - + } default: return string.Empty; diff --git a/MareSynchronos/UI/DataAnalysisUi.cs b/MareSynchronos/UI/DataAnalysisUi.cs index 5b54ac2..4132373 100644 --- a/MareSynchronos/UI/DataAnalysisUi.cs +++ b/MareSynchronos/UI/DataAnalysisUi.cs @@ -3,6 +3,7 @@ using Dalamud.Interface.Colors; using Dalamud.Interface.Raii; using ImGuiNET; using MareSynchronos.API.Data.Enum; +using MareSynchronos.Interop; using MareSynchronos.Services; using MareSynchronos.Services.Mediator; using MareSynchronos.Utils; @@ -14,16 +15,26 @@ namespace MareSynchronos.UI; public class DataAnalysisUi : WindowMediatorSubscriberBase { private readonly CharacterAnalyzer _characterAnalyzer; + private readonly IpcManager _ipcManager; private bool _hasUpdate = false; private Dictionary>? _cachedAnalysis; private string _selectedHash = string.Empty; private ObjectKind _selectedObjectTab; private string _selectedFileTypeTab = string.Empty; + private bool _enableBc7ConversionMode = false; + private readonly Dictionary _texturesToConvert = new(StringComparer.Ordinal); + private Task? _conversionTask; + private CancellationTokenSource _conversionCancellationTokenSource = new(); + private readonly Progress<(string, int)> _conversionProgress = new(); + private string _conversionCurrentFileName = string.Empty; + private int _conversionCurrentFileProgress = 0; + private bool _modalOpen = false; + private bool _showModal = false; - public DataAnalysisUi(ILogger logger, MareMediator mediator, CharacterAnalyzer characterAnalyzer) : base(logger, mediator, "Mare Character Data Analysis") + public DataAnalysisUi(ILogger logger, MareMediator mediator, CharacterAnalyzer characterAnalyzer, IpcManager ipcManager) : base(logger, mediator, "Mare Character Data Analysis") { _characterAnalyzer = characterAnalyzer; - + _ipcManager = ipcManager; Mediator.Subscribe(this, (_) => { _hasUpdate = true; @@ -42,16 +53,66 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase Y = 2160 } }; + + _conversionProgress.ProgressChanged += ConversionProgress_ProgressChanged; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + _conversionProgress.ProgressChanged -= ConversionProgress_ProgressChanged; + } + + private void ConversionProgress_ProgressChanged(object? sender, (string, int) e) + { + _conversionCurrentFileName = e.Item1; + _conversionCurrentFileProgress = e.Item2; } public override void OnOpen() { _hasUpdate = true; _selectedHash = string.Empty; + _enableBc7ConversionMode = false; + _texturesToConvert.Clear(); } public override void Draw() { + if (_conversionTask != null && !_conversionTask.IsCompleted) + { + _showModal = true; + if (ImGui.BeginPopupModal("BC7 Conversion in Progress")) + { + ImGui.Text("BC7 Conversion in progress: " + _conversionCurrentFileProgress + "/" + _texturesToConvert.Count); + UiSharedService.TextWrapped("Current file: " + _conversionCurrentFileName); + if (UiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel conversion")) + { + _conversionCancellationTokenSource.Cancel(); + } + UiSharedService.SetScaledWindowSize(500); + ImGui.EndPopup(); + } + else + { + _modalOpen = false; + } + } + else if (_conversionTask != null && _conversionTask.IsCompleted && _texturesToConvert.Count > 0) + { + _conversionTask = null; + _texturesToConvert.Clear(); + _showModal = false; + _modalOpen = false; + _enableBc7ConversionMode = false; + } + + if (_showModal && !_modalOpen) + { + ImGui.OpenPopup("BC7 Conversion in Progress"); + _modalOpen = true; + } + if (_hasUpdate) { _cachedAnalysis = _characterAnalyzer.LastAnalysis.DeepClone(); @@ -62,25 +123,32 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase if (_cachedAnalysis!.Count == 0) return; - if (_cachedAnalysis!.Any(c => c.Value.Any(f => !f.Value.IsComputed))) + bool isAnalyzing = _characterAnalyzer.IsAnalysisRunning; + if (isAnalyzing) { - bool isAnalyzing = _characterAnalyzer.IsAnalysisRunning; - if (isAnalyzing) + UiSharedService.ColorTextWrapped($"Analyzing {_characterAnalyzer.CurrentFile}/{_characterAnalyzer.TotalFiles}", + ImGuiColors.DalamudYellow); + if (UiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel analysis")) { - UiSharedService.ColorTextWrapped($"Analyzing {_characterAnalyzer.CurrentFile}/{_characterAnalyzer.TotalFiles}", + _characterAnalyzer.CancelAnalyze(); + } + } + else + { + if (_cachedAnalysis!.Any(c => c.Value.Any(f => !f.Value.IsComputed))) + { + UiSharedService.ColorTextWrapped("Some entries in the analysis have file size not determined yet, press the button below to analyze your current data", ImGuiColors.DalamudYellow); - if (UiSharedService.IconTextButton(FontAwesomeIcon.StopCircle, "Cancel analysis")) + if (UiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start analysis (missing entries)")) { - _characterAnalyzer.CancelAnalyze(); + _ = _characterAnalyzer.ComputeAnalysis(false); } } else { - UiSharedService.ColorTextWrapped("Some entries in the analysis have file size not determined yet, press the button below to analyze your current data", - ImGuiColors.DalamudYellow); - if (UiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start analysis")) + if (UiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start analysis (recalculate all entries)")) { - _ = _characterAnalyzer.ComputeAnalysis(false); + _ = _characterAnalyzer.ComputeAnalysis(false, true); } } } @@ -154,6 +222,8 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase _selectedHash = string.Empty; _selectedObjectTab = kvp.Key; _selectedFileTypeTab = string.Empty; + _enableBc7ConversionMode = false; + _texturesToConvert.Clear(); } using var fileTabBar = ImRaii.TabBar("fileTabs"); @@ -168,7 +238,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase fileGroupText += " (!)"; } ImRaii.IEndObject fileTab; - using (var textcol = ImRaii.PushColor(ImGuiCol.Text, UiSharedService.Color(new(0, 0, 0, 1)), + using (var textcol = ImRaii.PushColor(ImGuiCol.Text, UiSharedService.Color(new(0, 0, 0, 1)), requiresCompute && !string.Equals(_selectedFileTypeTab, fileGroup.Key, StringComparison.Ordinal))) { fileTab = ImRaii.TabItem(fileGroupText + "###" + fileGroup.Key); @@ -180,6 +250,8 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase { _selectedFileTypeTab = fileGroup.Key; _selectedHash = string.Empty; + _enableBc7ConversionMode = false; + _texturesToConvert.Clear(); } ImGui.TextUnformatted($"{fileGroup.Key} files"); @@ -194,6 +266,28 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase ImGui.SameLine(); ImGui.TextUnformatted(UiSharedService.ByteToString(fileGroup.Sum(c => c.CompressedSize))); + if (_selectedFileTypeTab == "tex") + { + ImGui.Checkbox("Enable BC7 Conversion Mode", ref _enableBc7ConversionMode); + if (_enableBc7ConversionMode) + { + UiSharedService.ColorText("WARNING BC7 CONVERSION:", ImGuiColors.DalamudYellow); + ImGui.SameLine(); + UiSharedService.ColorText("Converting textures to BC7 is irreversible!", ImGuiColors.DalamudRed); + UiSharedService.ColorTextWrapped("- Converting textures to BC7 will reduce their size (compressed and uncompressed) drastically. It is recommended to be used for large (4k+) textures." + + Environment.NewLine + "- Some textures, especially ones utilizing colorsets, might not be suited for BC7 conversion and might produce visual artifacts." + + Environment.NewLine + "- Before converting textures, make sure to have the original files of the mod you are converting so you can reimport it in case of issues." + + Environment.NewLine + "- Conversion will convert all found texture duplicates (entries with more than 1 file path) automatically." + + Environment.NewLine + "- Converting textures to BC7 is a very expensive operation and, depending on the amount of textures to convert, will take a while to complete." + , ImGuiColors.DalamudYellow); + if (_texturesToConvert.Count > 0 && UiSharedService.IconTextButton(FontAwesomeIcon.PlayCircle, "Start conversion of " + _texturesToConvert.Count + " texture(s)")) + { + _conversionCancellationTokenSource = _conversionCancellationTokenSource.CancelRecreate(); + _conversionTask = _ipcManager.PenumbraConvertTextureFiles(_logger, _texturesToConvert, _conversionProgress, _conversionCancellationTokenSource.Token); + } + } + } + ImGui.Separator(); DrawTable(fileGroup); @@ -240,7 +334,7 @@ public class DataAnalysisUi : WindowMediatorSubscriberBase private void DrawTable(IGrouping fileGroup) { - using var table = ImRaii.Table("Analysis", fileGroup.Key == "tex" ? 6 : 5, ImGuiTableFlags.Sortable | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit, + using var table = ImRaii.Table("Analysis", fileGroup.Key == "tex" ? (_enableBc7ConversionMode ? 7 : 6) : 5, ImGuiTableFlags.Sortable | ImGuiTableFlags.RowBg | ImGuiTableFlags.ScrollY | ImGuiTableFlags.SizingFixedFit, new Vector2(0, 300)); if (!table.Success) return; ImGui.TableSetupColumn("Hash"); @@ -248,7 +342,11 @@ new Vector2(0, 300)); ImGui.TableSetupColumn("Gamepaths"); ImGui.TableSetupColumn("Original Size"); ImGui.TableSetupColumn("Compressed Size"); - if (fileGroup.Key == "tex") ImGui.TableSetupColumn("Format"); + if (fileGroup.Key == "tex") + { + ImGui.TableSetupColumn("Format"); + if (_enableBc7ConversionMode) ImGui.TableSetupColumn("Convert to BC7"); + } ImGui.TableSetupScrollFreeze(0, 1); ImGui.TableHeadersRow(); @@ -319,6 +417,28 @@ new Vector2(0, 300)); ImGui.TableNextColumn(); ImGui.TextUnformatted(item.Format.Value); if (ImGui.IsItemClicked()) _selectedHash = item.Hash; + if (_enableBc7ConversionMode) + { + ImGui.TableNextColumn(); + if (item.Format.Value == "BC7") + { + ImGui.Text(""); + continue; + } + var filePath = item.FilePaths[0]; + bool toConvert = _texturesToConvert.ContainsKey(filePath); + if (ImGui.Checkbox("###convert" + item.Hash, ref toConvert)) + { + if (toConvert && !_texturesToConvert.ContainsKey(filePath)) + { + _texturesToConvert[filePath] = item.FilePaths.Skip(1).ToArray(); + } + else if (!toConvert && _texturesToConvert.ContainsKey(filePath)) + { + _texturesToConvert.Remove(filePath); + } + } + } } } } diff --git a/MareSynchronos/UI/PopoutProfileUi.cs b/MareSynchronos/UI/PopoutProfileUi.cs index c9f7a0c..65ce268 100644 --- a/MareSynchronos/UI/PopoutProfileUi.cs +++ b/MareSynchronos/UI/PopoutProfileUi.cs @@ -146,9 +146,11 @@ public class PopoutProfileUi : WindowMediatorSubscriberBase if (_pair.GroupPair.Any()) { ImGui.TextUnformatted("Paired through Syncshells:"); - foreach (var groupPair in _pair.GroupPair) + foreach (var groupPair in _pair.GroupPair.Select(k => k.Key)) { - ImGui.TextUnformatted("- " + groupPair.Key.GroupAliasOrGID); + var groupNote = _serverManager.GetNoteForGid(groupPair.GID); + var groupString = string.IsNullOrEmpty(groupNote) ? groupPair.GroupAliasOrGID : $"{groupNote} ({groupPair.GroupAliasOrGID})"; + ImGui.TextUnformatted("- " + groupString); } }