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.UI.Services; using LightlessSync.Utils; using System.Numerics; namespace LightlessSync.UI.Components; public enum OptimizationPanelSection { Texture, Model, } public sealed class OptimizationSettingsPanel { private readonly UiSharedService _uiSharedService; private readonly PlayerPerformanceConfigService _performanceConfigService; private readonly PairUiService _pairUiService; private const ImGuiTableFlags SettingsTableFlags = ImGuiTableFlags.SizingStretchProp | ImGuiTableFlags.NoBordersInBody | ImGuiTableFlags.PadOuterX; public OptimizationSettingsPanel( UiSharedService uiSharedService, PlayerPerformanceConfigService performanceConfigService, PairUiService pairUiService) { _uiSharedService = uiSharedService; _performanceConfigService = performanceConfigService; _pairUiService = pairUiService; } public void DrawSettingsTrees( string textureLabel, Vector4 textureColor, string modelLabel, Vector4 modelColor, Func beginTree) { if (beginTree(textureLabel, textureColor)) { DrawTextureSection(showTitle: false); UiSharedService.ColoredSeparator(textureColor, 1.5f); ImGui.TreePop(); } ImGui.Separator(); if (beginTree(modelLabel, modelColor)) { DrawModelSection(showTitle: false); UiSharedService.ColoredSeparator(modelColor, 1.5f); ImGui.TreePop(); } } public void DrawPopup(OptimizationPanelSection section) { switch (section) { case OptimizationPanelSection.Texture: DrawTextureSection(showTitle: false); break; case OptimizationPanelSection.Model: DrawModelSection(showTitle: false); break; } } private void DrawTextureSection(bool showTitle) { var scale = ImGuiHelpers.GlobalScale; DrawSectionIntro( FontAwesomeIcon.Images, UIColors.Get("LightlessYellow"), "Texture Optimization", "Reduce texture memory by trimming mip levels and downscaling oversized textures.", showTitle); DrawCallout("texture-opt-warning", UIColors.Get("DimRed"), () => { _uiSharedService.MediumText("Warning", UIColors.Get("DimRed")); _uiSharedService.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.")); _uiSharedService.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(".")); _uiSharedService.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.")); _uiSharedService.DrawNoteLine("!!! ", UIColors.Get("DimRed"), new SeStringUtils.RichTextEntry("When enabled, we cannot provide support for appearance issues caused by this setting!", UIColors.Get("DimRed"), true)); }); DrawCallout("texture-opt-info", UIColors.Get("LightlessGrey"), () => { _uiSharedService.DrawNoteLine("i ", UIColors.Get("LightlessGrey"), new SeStringUtils.RichTextEntry("Compression, downscale, and mip trimming only apply to "), new SeStringUtils.RichTextEntry("newly downloaded pairs", UIColors.Get("LightlessYellow"), true), new SeStringUtils.RichTextEntry(". Existing downloads are not reprocessed; re-download to apply.")); }); ImGui.Dummy(new Vector2(0f, 2f * scale)); DrawGroupHeader("Core Controls", UIColors.Get("LightlessYellow")); var textureConfig = _performanceConfigService.Current; using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(6f * scale, 2f * scale))) using (var table = ImRaii.Table("texture-opt-core", 3, SettingsTableFlags)) { if (table) { ImGui.TableSetupColumn("Label", ImGuiTableColumnFlags.WidthFixed, 220f * scale); ImGui.TableSetupColumn("Control", ImGuiTableColumnFlags.WidthFixed, 180f * scale); ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); DrawControlRow("Trim mip levels", () => { var trimNonIndex = textureConfig.EnableNonIndexTextureMipTrim; var accent = UIColors.Get("LightlessYellow"); if (DrawAccentCheckbox("##texture-trim-mips", ref trimNonIndex, accent)) { textureConfig.EnableNonIndexTextureMipTrim = trimNonIndex; _performanceConfigService.Save(); } }, "Removes high-resolution mip levels from oversized non-index textures.", UIColors.Get("LightlessYellow"), UIColors.Get("LightlessYellow")); DrawControlRow("Downscale index textures", () => { var downscaleIndex = textureConfig.EnableIndexTextureDownscale; var accent = UIColors.Get("LightlessYellow"); if (DrawAccentCheckbox("##texture-downscale-index", ref downscaleIndex, accent)) { textureConfig.EnableIndexTextureDownscale = downscaleIndex; _performanceConfigService.Save(); } }, "Downscales oversized index textures to the configured dimension.", UIColors.Get("LightlessYellow"), UIColors.Get("LightlessYellow")); DrawControlRow("Max texture dimension", () => { var dimensionOptions = new[] { 512, 1024, 2048, 4096 }; var optionLabels = dimensionOptions.Select(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(-1f); if (ImGui.Combo("##texture-max-dimension", ref selectedIndex, optionLabels, optionLabels.Length)) { textureConfig.TextureDownscaleMaxDimension = dimensionOptions[selectedIndex]; _performanceConfigService.Save(); } }, "Textures above this size are reduced to the limit. Default: 2048."); } } if (!textureConfig.EnableNonIndexTextureMipTrim && !textureConfig.EnableIndexTextureDownscale && !textureConfig.EnableUncompressedTextureCompression) { UiSharedService.ColorTextWrapped( "Texture trimming, downscale, and compression are disabled. Lightless will keep original textures regardless of size.", UIColors.Get("DimRed")); } ImGui.Dummy(new Vector2(0f, 4f * scale)); DrawGroupHeader("Behavior & Exceptions", UIColors.Get("LightlessYellow")); using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(6f * scale, 2f * scale))) using (var table = ImRaii.Table("texture-opt-behavior", 3, SettingsTableFlags)) { if (table) { ImGui.TableSetupColumn("Label", ImGuiTableColumnFlags.WidthFixed, 220f * scale); ImGui.TableSetupColumn("Control", ImGuiTableColumnFlags.WidthFixed, 180f * scale); ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); DrawControlRow("Only downscale uncompressed", () => { var onlyUncompressed = textureConfig.OnlyDownscaleUncompressedTextures; if (ImGui.Checkbox("##texture-only-uncompressed", ref onlyUncompressed)) { textureConfig.OnlyDownscaleUncompressedTextures = onlyUncompressed; _performanceConfigService.Save(); } }, "When disabled, block-compressed textures can be downscaled too."); } } ImGui.Dummy(new Vector2(0f, 2f * scale)); DrawTextureCompressionCard(textureConfig); using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(6f * scale, 2f * scale))) using (var table = ImRaii.Table("texture-opt-behavior-extra", 3, SettingsTableFlags)) { if (table) { ImGui.TableSetupColumn("Label", ImGuiTableColumnFlags.WidthFixed, 220f * scale); ImGui.TableSetupColumn("Control", ImGuiTableColumnFlags.WidthFixed, 180f * scale); ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); DrawControlRow("Keep original texture files", () => { var keepOriginalTextures = textureConfig.KeepOriginalTextureFiles; if (ImGui.Checkbox("##texture-keep-original", ref keepOriginalTextures)) { textureConfig.KeepOriginalTextureFiles = keepOriginalTextures; _performanceConfigService.Save(); } }, "Keeps the original texture alongside the downscaled copy."); DrawControlRow("Skip preferred/direct pairs", () => { var skipPreferredDownscale = textureConfig.SkipTextureDownscaleForPreferredPairs; if (ImGui.Checkbox("##texture-skip-preferred", ref skipPreferredDownscale)) { textureConfig.SkipTextureDownscaleForPreferredPairs = skipPreferredDownscale; _performanceConfigService.Save(); } }, "Leaves textures untouched for preferred/direct pairs."); } } UiSharedService.ColorTextWrapped( "Note: Disabling \"Keep original texture files\" prevents saved/effective VRAM usage information.", UIColors.Get("LightlessYellow")); ImGui.Dummy(new Vector2(0f, 4f * scale)); DrawSummaryPanel("Usage Summary", UIColors.Get("LightlessPurple"), DrawTextureDownscaleCounters); } private void DrawModelSection(bool showTitle) { var scale = ImGuiHelpers.GlobalScale; DrawSectionIntro( FontAwesomeIcon.ProjectDiagram, UIColors.Get("LightlessOrange"), "Model Optimization", "Reduce triangle counts by decimating models above a threshold.", showTitle); DrawCallout("model-opt-warning", UIColors.Get("DimRed"), () => { _uiSharedService.MediumText("Warning", UIColors.Get("DimRed")); _uiSharedService.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.")); _uiSharedService.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(".")); _uiSharedService.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.")); _uiSharedService.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(0f, 2f * scale)); DrawCallout("model-opt-behavior", UIColors.Get("LightlessGreen"), () => { _uiSharedService.DrawNoteLine("! ", UIColors.Get("LightlessGreen"), new SeStringUtils.RichTextEntry("Meshes above the "), new SeStringUtils.RichTextEntry("triangle threshold", UIColors.Get("LightlessGreen"), true), new SeStringUtils.RichTextEntry(" will be decimated to the "), new SeStringUtils.RichTextEntry("target ratio", UIColors.Get("LightlessGreen"), true), new SeStringUtils.RichTextEntry(". This can reduce quality or alter intended structure.")); _uiSharedService.DrawNoteLine("i ", UIColors.Get("LightlessGreen"), new SeStringUtils.RichTextEntry("Decimation only applies to "), new SeStringUtils.RichTextEntry("newly downloaded pairs", UIColors.Get("LightlessYellow"), true), new SeStringUtils.RichTextEntry(". Existing downloads are not reprocessed; re-download to apply.")); }); DrawGroupHeader("Core Controls", UIColors.Get("LightlessOrange")); var performanceConfig = _performanceConfigService.Current; using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(6f * scale, 2f * scale))) using (var table = ImRaii.Table("model-opt-core", 3, SettingsTableFlags)) { if (table) { ImGui.TableSetupColumn("Label", ImGuiTableColumnFlags.WidthFixed, 220f * scale); ImGui.TableSetupColumn("Control", ImGuiTableColumnFlags.WidthFixed, 180f * scale); ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); DrawControlRow("Enable model decimation", () => { var enableDecimation = performanceConfig.EnableModelDecimation; var accent = UIColors.Get("LightlessOrange"); if (DrawAccentCheckbox("##enable-model-decimation", ref enableDecimation, accent)) { performanceConfig.EnableModelDecimation = enableDecimation; _performanceConfigService.Save(); } }, "Generates a decimated copy of models after download.", UIColors.Get("LightlessOrange"), UIColors.Get("LightlessOrange")); DrawControlRow("Decimate above (triangles)", () => { var triangleThreshold = performanceConfig.ModelDecimationTriangleThreshold; ImGui.SetNextItemWidth(-1f); if (ImGui.SliderInt("##model-decimation-threshold", ref triangleThreshold, 1_000, 100_000)) { performanceConfig.ModelDecimationTriangleThreshold = Math.Clamp(triangleThreshold, 1_000, 100_000); _performanceConfigService.Save(); } }, "Models below this triangle count are left untouched. Default: 15,000."); DrawControlRow("Target triangle ratio", () => { 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; _performanceConfigService.Save(); targetPercent = clampedPercent; } ImGui.SetNextItemWidth(-1f); if (ImGui.SliderFloat("##model-decimation-target", ref targetPercent, 60f, 99f, "%.0f%%")) { performanceConfig.ModelDecimationTargetRatio = Math.Clamp(targetPercent / 100f, 0.6f, 0.99f); _performanceConfigService.Save(); } }, "Ratio relative to original triangle count (80% keeps 80%). Default: 80%."); } } ImGui.Dummy(new Vector2(0f, 2f * scale)); DrawGroupHeader("Behavior & Exceptions", UIColors.Get("LightlessOrange")); using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(6f * scale, 2f * scale))) using (var table = ImRaii.Table("model-opt-behavior-table", 3, SettingsTableFlags)) { if (table) { ImGui.TableSetupColumn("Label", ImGuiTableColumnFlags.WidthFixed, 220f * scale); ImGui.TableSetupColumn("Control", ImGuiTableColumnFlags.WidthFixed, 180f * scale); ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); DrawControlRow("Normalize tangents", () => { var normalizeTangents = performanceConfig.ModelDecimationNormalizeTangents; if (ImGui.Checkbox("##model-normalize-tangents", ref normalizeTangents)) { performanceConfig.ModelDecimationNormalizeTangents = normalizeTangents; _performanceConfigService.Save(); } }, "Normalizes tangents to reduce shading artifacts."); DrawControlRow("Keep original model files", () => { var keepOriginalModels = performanceConfig.KeepOriginalModelFiles; if (ImGui.Checkbox("##model-keep-original", ref keepOriginalModels)) { performanceConfig.KeepOriginalModelFiles = keepOriginalModels; _performanceConfigService.Save(); } }, "Keeps the original model alongside the decimated copy."); DrawControlRow("Skip preferred/direct pairs", () => { var skipPreferredDecimation = performanceConfig.SkipModelDecimationForPreferredPairs; if (ImGui.Checkbox("##model-skip-preferred", ref skipPreferredDecimation)) { performanceConfig.SkipModelDecimationForPreferredPairs = skipPreferredDecimation; _performanceConfigService.Save(); } }, "Leaves models untouched for preferred/direct pairs."); } } UiSharedService.ColorTextWrapped( "Note: Disabling \"Keep original model files\" prevents saved/effective triangle usage information.", UIColors.Get("LightlessYellow")); ImGui.Dummy(new Vector2(0f, 2f * scale)); DrawGroupHeader("Decimation Targets", UIColors.Get("LightlessGrey"), "Hair mods are always excluded from decimation."); _uiSharedService.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(".")); _uiSharedService.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(".")); _uiSharedService.DrawNoteLine("!!! ", UIColors.Get("DimRed"), new SeStringUtils.RichTextEntry("Automatic decimation is not perfect and can cause meshes with bad topology to be worse.", UIColors.Get("DimRed"), true)); DrawTargetGrid(performanceConfig); ImGui.Dummy(new Vector2(0f, 4f * scale)); DrawSummaryPanel("Usage Summary", UIColors.Get("LightlessPurple"), DrawTriangleDecimationCounters); } private void DrawTargetGrid(PlayerPerformanceConfig config) { var scale = ImGuiHelpers.GlobalScale; using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(6f * scale, 2f * scale))) using (var table = ImRaii.Table("model-opt-targets", 3, SettingsTableFlags)) { if (!table) { return; } ImGui.TableSetupColumn("Label", ImGuiTableColumnFlags.WidthFixed, 220f * scale); ImGui.TableSetupColumn("Control", ImGuiTableColumnFlags.WidthFixed, 180f * scale); ImGui.TableSetupColumn("Description", ImGuiTableColumnFlags.WidthStretch); DrawControlRow("Body", () => { var allowBody = config.ModelDecimationAllowBody; if (ImGui.Checkbox("##model-target-body", ref allowBody)) { config.ModelDecimationAllowBody = allowBody; _performanceConfigService.Save(); } }, "Body meshes (torso, limbs)."); DrawControlRow("Face/head", () => { var allowFaceHead = config.ModelDecimationAllowFaceHead; if (ImGui.Checkbox("##model-target-facehead", ref allowFaceHead)) { config.ModelDecimationAllowFaceHead = allowFaceHead; _performanceConfigService.Save(); } }, "Face and head meshes."); DrawControlRow("Tails/Ears", () => { var allowTail = config.ModelDecimationAllowTail; if (ImGui.Checkbox("##model-target-tail", ref allowTail)) { config.ModelDecimationAllowTail = allowTail; _performanceConfigService.Save(); } }, "Tail, ear, and similar appendages."); DrawControlRow("Clothing", () => { var allowClothing = config.ModelDecimationAllowClothing; if (ImGui.Checkbox("##model-target-clothing", ref allowClothing)) { config.ModelDecimationAllowClothing = allowClothing; _performanceConfigService.Save(); } }, "Outfits, shoes, gloves, hats."); DrawControlRow("Accessories", () => { var allowAccessories = config.ModelDecimationAllowAccessories; if (ImGui.Checkbox("##model-target-accessories", ref allowAccessories)) { config.ModelDecimationAllowAccessories = allowAccessories; _performanceConfigService.Save(); } }, "Jewelry and small add-ons."); } } private void DrawSectionIntro(FontAwesomeIcon icon, Vector4 color, string title, string subtitle, bool showTitle) { var scale = ImGuiHelpers.GlobalScale; if (showTitle) { using (_uiSharedService.MediumFont.Push()) { _uiSharedService.IconText(icon, color); ImGui.SameLine(0f, 6f * scale); ImGui.TextColored(color, title); } ImGui.TextColored(UIColors.Get("LightlessGrey"), subtitle); } else { _uiSharedService.IconText(icon, color); ImGui.SameLine(0f, 6f * scale); ImGui.TextColored(UIColors.Get("LightlessGrey"), subtitle); } ImGui.Dummy(new Vector2(0f, 2f * scale)); } private void DrawGroupHeader(string title, Vector4 color, string? helpText = null) { using var font = _uiSharedService.MediumFont.Push(); ImGui.TextColored(color, title); if (!string.IsNullOrWhiteSpace(helpText)) { _uiSharedService.DrawHelpText(helpText); } UiSharedService.ColoredSeparator(color, 1.2f); } private void DrawCallout(string id, Vector4 color, Action content) { var scale = ImGuiHelpers.GlobalScale; var bg = new Vector4(color.X, color.Y, color.Z, 0.08f); var border = new Vector4(color.X, color.Y, color.Z, 0.25f); DrawPanelBox(id, bg, border, 6f * scale, new Vector2(10f * scale, 6f * scale), content); } private void DrawSummaryPanel(string title, Vector4 accent, Action content) { var scale = ImGuiHelpers.GlobalScale; var bg = new Vector4(accent.X, accent.Y, accent.Z, 0.06f); var border = new Vector4(accent.X, accent.Y, accent.Z, 0.2f); DrawPanelBox($"summary-{title}", bg, border, 6f * scale, new Vector2(10f * scale, 6f * scale), () => { _uiSharedService.MediumText(title, accent); content(); }); } private void DrawTextureCompressionCard(PlayerPerformanceConfig textureConfig) { var scale = ImGuiHelpers.GlobalScale; var baseColor = UIColors.Get("LightlessGrey"); var bg = new Vector4(baseColor.X, baseColor.Y, baseColor.Z, 0.12f); var border = new Vector4(baseColor.X, baseColor.Y, baseColor.Z, 0.32f); DrawPanelBox("texture-compression-card", bg, border, 6f * scale, new Vector2(10f * scale, 6f * scale), () => { using (ImRaii.PushStyle(ImGuiStyleVar.CellPadding, new Vector2(6f * scale, 2f * scale))) using (var table = ImRaii.Table("texture-opt-compress-card", 2, SettingsTableFlags)) { if (!table) { return; } ImGui.TableSetupColumn("Label", ImGuiTableColumnFlags.WidthFixed, 220f * scale); ImGui.TableSetupColumn("Control", ImGuiTableColumnFlags.WidthStretch); DrawInlineDescriptionRow("Compress uncompressed textures", () => { var autoCompress = textureConfig.EnableUncompressedTextureCompression; if (UiSharedService.CheckboxWithBorder("##texture-auto-compress", ref autoCompress, baseColor)) { textureConfig.EnableUncompressedTextureCompression = autoCompress; _performanceConfigService.Save(); } }, "Converts uncompressed textures to BC formats based on map type (heavy). Runs after downscale/mip trim.", drawLabelSuffix: () => { _uiSharedService.IconText(FontAwesomeIcon.ExclamationTriangle, UIColors.Get("LightlessYellow")); UiSharedService.AttachToolTip("This feature can be demanding and will increase character load times."); }); DrawInlineDescriptionRow("Skip mipmaps for auto-compress", () => { var skipMipMaps = textureConfig.SkipUncompressedTextureCompressionMipMaps; if (UiSharedService.CheckboxWithBorder("##texture-auto-compress-skip-mips", ref skipMipMaps, baseColor)) { textureConfig.SkipUncompressedTextureCompressionMipMaps = skipMipMaps; _performanceConfigService.Save(); } }, "Skips mipmap generation to speed up compression, but can cause shimmering.", disableControl: !textureConfig.EnableUncompressedTextureCompression); } }); } private void DrawInlineDescriptionRow( string label, Action drawControl, string description, Action? drawLabelSuffix = null, bool disableControl = false) { var scale = ImGuiHelpers.GlobalScale; ImGui.TableNextRow(); ImGui.TableSetColumnIndex(0); ImGui.AlignTextToFramePadding(); ImGui.TextUnformatted(label); if (drawLabelSuffix != null) { ImGui.SameLine(0f, 4f * scale); drawLabelSuffix(); } ImGui.TableSetColumnIndex(1); using (ImRaii.Disabled(disableControl)) { drawControl(); } ImGui.SameLine(0f, 8f * scale); using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("LightlessGrey"))) { ImGui.PushTextWrapPos(ImGui.GetCursorPos().X + ImGui.GetContentRegionAvail().X); ImGui.TextUnformatted(description); ImGui.PopTextWrapPos(); } } private void DrawControlRow(string label, Action drawControl, string description, Vector4? labelColor = null, Vector4? cardAccent = null, Action? drawLabelSuffix = null) { var scale = ImGuiHelpers.GlobalScale; if (!cardAccent.HasValue) { ImGui.TableNextRow(); ImGui.TableSetColumnIndex(0); ImGui.AlignTextToFramePadding(); using var labelTint = ImRaii.PushColor(ImGuiCol.Text, labelColor ?? Vector4.Zero, labelColor.HasValue); ImGui.TextUnformatted(label); if (drawLabelSuffix != null) { ImGui.SameLine(0f, 4f * scale); drawLabelSuffix(); } ImGui.TableSetColumnIndex(1); drawControl(); ImGui.TableSetColumnIndex(2); using var color = ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("LightlessGrey")); ImGui.TextWrapped(description); return; } var padX = 6f * scale; var padY = 3f * scale; var rowGap = 4f * scale; var accent = cardAccent.Value; var drawList = ImGui.GetWindowDrawList(); ImGui.TableNextRow(); ImGui.TableSetColumnIndex(0); var col0Start = ImGui.GetCursorScreenPos(); ImGui.TableSetColumnIndex(1); var col1Start = ImGui.GetCursorScreenPos(); ImGui.TableSetColumnIndex(2); var col2Start = ImGui.GetCursorScreenPos(); var col2Width = ImGui.GetContentRegionAvail().X; float descriptionHeight; using (ImRaii.PushColor(ImGuiCol.Text, new Vector4(0f, 0f, 0f, 0f))) { ImGui.SetCursorScreenPos(col2Start); ImGui.PushTextWrapPos(ImGui.GetCursorPos().X + col2Width); ImGui.TextUnformatted(description); ImGui.PopTextWrapPos(); descriptionHeight = ImGui.GetItemRectSize().Y; } var lineHeight = ImGui.GetTextLineHeight(); var labelHeight = lineHeight; var controlHeight = ImGui.GetFrameHeight(); var contentHeight = MathF.Max(labelHeight, MathF.Max(controlHeight, descriptionHeight)); var lineCount = Math.Max(1, (int)MathF.Round(descriptionHeight / MathF.Max(1f, lineHeight))); var descOffset = lineCount > 1 ? lineHeight * 0.18f : 0f; var cardTop = col0Start.Y; var contentTop = cardTop + padY; var cardHeight = contentHeight + (padY * 2f); var labelY = contentTop + (contentHeight - labelHeight) * 0.5f; var controlY = contentTop + (contentHeight - controlHeight) * 0.5f; var descY = contentTop + (contentHeight - descriptionHeight) * 0.5f - descOffset; drawList.ChannelsSplit(2); drawList.ChannelsSetCurrent(1); ImGui.TableSetColumnIndex(0); ImGui.SetCursorScreenPos(new Vector2(col0Start.X, labelY)); using (ImRaii.PushColor(ImGuiCol.Text, labelColor ?? Vector4.Zero, labelColor.HasValue)) { ImGui.TextUnformatted(label); if (drawLabelSuffix != null) { ImGui.SameLine(0f, 4f * scale); drawLabelSuffix(); } } ImGui.TableSetColumnIndex(1); ImGui.SetCursorScreenPos(new Vector2(col1Start.X, controlY)); drawControl(); ImGui.TableSetColumnIndex(2); ImGui.SetCursorScreenPos(new Vector2(col2Start.X, descY)); using (ImRaii.PushColor(ImGuiCol.Text, UIColors.Get("LightlessGrey"))) { ImGui.PushTextWrapPos(ImGui.GetCursorPos().X + col2Width); ImGui.TextUnformatted(description); ImGui.PopTextWrapPos(); } var rectMin = new Vector2(col0Start.X - padX, cardTop); var rectMax = new Vector2(col2Start.X + col2Width + padX, cardTop + cardHeight); var fill = new Vector4(accent.X, accent.Y, accent.Z, 0.07f); var border = new Vector4(accent.X, accent.Y, accent.Z, 0.35f); var rounding = MathF.Max(5f, ImGui.GetStyle().FrameRounding) * scale; var borderThickness = MathF.Max(1f, ImGui.GetStyle().ChildBorderSize); var clipMin = drawList.GetClipRectMin(); var clipMax = drawList.GetClipRectMax(); clipMin.X = MathF.Min(clipMin.X, rectMin.X); clipMax.X = MathF.Max(clipMax.X, rectMax.X); drawList.ChannelsSetCurrent(0); drawList.PushClipRect(clipMin, clipMax, false); drawList.AddRectFilled(rectMin, rectMax, UiSharedService.Color(fill), rounding); drawList.AddRect(rectMin, rectMax, UiSharedService.Color(border), rounding, ImDrawFlags.None, borderThickness); drawList.PopClipRect(); drawList.ChannelsMerge(); ImGui.TableSetColumnIndex(2); ImGui.SetCursorScreenPos(new Vector2(col2Start.X, cardTop + cardHeight)); ImGui.Dummy(new Vector2(0f, rowGap)); } private static bool DrawAccentCheckbox(string id, ref bool value, Vector4 accent) { var frame = new Vector4(accent.X, accent.Y, accent.Z, 0.14f); var frameHovered = new Vector4(accent.X, accent.Y, accent.Z, 0.22f); var frameActive = new Vector4(accent.X, accent.Y, accent.Z, 0.3f); bool changed; using (ImRaii.PushColor(ImGuiCol.CheckMark, accent)) using (ImRaii.PushColor(ImGuiCol.FrameBg, frame)) using (ImRaii.PushColor(ImGuiCol.FrameBgHovered, frameHovered)) using (ImRaii.PushColor(ImGuiCol.FrameBgActive, frameActive)) { changed = ImGui.Checkbox(id, ref value); } return changed; } private static void DrawPanelBox(string id, Vector4 background, Vector4 border, float rounding, Vector2 padding, Action content) { using (ImRaii.PushId(id)) { var startPos = ImGui.GetCursorScreenPos(); var availableWidth = ImGui.GetContentRegionAvail().X; var drawList = ImGui.GetWindowDrawList(); drawList.ChannelsSplit(2); drawList.ChannelsSetCurrent(1); using (ImRaii.Group()) { ImGui.Dummy(new Vector2(0f, padding.Y)); ImGui.Indent(padding.X); content(); ImGui.Unindent(padding.X); ImGui.Dummy(new Vector2(0f, padding.Y)); } var rectMin = startPos; var rectMax = new Vector2(startPos.X + availableWidth, ImGui.GetItemRectMax().Y); var borderThickness = MathF.Max(1f, ImGui.GetStyle().ChildBorderSize); drawList.ChannelsSetCurrent(0); drawList.AddRectFilled(rectMin, rectMax, UiSharedService.Color(background), rounding); drawList.AddRect(rectMin, rectMax, UiSharedService.Color(border), rounding, ImDrawFlags.None, borderThickness); drawList.ChannelsMerge(); } } private void DrawTextureDownscaleCounters() { HashSet 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 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}"); } } 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 tris"); } if (triangleCount >= 1_000) { return FormattableString.Invariant($"{triangleCount / 1_000d:0.#}k tris"); } return $"{triangleCount} tris"; } }