55using ElezenTools . UI ;
66using Snowcloak . API . Data . Enum ;
77using Microsoft . Extensions . Logging ;
8+ using Penumbra . Api . Enums ;
9+ using Snowcloak . Interop . Ipc ;
810using Snowcloak . Services ;
911using Snowcloak . Services . Mediator ;
1012using Snowcloak . Utils ;
@@ -15,22 +17,30 @@ namespace Snowcloak.UI;
1517public class DataAnalysisUi : WindowMediatorSubscriberBase
1618{
1719 private readonly CharacterAnalyzer _characterAnalyzer ;
18- private readonly UiSharedService _uiSharedService ;
20+ private readonly Progress < ( string , int ) > _conversionProgress = new ( ) ;
21+ private readonly IpcManager _ipcManager ;
22+ private readonly Dictionary < string , ( TextureType TextureType , string [ ] Duplicates ) > _texturesToConvert = new ( StringComparer . Ordinal ) ;
1923 private Dictionary < ObjectKind , Dictionary < string , CharacterAnalyzer . FileDataEntry > > ? _cachedAnalysis ;
24+ private CancellationTokenSource _conversionCancellationTokenSource = new ( ) ;
25+ private string _conversionCurrentFileName = string . Empty ;
26+ private int _conversionCurrentFileProgress ;
27+ private Task ? _conversionTask ;
28+ private bool _enableTextureCompressionMode ;
2029 private bool _hasUpdate = false ;
30+ private bool _modalOpen ;
2131 private bool _sortDirty = true ;
2232 private string _selectedFileTypeTab = string . Empty ;
2333 private string _selectedHash = string . Empty ;
2434 private ObjectKind _selectedObjectTab ;
35+ private bool _showModal ;
2536
2637 public DataAnalysisUi ( ILogger < DataAnalysisUi > logger , SnowMediator mediator ,
27- CharacterAnalyzer characterAnalyzer ,
28- PerformanceCollectorService performanceCollectorService ,
29- UiSharedService uiSharedService )
38+ CharacterAnalyzer characterAnalyzer , IpcManager ipcManager ,
39+ PerformanceCollectorService performanceCollectorService )
3040 : base ( logger , mediator , "Snowcloak Character Data Analysis###SnowcloakDataAnalysisUI" , performanceCollectorService )
3141 {
3242 _characterAnalyzer = characterAnalyzer ;
33- _uiSharedService = uiSharedService ;
43+ _ipcManager = ipcManager ;
3444 WindowName = "Character Data Analysis" ;
3545 Mediator . Subscribe < CharacterDataAnalyzedMessage > ( this , ( _ ) =>
3646 {
@@ -49,10 +59,47 @@ public DataAnalysisUi(ILogger<DataAnalysisUi> logger, SnowMediator mediator,
4959 Y = 2160
5060 }
5161 } ;
62+
63+ _conversionProgress . ProgressChanged += ConversionProgress_ProgressChanged ;
5264 }
5365
5466 protected override void DrawInternal ( )
5567 {
68+ const string conversionPopupTitle = "Texture Compression in Progress" ;
69+ if ( _conversionTask != null && ! _conversionTask . IsCompleted )
70+ {
71+ _showModal = true ;
72+ if ( ImGui . BeginPopupModal ( conversionPopupTitle ) )
73+ {
74+ ImGui . TextUnformatted ( string . Format ( "Texture compression in progress: {0}/{1}" , _conversionCurrentFileProgress , _texturesToConvert . Count ) ) ;
75+ ImGui . TextWrapped ( string . Format ( "Current file: {0}" , _conversionCurrentFileName ) ) ;
76+ if ( ElezenImgui . ShowIconButton ( FontAwesomeIcon . StopCircle , "Cancel compression" ) )
77+ {
78+ _conversionCancellationTokenSource . Cancel ( ) ;
79+ }
80+ UiSharedService . SetScaledWindowSize ( 500 ) ;
81+ ImGui . EndPopup ( ) ;
82+ }
83+ else
84+ {
85+ _modalOpen = false ;
86+ }
87+ }
88+ else if ( _conversionTask != null && _conversionTask . IsCompleted && _texturesToConvert . Count > 0 )
89+ {
90+ _conversionTask = null ;
91+ _texturesToConvert . Clear ( ) ;
92+ _showModal = false ;
93+ _modalOpen = false ;
94+ _enableTextureCompressionMode = false ;
95+ }
96+
97+ if ( _showModal && ! _modalOpen )
98+ {
99+ ImGui . OpenPopup ( conversionPopupTitle ) ;
100+ _modalOpen = true ;
101+ }
102+
56103 if ( _hasUpdate )
57104 {
58105 _cachedAnalysis = _characterAnalyzer . LastAnalysis . DeepClone ( ) ;
@@ -212,6 +259,8 @@ protected override void DrawInternal()
212259 _selectedHash = string . Empty ;
213260 _selectedObjectTab = kvp . Key ;
214261 _selectedFileTypeTab = string . Empty ;
262+ _enableTextureCompressionMode = false ;
263+ _texturesToConvert . Clear ( ) ;
215264 }
216265
217266 using var fileTabBar = ImRaii . TabBar ( "fileTabs" ) ;
@@ -234,6 +283,8 @@ protected override void DrawInternal()
234283 {
235284 _selectedFileTypeTab = fileGroup . Key ;
236285 _selectedHash = string . Empty ;
286+ _enableTextureCompressionMode = false ;
287+ _texturesToConvert . Clear ( ) ;
237288 }
238289
239290 ImGui . TextUnformatted ( string . Format ( "{0} files" , fileGroup . Key ) ) ;
@@ -248,6 +299,29 @@ protected override void DrawInternal()
248299 ImGui . SameLine ( ) ;
249300 ImGui . TextUnformatted ( UiSharedService . ByteToString ( fileGroup . Sum ( c => c . CompressedSize ) ) ) ;
250301
302+ if ( string . Equals ( _selectedFileTypeTab , "tex" , StringComparison . Ordinal ) )
303+ {
304+ ImGui . Checkbox ( "Enable BC7 compression mode" , ref _enableTextureCompressionMode ) ;
305+ if ( _enableTextureCompressionMode )
306+ {
307+ ElezenImgui . ColouredText ( "WARNING REGARDING TEXTURE COMPRESSION:" , ImGuiColors . DalamudYellow ) ;
308+ ImGui . SameLine ( ) ;
309+ ElezenImgui . ColouredText ( "Converting textures is irreversible!" , ImGuiColors . DalamudRed ) ;
310+ ElezenImgui . ColouredWrappedText ( "- Compressing textures can reduce file size and VRAM use, especially for large textures."
311+ + Environment . NewLine + "- Selected textures will be converted to BC7."
312+ + Environment . NewLine + "- Some textures, especially colorsets, normal maps, greyscale maps, and detail maps, can produce visual artifacts after BC7 compression."
313+ + Environment . NewLine + "- Keep the original mod files available so you can reimport them if the result is wrong."
314+ + Environment . NewLine + "- Duplicate texture files are converted automatically."
315+ + Environment . NewLine + "- Texture compression is expensive and can take a while." ,
316+ ImGuiColors . DalamudYellow ) ;
317+ if ( _texturesToConvert . Count > 0 && ElezenImgui . ShowIconButton ( FontAwesomeIcon . PlayCircle , string . Format ( "Start compression of {0} texture(s)" , _texturesToConvert . Count ) ) )
318+ {
319+ _conversionCancellationTokenSource = _conversionCancellationTokenSource . CancelRecreate ( ) ;
320+ _conversionTask = _ipcManager . Penumbra . ConvertTextureFiles ( _logger , _texturesToConvert , _conversionProgress , _conversionCancellationTokenSource . Token ) ;
321+ }
322+ }
323+ }
324+
251325 ImGui . Separator ( ) ;
252326 DrawTable ( fileGroup ) ;
253327 ImGui . EndTabItem ( ) ;
@@ -297,6 +371,10 @@ protected override void DrawInternal()
297371 ElezenImgui . WrappedText ( traits . FormatSummary ) ;
298372 ElezenImgui . WrappedText ( string . Format ( "Channel variance (RGB): {0}/{1}/{2}" , traits . RedVariance . ToString ( "0.0" ) , traits . GreenVariance . ToString ( "0.0" ) , traits . BlueVariance . ToString ( "0.0" ) ) ) ;
299373 ElezenImgui . WrappedText ( string . Format ( "Alpha transitions: {0}" , traits . AlphaTransitionDensity . ToString ( "P1" ) ) ) ;
374+ if ( IsRiskyConversion ( item ) )
375+ {
376+ ElezenImgui . ColouredWrappedText ( "Flagged as risky for compression (colorset/dye path, high alpha transitions, or greyscale map)." , ImGuiColors . DalamudOrange ) ;
377+ }
300378 }
301379 }
302380 }
@@ -305,17 +383,35 @@ public override void OnOpen()
305383 {
306384 _hasUpdate = true ;
307385 _selectedHash = string . Empty ;
386+ _enableTextureCompressionMode = false ;
387+ _texturesToConvert . Clear ( ) ;
308388 }
309389
310390 protected override void Dispose ( bool disposing )
311391 {
312392 base . Dispose ( disposing ) ;
393+ _conversionProgress . ProgressChanged -= ConversionProgress_ProgressChanged ;
394+ try
395+ {
396+ _conversionCancellationTokenSource . Cancel ( ) ;
397+ }
398+ catch ( ObjectDisposedException ex )
399+ {
400+ _logger . LogTrace ( ex , "Texture compression cancellation token source was already disposed." ) ;
401+ }
402+ _conversionCancellationTokenSource . Dispose ( ) ;
403+ }
404+
405+ private void ConversionProgress_ProgressChanged ( object ? sender , ( string , int ) e )
406+ {
407+ _conversionCurrentFileName = e . Item1 ;
408+ _conversionCurrentFileProgress = e . Item2 ;
313409 }
314410
315411 private void DrawTable ( IGrouping < string , CharacterAnalyzer . FileDataEntry > fileGroup )
316412 {
317413 var tableColumns = string . Equals ( fileGroup . Key , "tex" , StringComparison . Ordinal )
318- ? 6
414+ ? ( _enableTextureCompressionMode ? 7 : 6 )
319415 : ( string . Equals ( fileGroup . Key , "mdl" , StringComparison . Ordinal ) ? 6 : 5 ) ;
320416 using var table = ImRaii . Table ( "Analysis" , tableColumns , ImGuiTableFlags . Sortable | ImGuiTableFlags . RowBg | ImGuiTableFlags . ScrollY | ImGuiTableFlags . SizingFixedFit ,
321417 new Vector2 ( 0 , 300 ) ) ;
@@ -328,6 +424,10 @@ private void DrawTable(IGrouping<string, CharacterAnalyzer.FileDataEntry> fileGr
328424 if ( string . Equals ( fileGroup . Key , "tex" , StringComparison . Ordinal ) )
329425 {
330426 ImGui . TableSetupColumn ( "Format" ) ;
427+ if ( _enableTextureCompressionMode )
428+ {
429+ ImGui . TableSetupColumn ( "BC7" ) ;
430+ }
331431 }
332432 if ( string . Equals ( fileGroup . Key , "mdl" , StringComparison . Ordinal ) )
333433 {
@@ -404,6 +504,43 @@ private void DrawTable(IGrouping<string, CharacterAnalyzer.FileDataEntry> fileGr
404504 ImGui . TableNextColumn ( ) ;
405505 ImGui . TextUnformatted ( item . Format . Value ) ;
406506 if ( ImGui . IsItemClicked ( ) ) _selectedHash = item . Hash ;
507+ if ( _enableTextureCompressionMode )
508+ {
509+ ImGui . TableNextColumn ( ) ;
510+ if ( IsAlreadyBlockCompressed ( item ) )
511+ {
512+ ImGui . TextUnformatted ( "" ) ;
513+ continue ;
514+ }
515+
516+ var filePath = item . FilePaths [ 0 ] ;
517+ var toConvert = _texturesToConvert . ContainsKey ( filePath ) ;
518+ if ( ImGui . Checkbox ( "###convert" + item . Hash , ref toConvert ) )
519+ {
520+ if ( toConvert )
521+ {
522+ _texturesToConvert [ filePath ] = ( TextureType . Bc7Tex , item . FilePaths . Skip ( 1 ) . ToArray ( ) ) ;
523+ }
524+ else
525+ {
526+ _texturesToConvert . Remove ( filePath ) ;
527+ }
528+ }
529+ if ( toConvert )
530+ {
531+ _texturesToConvert [ filePath ] = ( TextureType . Bc7Tex , item . FilePaths . Skip ( 1 ) . ToArray ( ) ) ;
532+ }
533+ if ( IsRiskyConversion ( item ) )
534+ {
535+ ImGui . SameLine ( ) ;
536+ ElezenImgui . ShowIcon ( FontAwesomeIcon . ExclamationTriangle , ImGuiColors . DalamudOrange ) ;
537+ ElezenImgui . AttachTooltip ( "Texture flagged as risky for BC7 compression. Proceed with caution." ) ;
538+ if ( ! toConvert )
539+ {
540+ _texturesToConvert . Remove ( filePath ) ;
541+ }
542+ }
543+ }
407544 }
408545 if ( string . Equals ( fileGroup . Key , "mdl" , StringComparison . Ordinal ) )
409546 {
@@ -413,4 +550,17 @@ private void DrawTable(IGrouping<string, CharacterAnalyzer.FileDataEntry> fileGr
413550 }
414551 }
415552 }
553+
554+ private static bool IsAlreadyBlockCompressed ( CharacterAnalyzer . FileDataEntry item )
555+ {
556+ var format = item . Format . Value ;
557+ return format . StartsWith ( "BC" , StringComparison . OrdinalIgnoreCase )
558+ || format . StartsWith ( "DXT" , StringComparison . OrdinalIgnoreCase )
559+ || format . StartsWith ( "24864" , StringComparison . Ordinal ) ;
560+ }
561+
562+ private static bool IsRiskyConversion ( CharacterAnalyzer . FileDataEntry item )
563+ {
564+ return item . IsRiskyTexture ;
565+ }
416566}
0 commit comments