sigma update

This commit is contained in:
2026-01-16 11:00:58 +09:00
parent 59ed03a825
commit 96123d00a2
51 changed files with 6640 additions and 1382 deletions

View File

@@ -25,6 +25,7 @@ using LightlessSync.Services.LightFinder;
using LightlessSync.Services.Mediator;
using LightlessSync.Services.PairProcessing;
using LightlessSync.Services.ServerConfiguration;
using LightlessSync.UI.Components;
using LightlessSync.UI.Models;
using LightlessSync.UI.Services;
using LightlessSync.UI.Style;
@@ -66,6 +67,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
private readonly PairUiService _pairUiService;
private readonly PerformanceCollectorService _performanceCollector;
private readonly PlayerPerformanceConfigService _playerPerformanceConfigService;
private readonly OptimizationSettingsPanel _optimizationSettingsPanel;
private readonly PairProcessingLimiter _pairProcessingLimiter;
private readonly EventAggregator _eventAggregator;
private readonly ServerConfigurationManager _serverConfigurationManager;
@@ -133,6 +135,12 @@ public class SettingsUi : WindowMediatorSubscriberBase
private readonly Dictionary<string, double> _generalTreeHighlights = new(StringComparer.Ordinal);
private const float GeneralTreeHighlightDuration = 1.5f;
private readonly SeluneBrush _generalSeluneBrush = new();
private string? _performanceScrollTarget = null;
private string? _performanceOpenTreeTarget = null;
private const string PerformanceWarningsLabel = "Warnings";
private const string PerformanceAutoPauseLabel = "Auto Pause";
private const string PerformanceTextureOptimizationLabel = "Texture Optimization";
private const string PerformanceModelOptimizationLabel = "Model Optimization";
private static readonly (string Label, SeIconChar Icon)[] LightfinderIconPresets = new[]
{
@@ -208,6 +216,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
_httpClient = httpClient;
_fileCompactor = fileCompactor;
_uiShared = uiShared;
_optimizationSettingsPanel = new OptimizationSettingsPanel(_uiShared, _playerPerformanceConfigService, _pairUiService);
_nameplateService = nameplateService;
_actorObjectService = actorObjectService;
_validationProgress = new Progress<(int, int, FileCacheEntity)>(v => _currentProgress = v);
@@ -229,6 +238,11 @@ public class SettingsUi : WindowMediatorSubscriberBase
_selectGeneralTabOnNextDraw = true;
FocusGeneralTree("Lightfinder");
});
Mediator.Subscribe<OpenPerformanceSettingsMessage>(this, msg =>
{
IsOpen = true;
FocusPerformanceSection(msg.Section);
});
Mediator.Subscribe<SwitchToIntroUiMessage>(this, (_) => IsOpen = false);
Mediator.Subscribe<CutsceneStartMessage>(this, (_) => UiSharedService_GposeStart());
Mediator.Subscribe<CutsceneEndMessage>(this, (_) => UiSharedService_GposeEnd());
@@ -516,162 +530,6 @@ public class SettingsUi : WindowMediatorSubscriberBase
}
}
private void DrawTextureDownscaleCounters()
{
HashSet<Pair> trackedPairs = new();
var snapshot = _pairUiService.GetSnapshot();
foreach (var pair in snapshot.DirectPairs)
{
trackedPairs.Add(pair);
}
foreach (var group in snapshot.GroupPairs.Values)
{
foreach (var pair in group)
{
trackedPairs.Add(pair);
}
}
long totalOriginalBytes = 0;
long totalEffectiveBytes = 0;
var hasData = false;
foreach (var pair in trackedPairs)
{
if (!pair.IsVisible)
continue;
var original = pair.LastAppliedApproximateVRAMBytes;
var effective = pair.LastAppliedApproximateEffectiveVRAMBytes;
if (original >= 0)
{
hasData = true;
totalOriginalBytes += original;
}
if (effective >= 0)
{
hasData = true;
totalEffectiveBytes += effective;
}
}
if (!hasData)
{
ImGui.TextDisabled("VRAM usage has not been calculated yet.");
return;
}
var savedBytes = Math.Max(0L, totalOriginalBytes - totalEffectiveBytes);
var originalText = UiSharedService.ByteToString(totalOriginalBytes, addSuffix: true);
var effectiveText = UiSharedService.ByteToString(totalEffectiveBytes, addSuffix: true);
var savedText = UiSharedService.ByteToString(savedBytes, addSuffix: true);
ImGui.TextUnformatted($"Total VRAM usage (original): {originalText}");
ImGui.TextUnformatted($"Total VRAM usage (effective): {effectiveText}");
if (savedBytes > 0)
{
UiSharedService.ColorText($"VRAM saved by downscaling: {savedText}", UIColors.Get("LightlessGreen"));
}
else
{
ImGui.TextUnformatted($"VRAM saved by downscaling: {savedText}");
}
}
private void DrawTriangleDecimationCounters()
{
HashSet<Pair> trackedPairs = new();
var snapshot = _pairUiService.GetSnapshot();
foreach (var pair in snapshot.DirectPairs)
{
trackedPairs.Add(pair);
}
foreach (var group in snapshot.GroupPairs.Values)
{
foreach (var pair in group)
{
trackedPairs.Add(pair);
}
}
long totalOriginalTris = 0;
long totalEffectiveTris = 0;
var hasData = false;
foreach (var pair in trackedPairs)
{
if (!pair.IsVisible)
continue;
var original = pair.LastAppliedDataTris;
var effective = pair.LastAppliedApproximateEffectiveTris;
if (original >= 0)
{
hasData = true;
totalOriginalTris += original;
}
if (effective >= 0)
{
hasData = true;
totalEffectiveTris += effective;
}
}
if (!hasData)
{
ImGui.TextDisabled("Triangle usage has not been calculated yet.");
return;
}
var savedTris = Math.Max(0L, totalOriginalTris - totalEffectiveTris);
var originalText = FormatTriangleCount(totalOriginalTris);
var effectiveText = FormatTriangleCount(totalEffectiveTris);
var savedText = FormatTriangleCount(savedTris);
ImGui.TextUnformatted($"Total triangle usage (original): {originalText}");
ImGui.TextUnformatted($"Total triangle usage (effective): {effectiveText}");
if (savedTris > 0)
{
UiSharedService.ColorText($"Triangles saved by decimation: {savedText}", UIColors.Get("LightlessGreen"));
}
else
{
ImGui.TextUnformatted($"Triangles saved by decimation: {savedText}");
}
static string FormatTriangleCount(long triangleCount)
{
if (triangleCount < 0)
{
return "n/a";
}
if (triangleCount >= 1_000_000)
{
return FormattableString.Invariant($"{triangleCount / 1_000_000d:0.#}m tris");
}
if (triangleCount >= 1_000)
{
return FormattableString.Invariant($"{triangleCount / 1_000d:0.#}k tris");
}
return $"{triangleCount} tris";
}
}
private void DrawThemeVectorRow(MainStyle.StyleVector2Option option)
{
ImGui.TableNextRow();
@@ -1593,6 +1451,24 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.SameLine();
ImGui.TextColored(statusColor, $"[{(pair.IsVisible ? "Visible" : pair.IsOnline ? "Online" : "Offline")}]");
if (_uiShared.IconTextButton(FontAwesomeIcon.Copy, "Copy Pair Diagnostics##pairDebugCopy"))
{
ImGui.SetClipboardText(BuildPairDiagnosticsClipboard(pair, snapshot));
}
UiSharedService.AttachToolTip("Copies the current pair diagnostics to the clipboard.");
ImGui.SameLine();
if (_uiShared.IconTextButton(FontAwesomeIcon.Copy, "Copy Last Data JSON##pairDebugCopyLastData"))
{
var lastDataForClipboard = pair.LastReceivedCharacterData;
ImGui.SetClipboardText(lastDataForClipboard is null
? "ERROR: No character data has been received for this pair."
: JsonSerializer.Serialize(lastDataForClipboard, DebugJsonOptions));
}
UiSharedService.AttachToolTip("Copies the last received character data JSON to the clipboard.");
if (ImGui.BeginTable("##pairDebugProperties", 2, ImGuiTableFlags.SizingStretchProp))
{
DrawPairPropertyRow("UID", pair.UserData.UID);
@@ -1722,6 +1598,141 @@ public class SettingsUi : WindowMediatorSubscriberBase
DrawPairEventLog(pair);
}
private string BuildPairDiagnosticsClipboard(Pair pair, PairUiSnapshot snapshot)
{
var debugInfo = pair.GetDebugInfo();
StringBuilder sb = new();
sb.AppendLine("Lightless Pair Diagnostics");
sb.AppendLine($"Generated: {DateTime.Now.ToString("G", CultureInfo.CurrentCulture)}");
sb.AppendLine();
sb.AppendLine("Pair");
sb.AppendLine($"Alias/UID: {pair.UserData.AliasOrUID}");
sb.AppendLine($"UID: {pair.UserData.UID}");
sb.AppendLine($"Alias: {(string.IsNullOrEmpty(pair.UserData.Alias) ? "(none)" : pair.UserData.Alias)}");
sb.AppendLine($"Player Name: {pair.PlayerName ?? "(not cached)"}");
sb.AppendLine($"Handler Ident: {(string.IsNullOrEmpty(pair.Ident) ? "(not bound)" : pair.Ident)}");
sb.AppendLine($"Character Id: {FormatCharacterId(pair.PlayerCharacterId)}");
sb.AppendLine($"Direct Pair: {FormatBool(pair.IsDirectlyPaired)}");
sb.AppendLine($"Individual Status: {pair.IndividualPairStatus}");
sb.AppendLine($"Any Connection: {FormatBool(pair.HasAnyConnection())}");
sb.AppendLine($"Paused: {FormatBool(pair.IsPaused)}");
sb.AppendLine($"Visible: {FormatBool(pair.IsVisible)}");
sb.AppendLine($"Online: {FormatBool(pair.IsOnline)}");
sb.AppendLine($"Has Handler: {FormatBool(debugInfo.HasHandler)}");
sb.AppendLine($"Handler Initialized: {FormatBool(debugInfo.HandlerInitialized)}");
sb.AppendLine($"Handler Visible: {FormatBool(debugInfo.HandlerVisible)}");
sb.AppendLine($"Last Time person rendered in: {FormatTimestamp(debugInfo.InvisibleSinceUtc)}");
sb.AppendLine($"Handler Timer Temp Collection removal: {FormatCountdown(debugInfo.VisibilityEvictionRemainingSeconds)}");
sb.AppendLine($"Handler Scheduled For Deletion: {FormatBool(debugInfo.HandlerScheduledForDeletion)}");
sb.AppendLine($"Note: {pair.GetNote() ?? "(none)"}");
sb.AppendLine();
sb.AppendLine("Applied Data");
sb.AppendLine($"Last Data Size: {FormatBytes(pair.LastAppliedDataBytes)}");
sb.AppendLine($"Approx. VRAM: {FormatBytes(pair.LastAppliedApproximateVRAMBytes)}");
sb.AppendLine($"Effective VRAM: {FormatBytes(pair.LastAppliedApproximateEffectiveVRAMBytes)}");
sb.AppendLine($"Last Triangles: {(pair.LastAppliedDataTris < 0 ? "n/a" : pair.LastAppliedDataTris.ToString(CultureInfo.InvariantCulture))}");
sb.AppendLine($"Effective Triangles: {(pair.LastAppliedApproximateEffectiveTris < 0 ? "n/a" : pair.LastAppliedApproximateEffectiveTris.ToString(CultureInfo.InvariantCulture))}");
sb.AppendLine();
sb.AppendLine("Last Received Character Data");
var lastData = pair.LastReceivedCharacterData;
if (lastData is null)
{
sb.AppendLine("None");
}
else
{
var fileReplacementCount = lastData.FileReplacements.Values.Sum(list => list?.Count ?? 0);
var totalGamePaths = lastData.FileReplacements.Values.Sum(list => list?.Sum(replacement => replacement.GamePaths.Length) ?? 0);
sb.AppendLine($"File replacements: {fileReplacementCount} entries across {totalGamePaths} game paths.");
sb.AppendLine($"Customize+: {lastData.CustomizePlusData.Count}, Glamourer entries: {lastData.GlamourerData.Count}");
sb.AppendLine($"Manipulation length: {lastData.ManipulationData.Length}, Heels set: {FormatBool(!string.IsNullOrEmpty(lastData.HeelsData))}");
}
sb.AppendLine();
sb.AppendLine("Application Timeline");
sb.AppendLine($"Last Data Received: {FormatTimestamp(debugInfo.LastDataReceivedAt)}");
sb.AppendLine($"Last Apply Attempt: {FormatTimestamp(debugInfo.LastApplyAttemptAt)}");
sb.AppendLine($"Last Successful Apply: {FormatTimestamp(debugInfo.LastSuccessfulApplyAt)}");
if (!string.IsNullOrEmpty(debugInfo.LastFailureReason))
{
sb.AppendLine();
sb.AppendLine($"Last failure: {debugInfo.LastFailureReason}");
if (debugInfo.BlockingConditions.Count > 0)
{
sb.AppendLine("Blocking conditions:");
foreach (var condition in debugInfo.BlockingConditions)
{
sb.AppendLine($"- {condition}");
}
}
}
sb.AppendLine();
sb.AppendLine("Application & Download State");
sb.AppendLine($"Applying Data: {FormatBool(debugInfo.IsApplying)}");
sb.AppendLine($"Downloading: {FormatBool(debugInfo.IsDownloading)}");
sb.AppendLine($"Pending Downloads: {debugInfo.PendingDownloadCount.ToString(CultureInfo.InvariantCulture)}");
sb.AppendLine($"Forbidden Downloads: {debugInfo.ForbiddenDownloadCount.ToString(CultureInfo.InvariantCulture)}");
sb.AppendLine($"Pending Mod Reapply: {FormatBool(debugInfo.PendingModReapply)}");
sb.AppendLine($"Mod Apply Deferred: {FormatBool(debugInfo.ModApplyDeferred)}");
sb.AppendLine($"Missing Critical Mods: {debugInfo.MissingCriticalMods.ToString(CultureInfo.InvariantCulture)}");
sb.AppendLine($"Missing Non-Critical Mods: {debugInfo.MissingNonCriticalMods.ToString(CultureInfo.InvariantCulture)}");
sb.AppendLine($"Missing Forbidden Mods: {debugInfo.MissingForbiddenMods.ToString(CultureInfo.InvariantCulture)}");
sb.AppendLine();
sb.AppendLine("Syncshell Memberships");
if (snapshot.PairsWithGroups.TryGetValue(pair, out var groups) && groups.Count > 0)
{
foreach (var group in groups.OrderBy(g => g.Group.AliasOrGID, StringComparer.OrdinalIgnoreCase))
{
var flags = group.GroupPairUserInfos.TryGetValue(pair.UserData.UID, out var info) ? info : GroupPairUserInfo.None;
var flagLabel = flags switch
{
GroupPairUserInfo.None => string.Empty,
_ => $" ({string.Join(", ", GetGroupInfoFlags(flags))})"
};
sb.AppendLine($"{group.Group.AliasOrGID} [{group.Group.GID}]{flagLabel}");
}
}
else
{
sb.AppendLine("Not a member of any syncshells.");
}
sb.AppendLine();
sb.AppendLine("Pair DTO Snapshot");
if (pair.UserPair is null)
{
sb.AppendLine("(unavailable)");
}
else
{
sb.AppendLine(JsonSerializer.Serialize(pair.UserPair, DebugJsonOptions));
}
var relevantEvents = GetRelevantPairEvents(pair, 40);
sb.AppendLine();
sb.AppendLine("Recent Events");
if (relevantEvents.Count == 0)
{
sb.AppendLine("No recent events were logged for this pair.");
}
else
{
foreach (var ev in relevantEvents)
{
var timestamp = ev.EventTime.ToString("T", CultureInfo.CurrentCulture);
sb.AppendLine($"{timestamp} [{ev.EventSource}] {ev.EventSeverity}: {ev.Message}");
}
}
return sb.ToString();
}
private static IEnumerable<string> GetGroupInfoFlags(GroupPairUserInfo info)
{
if (info.HasFlag(GroupPairUserInfo.IsModerator))
@@ -1735,23 +1746,28 @@ public class SettingsUi : WindowMediatorSubscriberBase
}
}
private void DrawPairEventLog(Pair pair)
private List<Event> GetRelevantPairEvents(Pair pair, int maxEvents)
{
ImGui.TextUnformatted("Recent Events");
var events = _eventAggregator.EventList.Value;
var alias = pair.UserData.Alias;
var aliasOrUid = pair.UserData.AliasOrUID;
var rawUid = pair.UserData.UID;
var playerName = pair.PlayerName;
var relevantEvents = events.Where(e =>
return events.Where(e =>
EventMatchesIdentifier(e, rawUid)
|| EventMatchesIdentifier(e, aliasOrUid)
|| EventMatchesIdentifier(e, alias)
|| (!string.IsNullOrEmpty(playerName) && string.Equals(e.Character, playerName, StringComparison.OrdinalIgnoreCase)))
.OrderByDescending(e => e.EventTime)
.Take(40)
.Take(maxEvents)
.ToList();
}
private void DrawPairEventLog(Pair pair)
{
ImGui.TextUnformatted("Recent Events");
var relevantEvents = GetRelevantPairEvents(pair, 40);
if (relevantEvents.Count == 0)
{
@@ -2290,11 +2306,29 @@ public class SettingsUi : WindowMediatorSubscriberBase
var syncshellOfflineSeparate = _configService.Current.ShowSyncshellOfflineUsersSeparately;
var greenVisiblePair = _configService.Current.ShowVisiblePairsGreenEye;
var enableParticleEffects = _configService.Current.EnableParticleEffects;
var showUiWhenUiHidden = _configService.Current.ShowUiWhenUiHidden;
var showUiInGpose = _configService.Current.ShowUiInGpose;
using (var behaviorTree = BeginGeneralTree("Behavior", UIColors.Get("LightlessPurple")))
{
if (behaviorTree.Visible)
{
if (ImGui.Checkbox("Show Lightless windows when game UI is hidden", ref showUiWhenUiHidden))
{
_configService.Current.ShowUiWhenUiHidden = showUiWhenUiHidden;
_configService.Save();
}
_uiShared.DrawHelpText("When disabled, Lightless windows (except chat) are hidden when the game UI is hidden.");
if (ImGui.Checkbox("Show Lightless windows in group pose", ref showUiInGpose))
{
_configService.Current.ShowUiInGpose = showUiInGpose;
_configService.Save();
}
_uiShared.DrawHelpText("When disabled, Lightless windows (except chat) are hidden while in group pose.");
if (ImGui.Checkbox("Enable Particle Effects", ref enableParticleEffects))
{
_configService.Current.EnableParticleEffects = enableParticleEffects;
@@ -3401,6 +3435,43 @@ public class SettingsUi : WindowMediatorSubscriberBase
_generalTreeHighlights[label] = ImGui.GetTime();
}
private void FocusPerformanceSection(PerformanceSettingsSection section)
{
_selectGeneralTabOnNextDraw = false;
_selectedMainTab = MainSettingsTab.Performance;
var label = section switch
{
PerformanceSettingsSection.TextureOptimization => PerformanceTextureOptimizationLabel,
PerformanceSettingsSection.ModelOptimization => PerformanceModelOptimizationLabel,
_ => PerformanceTextureOptimizationLabel,
};
_performanceOpenTreeTarget = label;
_performanceScrollTarget = label;
}
private bool BeginPerformanceTree(string label, Vector4 color)
{
var shouldForceOpen = string.Equals(_performanceOpenTreeTarget, label, StringComparison.Ordinal);
if (shouldForceOpen)
{
ImGui.SetNextItemOpen(true, ImGuiCond.Always);
}
var open = _uiShared.MediumTreeNode(label, color);
if (shouldForceOpen)
{
_performanceOpenTreeTarget = null;
}
if (open && string.Equals(_performanceScrollTarget, label, StringComparison.Ordinal))
{
ImGui.SetScrollHereY(0f);
_performanceScrollTarget = null;
}
return open;
}
private float GetGeneralTreeHighlightAlpha(string label)
{
if (!_generalTreeHighlights.TryGetValue(label, out var startTime))
@@ -3490,7 +3561,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
bool showPerformanceIndicator = _playerPerformanceConfigService.Current.ShowPerformanceIndicator;
if (_uiShared.MediumTreeNode("Warnings", UIColors.Get("LightlessPurple")))
if (BeginPerformanceTree(PerformanceWarningsLabel, UIColors.Get("LightlessPurple")))
{
if (ImGui.Checkbox("Show performance indicator", ref showPerformanceIndicator))
{
@@ -3586,7 +3657,7 @@ public class SettingsUi : WindowMediatorSubscriberBase
bool autoPauseInCombat = _playerPerformanceConfigService.Current.PauseInCombat;
bool autoPauseWhilePerforming = _playerPerformanceConfigService.Current.PauseWhilePerforming;
if (_uiShared.MediumTreeNode("Auto Pause", UIColors.Get("LightlessPurple")))
if (BeginPerformanceTree(PerformanceAutoPauseLabel, UIColors.Get("LightlessPurple")))
{
if (ImGui.Checkbox("Auto pause sync while combat", ref autoPauseInCombat))
{
@@ -3683,261 +3754,12 @@ public class SettingsUi : WindowMediatorSubscriberBase
ImGui.Separator();
if (_uiShared.MediumTreeNode("Texture Optimization", UIColors.Get("LightlessYellow")))
{
_uiShared.MediumText("Warning", UIColors.Get("DimRed"));
_uiShared.DrawNoteLine("! ", UIColors.Get("DimRed"),
new SeStringUtils.RichTextEntry("Texture compression and downscaling is potentially a "),
new SeStringUtils.RichTextEntry("destructive", UIColors.Get("DimRed"), true),
new SeStringUtils.RichTextEntry(" process and may cause broken or incorrect character appearances."));
_uiShared.DrawNoteLine("! ", UIColors.Get("DimRed"),
new SeStringUtils.RichTextEntry("This feature is encouraged to help "),
new SeStringUtils.RichTextEntry("lower-end systems with limited VRAM", UIColors.Get("LightlessYellow"), true),
new SeStringUtils.RichTextEntry(" and for use in "),
new SeStringUtils.RichTextEntry("performance-critical scenarios", UIColors.Get("LightlessYellow"), true),
new SeStringUtils.RichTextEntry("."));
_uiShared.DrawNoteLine("! ", UIColors.Get("DimRed"),
new SeStringUtils.RichTextEntry("Runtime downscaling "),
new SeStringUtils.RichTextEntry("MAY", UIColors.Get("DimRed"), true),
new SeStringUtils.RichTextEntry(" cause higher load on the system when processing downloads."));
_uiShared.DrawNoteLine("!!! ", UIColors.Get("DimRed"),
new SeStringUtils.RichTextEntry("When enabled, we cannot provide support for appearance issues caused by this setting!", UIColors.Get("DimRed"), true));
var textureConfig = _playerPerformanceConfigService.Current;
var trimNonIndex = textureConfig.EnableNonIndexTextureMipTrim;
if (ImGui.Checkbox("Trim mip levels for textures", ref trimNonIndex))
{
textureConfig.EnableNonIndexTextureMipTrim = trimNonIndex;
_playerPerformanceConfigService.Save();
}
_uiShared.DrawHelpText("When enabled, Lightless will remove high-resolution mip levels from textures (not index) that exceed the size limit and are not compressed with any kind compression.");
var downscaleIndex = textureConfig.EnableIndexTextureDownscale;
if (ImGui.Checkbox("Downscale index textures above limit", ref downscaleIndex))
{
textureConfig.EnableIndexTextureDownscale = downscaleIndex;
_playerPerformanceConfigService.Save();
}
_uiShared.DrawHelpText("Controls whether Lightless reduces index textures that exceed the size limit.");
var dimensionOptions = new[] { 512, 1024, 2048, 4096 };
var optionLabels = dimensionOptions.Select(selector: static value => value.ToString()).ToArray();
var currentDimension = textureConfig.TextureDownscaleMaxDimension;
var selectedIndex = Array.IndexOf(dimensionOptions, currentDimension);
if (selectedIndex < 0)
{
selectedIndex = Array.IndexOf(dimensionOptions, 2048);
}
ImGui.SetNextItemWidth(140 * ImGuiHelpers.GlobalScale);
if (ImGui.Combo("Maximum texture dimension", ref selectedIndex, optionLabels, optionLabels.Length))
{
textureConfig.TextureDownscaleMaxDimension = dimensionOptions[selectedIndex];
_playerPerformanceConfigService.Save();
}
_uiShared.DrawHelpText($"Textures above this size will be reduced until their largest dimension is at or below the limit. Block-compressed textures are skipped when \"Only downscale uncompressed\" is enabled.{UiSharedService.TooltipSeparator}Default: 2048");
var keepOriginalTextures = textureConfig.KeepOriginalTextureFiles;
if (ImGui.Checkbox("Keep original texture files", ref keepOriginalTextures))
{
textureConfig.KeepOriginalTextureFiles = keepOriginalTextures;
_playerPerformanceConfigService.Save();
}
_uiShared.DrawHelpText("When disabled, Lightless removes the original texture after a downscaled copy is created.");
ImGui.SameLine();
_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)
{
UiSharedService.ColorTextWrapped("Both trimming and downscale are disabled. Lightless will keep original textures regardless of size.", UIColors.Get("DimRed"));
}
ImGui.Dummy(new Vector2(5));
UiSharedService.ColoredSeparator(UIColors.Get("DimRed"), 3f);
var onlyUncompressed = textureConfig.OnlyDownscaleUncompressedTextures;
if (ImGui.Checkbox("Only downscale uncompressed textures", ref onlyUncompressed))
{
textureConfig.OnlyDownscaleUncompressedTextures = onlyUncompressed;
_playerPerformanceConfigService.Save();
}
_uiShared.DrawHelpText("If disabled, compressed textures will be targeted for downscaling too.");
UiSharedService.ColoredSeparator(UIColors.Get("DimRed"), 3f);
ImGui.Dummy(new Vector2(5));
DrawTextureDownscaleCounters();
ImGui.Dummy(new Vector2(5));
UiSharedService.ColoredSeparator(UIColors.Get("LightlessYellow"), 1.5f);
ImGui.TreePop();
}
ImGui.Separator();
if (_uiShared.MediumTreeNode("Model Optimization", UIColors.Get("DimRed")))
{
_uiShared.MediumText("Warning", UIColors.Get("DimRed"));
_uiShared.DrawNoteLine("! ", UIColors.Get("DimRed"),
new SeStringUtils.RichTextEntry("Model decimation is a "),
new SeStringUtils.RichTextEntry("destructive", UIColors.Get("DimRed"), true),
new SeStringUtils.RichTextEntry(" process and may cause broken or incorrect character appearances."));
_uiShared.DrawNoteLine("! ", UIColors.Get("DimRed"),
new SeStringUtils.RichTextEntry("This feature is encouraged to help "),
new SeStringUtils.RichTextEntry("lower-end systems with limited VRAM", UIColors.Get("LightlessYellow"), true),
new SeStringUtils.RichTextEntry(" and for use in "),
new SeStringUtils.RichTextEntry("performance-critical scenarios", UIColors.Get("LightlessYellow"), true),
new SeStringUtils.RichTextEntry("."));
_uiShared.DrawNoteLine("! ", UIColors.Get("DimRed"),
new SeStringUtils.RichTextEntry("Runtime decimation "),
new SeStringUtils.RichTextEntry("MAY", UIColors.Get("DimRed"), true),
new SeStringUtils.RichTextEntry(" cause higher load on the system when processing downloads."));
_uiShared.DrawNoteLine("!!! ", UIColors.Get("DimRed"),
new SeStringUtils.RichTextEntry("When enabled, we cannot provide support for appearance issues caused by this setting!", UIColors.Get("DimRed"), true));
ImGui.Dummy(new Vector2(15));
_uiShared.DrawNoteLine("! ", UIColors.Get("LightlessGreen"),
new SeStringUtils.RichTextEntry("If a mesh exceeds the "),
new SeStringUtils.RichTextEntry("triangle threshold", UIColors.Get("LightlessGreen"), true),
new SeStringUtils.RichTextEntry(", it will be decimated automatically to the set "),
new SeStringUtils.RichTextEntry("target triangle ratio", UIColors.Get("LightlessGreen"), true),
new SeStringUtils.RichTextEntry(". This will reduce quality of the mesh or may break it's intended structure."));
var performanceConfig = _playerPerformanceConfigService.Current;
var enableDecimation = performanceConfig.EnableModelDecimation;
if (ImGui.Checkbox("Enable model decimation", ref enableDecimation))
{
performanceConfig.EnableModelDecimation = enableDecimation;
_playerPerformanceConfigService.Save();
}
_uiShared.DrawHelpText("When enabled, Lightless generates a decimated copy of given model after download.");
var keepOriginalModels = performanceConfig.KeepOriginalModelFiles;
if (ImGui.Checkbox("Keep original model files", ref keepOriginalModels))
{
performanceConfig.KeepOriginalModelFiles = keepOriginalModels;
_playerPerformanceConfigService.Save();
}
_uiShared.DrawHelpText("When disabled, Lightless removes the original model after a decimated copy is created.");
ImGui.SameLine();
_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;
ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale);
if (ImGui.SliderInt("Decimate models above", ref triangleThreshold, 8_000, 100_000))
{
performanceConfig.ModelDecimationTriangleThreshold = Math.Clamp(triangleThreshold, 8_000, 100_000);
_playerPerformanceConfigService.Save();
}
ImGui.SameLine();
ImGui.Text("triangles");
_uiShared.DrawHelpText($"Models below this triangle count are left untouched.{UiSharedService.TooltipSeparator}Default: 50,000");
var targetPercent = (float)(performanceConfig.ModelDecimationTargetRatio * 100.0);
var clampedPercent = Math.Clamp(targetPercent, 60f, 99f);
if (Math.Abs(clampedPercent - targetPercent) > float.Epsilon)
{
performanceConfig.ModelDecimationTargetRatio = clampedPercent / 100.0;
_playerPerformanceConfigService.Save();
targetPercent = clampedPercent;
}
ImGui.SetNextItemWidth(300 * ImGuiHelpers.GlobalScale);
if (ImGui.SliderFloat("Target triangle ratio", ref targetPercent, 60f, 99f, "%.0f%%"))
{
performanceConfig.ModelDecimationTargetRatio = Math.Clamp(targetPercent / 100f, 0.6f, 0.99f);
_playerPerformanceConfigService.Save();
}
_uiShared.DrawHelpText($"Target ratio relative to original triangle count (80% keeps 80% of triangles).{UiSharedService.TooltipSeparator}Default: 80%");
ImGui.Dummy(new Vector2(15));
ImGui.TextUnformatted("Decimation targets");
_uiShared.DrawHelpText("Hair mods are always excluded from decimation.");
_uiShared.DrawNoteLine("! ", UIColors.Get("LightlessGreen"),
new SeStringUtils.RichTextEntry("Automatic decimation will only target the selected "),
new SeStringUtils.RichTextEntry("decimation targets", UIColors.Get("LightlessGreen"), true),
new SeStringUtils.RichTextEntry("."));
_uiShared.DrawNoteLine("! ", UIColors.Get("LightlessYellow"),
new SeStringUtils.RichTextEntry("It is advised to not decimate any body related meshes which includes: "),
new SeStringUtils.RichTextEntry("facial mods + sculpts, chest, legs, hands and feet", UIColors.Get("LightlessYellow"), true),
new SeStringUtils.RichTextEntry("."));
_uiShared.DrawNoteLine("!!! ", UIColors.Get("DimRed"),
new SeStringUtils.RichTextEntry("Remember, automatic decimation is not perfect and can cause meshes to be ruined, especially hair mods.", UIColors.Get("DimRed"), true));
var allowBody = performanceConfig.ModelDecimationAllowBody;
if (ImGui.Checkbox("Body", ref allowBody))
{
performanceConfig.ModelDecimationAllowBody = allowBody;
_playerPerformanceConfigService.Save();
}
var allowFaceHead = performanceConfig.ModelDecimationAllowFaceHead;
if (ImGui.Checkbox("Face/head", ref allowFaceHead))
{
performanceConfig.ModelDecimationAllowFaceHead = allowFaceHead;
_playerPerformanceConfigService.Save();
}
var allowTail = performanceConfig.ModelDecimationAllowTail;
if (ImGui.Checkbox("Tails/Ears", ref allowTail))
{
performanceConfig.ModelDecimationAllowTail = allowTail;
_playerPerformanceConfigService.Save();
}
var allowClothing = performanceConfig.ModelDecimationAllowClothing;
if (ImGui.Checkbox("Clothing (body/legs/shoes/gloves/hats)", ref allowClothing))
{
performanceConfig.ModelDecimationAllowClothing = allowClothing;
_playerPerformanceConfigService.Save();
}
var allowAccessories = performanceConfig.ModelDecimationAllowAccessories;
if (ImGui.Checkbox("Accessories (earring/rings/bracelet/necklace)", ref allowAccessories))
{
performanceConfig.ModelDecimationAllowAccessories = allowAccessories;
_playerPerformanceConfigService.Save();
}
ImGui.Dummy(new Vector2(5));
UiSharedService.ColoredSeparator(UIColors.Get("LightlessGrey"), 3f);
ImGui.Dummy(new Vector2(5));
DrawTriangleDecimationCounters();
ImGui.Dummy(new Vector2(5));
UiSharedService.ColoredSeparator(UIColors.Get("DimRed"), 1.5f);
ImGui.TreePop();
}
_optimizationSettingsPanel.DrawSettingsTrees(
PerformanceTextureOptimizationLabel,
UIColors.Get("LightlessYellow"),
PerformanceModelOptimizationLabel,
UIColors.Get("LightlessOrange"),
BeginPerformanceTree);
ImGui.Separator();
ImGui.Dummy(new Vector2(10));