Files
LightlessClient/LightlessSync/UI/Components/OptimizationSummaryCard.cs
defnotken 72a62b7449
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m9s
2.1.0 (#123)
# Patchnotes 2.1.0
The changes in this update are more than just "patches". With a new UI, a new feature, and a bunch of bug fixes, improvements and a new member on the dev team, we thought this was more of a minor update.

We would like to introduce @tsubasahane of MareCN to the team! We’re happy to work with them to bring Lightless and its features to the CN client as well as having another talented dev bring features and ideas to us. Speaking of which:

# Location Sharing (Big shout out to @tsubasahane for bringing this feature)

- Are you TIRED of scrambling to find the address of the venue you're in to share with your friends? We are introducing Location Sharing! An optional feature where you can share your location with direct pairs temporarily [30 minutes, 1 hour, 3 hours] minutes or until you turn it off for them. That's up to you! [#125](<#125>)  [#49](<Lightless-Sync/LightlessServer#49>)
- To share your location with a pair, click the three dots beside the pair and choose a duration to share with them. [#125](<#125>)  [#49](<Lightless-Sync/LightlessServer#49>)
- To view the location of someone who's shared with you, simply hover over the globe icon! [#125](<#125>)  [#49](<Lightless-Sync/LightlessServer#49>)

[1]

# Model Optimization (Mesh Decimating)
 - This new option can automatically “simplify” incoming character meshes to help performance by reducing triangle counts. You choose how strong the reduction is (default/recommended is 80%). [#131](<#131>)
 - Decimation only kicks in when a mesh is above a certain triangle threshold, and only for the items that qualify for it and you selected for. [#131](<#131>)
 - Hair meshes is always excluded, since simplifying hair meshes is very prone to breaking.
 - You can find everything under Settings → Performance → Model Optimization. [#131](<#131>)
+ ** IF YOU HAVE USED DECIMATION IN TESTING, PLEASE CLEAR YOUR CACHE  **

[2]

# Animation (PAP) Validation (Safer animations)
 - Lightless now checks your currently animations to see if they work with your local skeleton/bone mod. If an animation matches, it’s included in what gets sent to other players. If it doesn’t, Lightless will skip it and write a warning to your log showing how many were skipped due to skeleton changes. Its defaulted to Unsafe (off). turn it on if you experience crashes from others users. [#131](<#131>)
 - Lightless also does the same kind of check for incoming animation files, to make sure they match the body/skeleton they were sent with. [#131](<#131>)
 - Because these checks can sometimes be a little picky, you can adjust how strict they are in Settings -> General -> Animation & Bones to reduce false positives. [#131](<#131>)

# UI Changes (Thanks to @kyuwu for UI Changes)
- The top part of the main screen has gotten a makeover. You can adjust the colors of the gradiant in the Color settings of Lightless. [#127](<#127>)

[3]

- Settings have gotten some changes as well to make this change more universal, and will use the same color settings. [#127](<#127>)
- The particle effects of the gradient are toggleable in 'Settings -> UI -> Behavior' [#127](<#127>)
- Instead of showing download/upload on bottom of Main UI, it will show VRAM usage and triangles with their optimization options next to it [#138](<#138>)

# LightFinder / ShellFinder
- UI Changes that follow our new design follow the color codes for the Gradient top as the main screen does.  [#127](<#127>)

[4]

Co-authored-by: defnotken <itsdefnotken@gmail.com>
Co-authored-by: azyges <aaaaaa@aaa.aaa>
Co-authored-by: cake <admin@cakeandbanana.nl>
Co-authored-by: Tsubasa <tsubasa@noreply.git.lightless-sync.org>
Co-authored-by: choco <choco@patat.nl>
Co-authored-by: celine <aaa@aaa.aaa>
Co-authored-by: celine <celine@noreply.git.lightless-sync.org>
Co-authored-by: Tsubasahane <wozaiha@gmail.com>
Co-authored-by: cake <cake@noreply.git.lightless-sync.org>
Reviewed-on: #123
2026-01-20 19:43:00 +00:00

791 lines
32 KiB
C#

using Dalamud.Bindings.ImGui;
using Dalamud.Interface;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using LightlessSync.LightlessConfiguration;
using LightlessSync.LightlessConfiguration.Configurations;
using LightlessSync.PlayerData.Pairs;
using LightlessSync.Services.Mediator;
using LightlessSync.UI.Services;
using LightlessSync.UI.Style;
using LightlessSync.WebAPI.Files;
using System.Globalization;
using System.Numerics;
using System.Runtime.InteropServices;
namespace LightlessSync.UI.Components;
public sealed class OptimizationSummaryCard
{
private readonly UiSharedService _uiSharedService;
private readonly PairUiService _pairUiService;
private readonly PlayerPerformanceConfigService _playerPerformanceConfig;
private readonly FileUploadManager _fileTransferManager;
private readonly LightlessMediator _lightlessMediator;
private readonly OptimizationSettingsPanel _optimizationSettingsPanel;
private readonly SeluneBrush _optimizationBrush = new();
private const string OptimizationPopupId = "Optimization Settings##LightlessOptimization";
private bool _optimizationPopupOpen;
private bool _optimizationPopupRequest;
private OptimizationPanelSection _optimizationPopupSection = OptimizationPanelSection.Texture;
public OptimizationSummaryCard(
UiSharedService uiSharedService,
PairUiService pairUiService,
PlayerPerformanceConfigService playerPerformanceConfig,
FileUploadManager fileTransferManager,
LightlessMediator lightlessMediator)
{
_uiSharedService = uiSharedService;
_pairUiService = pairUiService;
_playerPerformanceConfig = playerPerformanceConfig;
_fileTransferManager = fileTransferManager;
_lightlessMediator = lightlessMediator;
_optimizationSettingsPanel = new OptimizationSettingsPanel(uiSharedService, playerPerformanceConfig, pairUiService);
}
public bool Draw(int activeDownloads)
{
var totals = GetPerformanceTotals();
var scale = ImGuiHelpers.GlobalScale;
var accent = UIColors.Get("LightlessPurple");
var accentBg = new Vector4(accent.X, accent.Y, accent.Z, 0.04f);
var accentBorder = new Vector4(accent.X, accent.Y, accent.Z, 0.16f);
var summaryPadding = new Vector2(12f * scale, 6f * scale);
var summaryItemSpacing = new Vector2(12f * scale, 4f * scale);
var cellPadding = new Vector2(6f * scale, 2f * scale);
var lineHeight = ImGui.GetFrameHeight();
var lineSpacing = summaryItemSpacing.Y;
var statsContentHeight = (lineHeight * 2f) + lineSpacing;
var summaryHeight = MathF.Max(56f * scale, statsContentHeight + (summaryPadding.Y * 2f) + (cellPadding.Y * 2f));
var activeUploads = _fileTransferManager.GetCurrentUploadsSnapshot().Count(upload => !upload.IsTransferred);
var textureButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Images);
var modelButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.ProjectDiagram);
var buttonWidth = MathF.Max(textureButtonSize.X, modelButtonSize.X);
var performanceConfig = _playerPerformanceConfig.Current;
var textureStatus = GetTextureOptimizationStatus(performanceConfig);
var modelStatus = GetModelOptimizationStatus(performanceConfig);
var textureStatusVisual = GetOptimizationStatusVisual(textureStatus);
var modelStatusVisual = GetOptimizationStatusVisual(modelStatus);
var textureStatusLines = BuildTextureOptimizationStatusLines(performanceConfig);
var modelStatusLines = BuildModelOptimizationStatusLines(performanceConfig);
var statusIconSpacing = 6f * scale;
var statusIconWidth = MathF.Max(GetIconWidth(textureStatusVisual.Icon), GetIconWidth(modelStatusVisual.Icon));
var buttonRowWidth = buttonWidth + statusIconWidth + statusIconSpacing;
var vramValue = totals.HasVramData
? UiSharedService.ByteToString(totals.DisplayVramBytes, addSuffix: true)
: "n/a";
var vramTooltip = BuildVramTooltipData(totals, UIColors.Get("LightlessBlue"));
var triangleValue = totals.HasTriangleData
? FormatTriangleCount(totals.DisplayTriangleCount)
: "n/a";
var triangleTooltip = BuildTriangleTooltipData(totals, UIColors.Get("LightlessPurple"));
var windowPos = ImGui.GetWindowPos();
var windowSize = ImGui.GetWindowSize();
var footerTop = ImGui.GetCursorScreenPos().Y;
var gradientTop = MathF.Max(windowPos.Y, footerTop - (12f * scale));
var gradientBottom = windowPos.Y + windowSize.Y;
var footerSettings = new SeluneGradientSettings
{
GradientColor = UIColors.Get("LightlessPurple"),
GradientPeakOpacity = 0.08f,
GradientPeakPosition = 0.18f,
BackgroundMode = SeluneGradientMode.Vertical,
};
using var footerSelune = Selune.Begin(_optimizationBrush, ImGui.GetWindowDrawList(), windowPos, windowSize, footerSettings);
footerSelune.DrawGradient(gradientTop, gradientBottom, ImGui.GetIO().DeltaTime);
using (ImRaii.PushStyle(ImGuiStyleVar.ChildRounding, 6f * scale))
using (ImRaii.PushStyle(ImGuiStyleVar.ChildBorderSize, MathF.Max(1f, ImGui.GetStyle().ChildBorderSize)))
using (ImRaii.PushStyle(ImGuiStyleVar.WindowPadding, summaryPadding))
using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, summaryItemSpacing))
using (ImRaii.PushColor(ImGuiCol.ChildBg, UiSharedService.Color(accentBg)))
using (ImRaii.PushColor(ImGuiCol.Border, UiSharedService.Color(accentBorder)))
using (var child = ImRaii.Child("optimizationSummary", new Vector2(-1f, summaryHeight), true, ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse))
{
if (child)
{
using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, cellPadding))
{
if (ImGui.BeginTable("optimizationSummaryTable", 2, ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.NoBordersInBody))
{
ImGui.TableSetupColumn("Stats", ImGuiTableColumnFlags.WidthStretch, 1f);
ImGui.TableSetupColumn("Button", ImGuiTableColumnFlags.WidthFixed, buttonRowWidth + 12f * scale);
ImGui.TableNextRow();
ImGui.TableNextColumn();
var availableHeight = summaryHeight - (summaryPadding.Y * 2f) - (cellPadding.Y * 2f);
var verticalPad = MathF.Max(0f, (availableHeight - statsContentHeight) * 0.5f);
using (ImRaii.PushStyle(ImGuiStyleVar.ItemSpacing, new Vector2(summaryItemSpacing.X, 0f)))
{
if (verticalPad > 0f)
{
ImGui.Dummy(new Vector2(0f, verticalPad));
}
DrawOptimizationStatLine(FontAwesomeIcon.Memory, UIColors.Get("LightlessBlue"), "VRAM usage", vramValue, vramTooltip, scale);
if (lineSpacing > 0f)
{
ImGui.Dummy(new Vector2(0f, lineSpacing));
}
DrawOptimizationStatLine(FontAwesomeIcon.ProjectDiagram, UIColors.Get("LightlessPurple"), "Triangles", triangleValue, triangleTooltip, scale);
if (verticalPad > 0f)
{
ImGui.Dummy(new Vector2(0f, verticalPad));
}
}
ImGui.TableNextColumn();
var separatorX = ImGui.GetCursorScreenPos().X - cellPadding.X;
var separatorTop = ImGui.GetWindowPos().Y + summaryPadding.Y;
var separatorBottom = ImGui.GetWindowPos().Y + summaryHeight - summaryPadding.Y;
ImGui.GetWindowDrawList().AddLine(
new Vector2(separatorX, separatorTop),
new Vector2(separatorX, separatorBottom),
ImGui.ColorConvertFloat4ToU32(accentBorder),
MathF.Max(1f, 1f * scale));
float cellWidth = ImGui.GetContentRegionAvail().X;
float offsetX = MathF.Max(0f, cellWidth - buttonRowWidth);
float alignedX = ImGui.GetCursorPosX() + offsetX;
using (ImRaii.PushStyle(ImGuiStyleVar.FrameRounding, 6f * scale))
using (ImRaii.PushColor(ImGuiCol.Button, ImGui.ColorConvertFloat4ToU32(new Vector4(0f, 0f, 0f, 0f))))
{
var buttonBorderThickness = 10f * scale;
var buttonRounding = ImGui.GetStyle().FrameRounding;
DrawOptimizationStatusButtonRow(
"Texture Optimization",
textureStatusVisual.Icon,
textureStatusVisual.Color,
textureStatusVisual.Label,
textureStatusLines,
FontAwesomeIcon.Images,
textureButtonSize,
"Texture Optimization",
activeUploads,
activeDownloads,
() => OpenOptimizationPopup(OptimizationPanelSection.Texture),
alignedX,
statusIconSpacing,
buttonBorderThickness,
buttonRounding);
DrawOptimizationStatusButtonRow(
"Model Optimization",
modelStatusVisual.Icon,
modelStatusVisual.Color,
modelStatusVisual.Label,
modelStatusLines,
FontAwesomeIcon.ProjectDiagram,
modelButtonSize,
"Model Optimization",
activeUploads,
activeDownloads,
() => OpenOptimizationPopup(OptimizationPanelSection.Model),
alignedX,
statusIconSpacing,
buttonBorderThickness,
buttonRounding);
}
ImGui.EndTable();
}
}
}
}
footerSelune.DrawHighlightOnly(gradientTop, gradientBottom, ImGui.GetIO().DeltaTime);
DrawOptimizationPopup();
return true;
}
private PerformanceTotals GetPerformanceTotals()
{
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 displayVramBytes = 0;
long originalVramBytes = 0;
long effectiveVramBytes = 0;
bool hasVramData = false;
bool hasOriginalVram = false;
bool hasEffectiveVram = false;
long displayTriangles = 0;
long originalTriangles = 0;
long effectiveTriangles = 0;
bool hasTriangleData = false;
bool hasOriginalTriangles = false;
bool hasEffectiveTriangles = false;
foreach (var pair in trackedPairs)
{
if (!pair.IsVisible)
{
continue;
}
var originalVram = pair.LastAppliedApproximateVRAMBytes;
var effectiveVram = pair.LastAppliedApproximateEffectiveVRAMBytes;
if (originalVram >= 0)
{
originalVramBytes += originalVram;
hasOriginalVram = true;
}
if (effectiveVram >= 0)
{
effectiveVramBytes += effectiveVram;
hasEffectiveVram = true;
}
if (effectiveVram >= 0)
{
displayVramBytes += effectiveVram;
hasVramData = true;
}
else if (originalVram >= 0)
{
displayVramBytes += originalVram;
hasVramData = true;
}
var originalTris = pair.LastAppliedDataTris;
var effectiveTris = pair.LastAppliedApproximateEffectiveTris;
if (originalTris >= 0)
{
originalTriangles += originalTris;
hasOriginalTriangles = true;
}
if (effectiveTris >= 0)
{
effectiveTriangles += effectiveTris;
hasEffectiveTriangles = true;
}
if (effectiveTris >= 0)
{
displayTriangles += effectiveTris;
hasTriangleData = true;
}
else if (originalTris >= 0)
{
displayTriangles += originalTris;
hasTriangleData = true;
}
}
return new PerformanceTotals(
displayVramBytes,
originalVramBytes,
effectiveVramBytes,
displayTriangles,
originalTriangles,
effectiveTriangles,
hasVramData,
hasOriginalVram,
hasEffectiveVram,
hasTriangleData,
hasOriginalTriangles,
hasEffectiveTriangles);
}
private void DrawOptimizationStatLine(FontAwesomeIcon icon, Vector4 iconColor, string label, string value, OptimizationStatTooltip? tooltip, float scale)
{
ImGui.AlignTextToFramePadding();
_uiSharedService.IconText(icon, iconColor);
var hovered = ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem);
ImGui.SameLine(0f, 6f * scale);
ImGui.TextUnformatted($"{label}: {value}");
hovered |= ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem);
if (hovered && tooltip.HasValue)
{
DrawOptimizationStatTooltip(tooltip.Value);
}
}
private static OptimizationStatTooltip? BuildVramTooltipData(PerformanceTotals totals, Vector4 titleColor)
{
if (!totals.HasOriginalVram && !totals.HasEffectiveVram)
{
return null;
}
var lines = new List<OptimizationTooltipLine>();
if (totals.HasOriginalVram)
{
lines.Add(new OptimizationTooltipLine(
"Original",
UiSharedService.ByteToString(totals.OriginalVramBytes, addSuffix: true),
UIColors.Get("LightlessYellow")));
}
if (totals.HasEffectiveVram)
{
lines.Add(new OptimizationTooltipLine(
"Effective",
UiSharedService.ByteToString(totals.EffectiveVramBytes, addSuffix: true),
UIColors.Get("LightlessGreen")));
}
if (totals.HasOriginalVram && totals.HasEffectiveVram)
{
var savedBytes = Math.Max(0L, totals.OriginalVramBytes - totals.EffectiveVramBytes);
if (savedBytes > 0)
{
lines.Add(new OptimizationTooltipLine(
"Saved",
UiSharedService.ByteToString(savedBytes, addSuffix: true),
titleColor));
}
}
return new OptimizationStatTooltip(
"Total VRAM usage",
"Approximate texture memory across visible users.",
titleColor,
lines);
}
private static OptimizationStatTooltip? BuildTriangleTooltipData(PerformanceTotals totals, Vector4 titleColor)
{
if (!totals.HasOriginalTriangles && !totals.HasEffectiveTriangles)
{
return null;
}
var lines = new List<OptimizationTooltipLine>();
if (totals.HasOriginalTriangles)
{
lines.Add(new OptimizationTooltipLine(
"Original",
$"{FormatTriangleCount(totals.OriginalTriangleCount)} tris",
UIColors.Get("LightlessYellow")));
}
if (totals.HasEffectiveTriangles)
{
lines.Add(new OptimizationTooltipLine(
"Effective",
$"{FormatTriangleCount(totals.EffectiveTriangleCount)} tris",
UIColors.Get("LightlessGreen")));
}
if (totals.HasOriginalTriangles && totals.HasEffectiveTriangles)
{
var savedTris = Math.Max(0L, totals.OriginalTriangleCount - totals.EffectiveTriangleCount);
if (savedTris > 0)
{
lines.Add(new OptimizationTooltipLine(
"Saved",
$"{FormatTriangleCount(savedTris)} tris",
titleColor));
}
}
return new OptimizationStatTooltip(
"Total triangles",
"Approximate triangle count across visible users.",
titleColor,
lines);
}
private 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");
}
if (triangleCount >= 1_000)
{
return FormattableString.Invariant($"{triangleCount / 1_000d:0.#}k");
}
return triangleCount.ToString(CultureInfo.InvariantCulture);
}
private enum OptimizationStatus
{
Off,
Partial,
On,
}
private static OptimizationStatus GetTextureOptimizationStatus(PlayerPerformanceConfig config)
{
bool trimEnabled = config.EnableNonIndexTextureMipTrim;
bool downscaleEnabled = config.EnableIndexTextureDownscale;
if (!trimEnabled && !downscaleEnabled)
{
return OptimizationStatus.Off;
}
return trimEnabled && downscaleEnabled
? OptimizationStatus.On
: OptimizationStatus.Partial;
}
private static OptimizationStatus GetModelOptimizationStatus(PlayerPerformanceConfig config)
{
if (!config.EnableModelDecimation)
{
return OptimizationStatus.Off;
}
bool hasTargets = config.ModelDecimationAllowBody
|| config.ModelDecimationAllowFaceHead
|| config.ModelDecimationAllowTail
|| config.ModelDecimationAllowClothing
|| config.ModelDecimationAllowAccessories;
return hasTargets
? OptimizationStatus.On
: OptimizationStatus.Partial;
}
private static (FontAwesomeIcon Icon, Vector4 Color, string Label) GetOptimizationStatusVisual(OptimizationStatus status)
{
return status switch
{
OptimizationStatus.On => (FontAwesomeIcon.Check, UIColors.Get("LightlessGreen"), "Enabled"),
OptimizationStatus.Partial => (FontAwesomeIcon.ExclamationTriangle, UIColors.Get("LightlessYellow"), "Partial"),
_ => (FontAwesomeIcon.Times, UIColors.Get("DimRed"), "Disabled"),
};
}
private static OptimizationTooltipLine[] BuildTextureOptimizationStatusLines(PlayerPerformanceConfig config)
{
return
[
new OptimizationTooltipLine("Trim mip levels", FormatOnOff(config.EnableNonIndexTextureMipTrim), GetOnOffColor(config.EnableNonIndexTextureMipTrim)),
new OptimizationTooltipLine("Downscale index textures", FormatOnOff(config.EnableIndexTextureDownscale), GetOnOffColor(config.EnableIndexTextureDownscale)),
new OptimizationTooltipLine("Max dimension", config.TextureDownscaleMaxDimension.ToString(CultureInfo.InvariantCulture)),
new OptimizationTooltipLine("Only downscale uncompressed", FormatOnOff(config.OnlyDownscaleUncompressedTextures), GetOnOffColor(config.OnlyDownscaleUncompressedTextures)),
new OptimizationTooltipLine("Compress uncompressed textures", FormatOnOff(config.EnableUncompressedTextureCompression), GetOnOffColor(config.EnableUncompressedTextureCompression)),
new OptimizationTooltipLine("Skip auto-compress mipmaps", FormatOnOff(config.SkipUncompressedTextureCompressionMipMaps), GetOnOffColor(config.SkipUncompressedTextureCompressionMipMaps)),
new OptimizationTooltipLine("Keep original textures", FormatOnOff(config.KeepOriginalTextureFiles), GetOnOffColor(config.KeepOriginalTextureFiles)),
new OptimizationTooltipLine("Skip preferred pairs", FormatOnOff(config.SkipTextureDownscaleForPreferredPairs), GetOnOffColor(config.SkipTextureDownscaleForPreferredPairs)),
];
}
private static OptimizationTooltipLine[] BuildModelOptimizationStatusLines(PlayerPerformanceConfig config)
{
var targets = new List<string>();
if (config.ModelDecimationAllowBody)
{
targets.Add("Body");
}
if (config.ModelDecimationAllowFaceHead)
{
targets.Add("Face/head");
}
if (config.ModelDecimationAllowTail)
{
targets.Add("Tails/Ears");
}
if (config.ModelDecimationAllowClothing)
{
targets.Add("Clothing");
}
if (config.ModelDecimationAllowAccessories)
{
targets.Add("Accessories");
}
var targetLabel = targets.Count > 0 ? string.Join(", ", targets) : "None";
var targetColor = targets.Count > 0 ? UIColors.Get("LightlessGreen") : UIColors.Get("DimRed");
var threshold = config.ModelDecimationTriangleThreshold.ToString("N0", CultureInfo.InvariantCulture);
var targetRatio = FormatPercent(config.ModelDecimationTargetRatio);
return
[
new OptimizationTooltipLine("Decimation enabled", FormatOnOff(config.EnableModelDecimation), GetOnOffColor(config.EnableModelDecimation)),
new OptimizationTooltipLine("Triangle threshold", threshold),
new OptimizationTooltipLine("Target ratio", targetRatio),
new OptimizationTooltipLine("Normalize tangents", FormatOnOff(config.ModelDecimationNormalizeTangents), GetOnOffColor(config.ModelDecimationNormalizeTangents)),
new OptimizationTooltipLine("Avoid body intersection", FormatOnOff(config.ModelDecimationAvoidBodyIntersection), GetOnOffColor(config.ModelDecimationAvoidBodyIntersection)),
new OptimizationTooltipLine("Keep original models", FormatOnOff(config.KeepOriginalModelFiles), GetOnOffColor(config.KeepOriginalModelFiles)),
new OptimizationTooltipLine("Skip preferred pairs", FormatOnOff(config.SkipModelDecimationForPreferredPairs), GetOnOffColor(config.SkipModelDecimationForPreferredPairs)),
new OptimizationTooltipLine("Targets", targetLabel, targetColor),
];
}
private static string FormatOnOff(bool value)
=> value ? "On" : "Off";
private static string FormatPercent(double value)
=> FormattableString.Invariant($"{value * 100d:0.#}%");
private static Vector4 GetOnOffColor(bool value)
=> value ? UIColors.Get("LightlessGreen") : UIColors.Get("DimRed");
private static float GetIconWidth(FontAwesomeIcon icon)
{
using var iconFont = ImRaii.PushFont(UiBuilder.IconFont);
return ImGui.CalcTextSize(icon.ToIconString()).X;
}
private readonly record struct OptimizationStatTooltip(string Title, string Description, Vector4 TitleColor, IReadOnlyList<OptimizationTooltipLine> Lines);
private static void DrawOptimizationStatTooltip(OptimizationStatTooltip tooltip)
{
ImGui.BeginTooltip();
ImGui.PushTextWrapPos(ImGui.GetFontSize() * 32f);
ImGui.TextColored(tooltip.TitleColor, tooltip.Title);
ImGui.TextColored(UIColors.Get("LightlessGrey"), tooltip.Description);
foreach (var line in tooltip.Lines)
{
ImGui.TextUnformatted($"{line.Label}:");
ImGui.SameLine();
if (line.ValueColor.HasValue)
{
ImGui.TextColored(line.ValueColor.Value, line.Value);
}
else
{
ImGui.TextUnformatted(line.Value);
}
}
ImGui.PopTextWrapPos();
ImGui.EndTooltip();
}
private static void DrawOptimizationButtonTooltip(string title, int activeUploads, int activeDownloads)
{
ImGui.BeginTooltip();
ImGui.PushTextWrapPos(ImGui.GetFontSize() * 32f);
ImGui.TextColored(UIColors.Get("LightlessPurple"), title);
ImGui.TextColored(UIColors.Get("LightlessGrey"), "Open optimization settings.");
if (activeUploads > 0 || activeDownloads > 0)
{
ImGui.Separator();
ImGui.TextUnformatted($"Active uploads: {activeUploads}");
ImGui.TextUnformatted($"Active downloads: {activeDownloads}");
}
ImGui.PopTextWrapPos();
ImGui.EndTooltip();
}
private readonly record struct OptimizationTooltipLine(string Label, string Value, Vector4? ValueColor = null);
private static void DrawOptimizationStatusTooltip(string title, string statusLabel, Vector4 statusColor, IReadOnlyList<OptimizationTooltipLine> lines)
{
ImGui.BeginTooltip();
ImGui.PushTextWrapPos(ImGui.GetFontSize() * 32f);
ImGui.TextColored(UIColors.Get("LightlessPurple"), title);
ImGui.TextUnformatted("Status:");
ImGui.SameLine();
ImGui.TextColored(statusColor, statusLabel);
foreach (var line in lines)
{
ImGui.TextUnformatted($"{line.Label}:");
ImGui.SameLine();
if (line.ValueColor.HasValue)
{
ImGui.TextColored(line.ValueColor.Value, line.Value);
}
else
{
ImGui.TextUnformatted(line.Value);
}
}
ImGui.PopTextWrapPos();
ImGui.EndTooltip();
}
private void DrawOptimizationStatusButtonRow(
string statusTitle,
FontAwesomeIcon statusIcon,
Vector4 statusColor,
string statusLabel,
IReadOnlyList<OptimizationTooltipLine> statusLines,
FontAwesomeIcon buttonIcon,
Vector2 buttonSize,
string tooltipTitle,
int activeUploads,
int activeDownloads,
Action openPopup,
float alignedX,
float iconSpacing,
float buttonBorderThickness,
float buttonRounding)
{
ImGui.SetCursorPosX(alignedX);
ImGui.AlignTextToFramePadding();
_uiSharedService.IconText(statusIcon, statusColor);
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem))
{
DrawOptimizationStatusTooltip(statusTitle, statusLabel, statusColor, statusLines);
}
ImGui.SameLine(0f, iconSpacing);
using (ImRaii.PushFont(UiBuilder.IconFont))
{
if (ImGui.Button(buttonIcon.ToIconString(), buttonSize))
{
openPopup();
}
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) || ImGui.IsItemActive())
{
Selune.RegisterHighlight(ImGui.GetItemRectMin(), ImGui.GetItemRectMax(), SeluneHighlightMode.Both, true, buttonBorderThickness, exactSize: true, clipToElement: true, roundingOverride: buttonRounding);
}
if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem))
{
DrawOptimizationButtonTooltip(tooltipTitle, activeUploads, activeDownloads);
}
}
private void OpenOptimizationPopup(OptimizationPanelSection section)
{
_optimizationPopupSection = section;
_optimizationPopupOpen = true;
_optimizationPopupRequest = true;
}
private void DrawOptimizationPopup()
{
if (!_optimizationPopupOpen)
{
return;
}
if (_optimizationPopupRequest)
{
ImGui.OpenPopup(OptimizationPopupId);
_optimizationPopupRequest = false;
}
var scale = ImGuiHelpers.GlobalScale;
ImGui.SetNextWindowSize(new Vector2(680f * scale, 640f * scale), ImGuiCond.Appearing);
if (ImGui.BeginPopupModal(OptimizationPopupId, ref _optimizationPopupOpen, UiSharedService.PopupWindowFlags))
{
DrawOptimizationPopupHeader();
ImGui.Separator();
ImGui.Dummy(new Vector2(0f, 4f * scale));
using (var child = ImRaii.Child("optimization-popup-body", new Vector2(0f, 0f), false, ImGuiWindowFlags.AlwaysVerticalScrollbar))
{
if (child)
{
_optimizationSettingsPanel.DrawPopup(_optimizationPopupSection);
}
}
ImGui.EndPopup();
}
}
private void DrawOptimizationPopupHeader()
{
var scale = ImGuiHelpers.GlobalScale;
var (title, icon, color, section) = GetPopupHeaderData(_optimizationPopupSection);
var settingsButtonSize = _uiSharedService.GetIconButtonSize(FontAwesomeIcon.Cog);
using (var table = ImRaii.Table("optimization-popup-header", 2, ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.NoBordersInBody))
{
if (!table)
{
return;
}
ImGui.TableSetupColumn("Title", ImGuiTableColumnFlags.WidthStretch);
ImGui.TableSetupColumn("Settings", ImGuiTableColumnFlags.WidthFixed, settingsButtonSize.X);
ImGui.TableNextRow();
ImGui.TableNextColumn();
using (_uiSharedService.MediumFont.Push())
{
_uiSharedService.IconText(icon, color);
ImGui.SameLine(0f, 6f * scale);
ImGui.TextColored(color, title);
}
ImGui.TableNextColumn();
using (ImRaii.PushFont(UiBuilder.IconFont))
{
if (ImGui.Button(FontAwesomeIcon.Cog.ToIconString(), settingsButtonSize))
{
OpenOptimizationSettings(section);
}
}
UiSharedService.AttachToolTip("Open this section in Settings.");
}
}
private void OpenOptimizationSettings(OptimizationPanelSection section)
{
var target = section == OptimizationPanelSection.Texture
? PerformanceSettingsSection.TextureOptimization
: PerformanceSettingsSection.ModelOptimization;
_lightlessMediator.Publish(new OpenPerformanceSettingsMessage(target));
_optimizationPopupOpen = false;
ImGui.CloseCurrentPopup();
}
private static (string Title, FontAwesomeIcon Icon, Vector4 Color, OptimizationPanelSection Section) GetPopupHeaderData(OptimizationPanelSection section)
{
return section == OptimizationPanelSection.Texture
? ("Texture Optimization", FontAwesomeIcon.Images, UIColors.Get("LightlessYellow"), OptimizationPanelSection.Texture)
: ("Model Optimization", FontAwesomeIcon.ProjectDiagram, UIColors.Get("LightlessOrange"), OptimizationPanelSection.Model);
}
[StructLayout(LayoutKind.Auto)]
private readonly record struct PerformanceTotals(
long DisplayVramBytes,
long OriginalVramBytes,
long EffectiveVramBytes,
long DisplayTriangleCount,
long OriginalTriangleCount,
long EffectiveTriangleCount,
bool HasVramData,
bool HasOriginalVram,
bool HasEffectiveVram,
bool HasTriangleData,
bool HasOriginalTriangles,
bool HasEffectiveTriangles);
}