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; DrawModelDecimationCard(performanceConfig); ImGui.Dummy(new Vector2(0f, 2f * scale)); DrawGroupHeader("Behavior & Exceptions", UIColors.Get("LightlessOrange")); DrawModelBehaviorCard(performanceConfig); 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); const string bodyDesc = "Body meshes (torso, limbs)."; DrawControlRow("Body", () => { var allowBody = config.ModelDecimationAllowBody; if (ImGui.Checkbox("##model-target-body", ref allowBody)) { config.ModelDecimationAllowBody = allowBody; _performanceConfigService.Save(); } if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) { config.ModelDecimationAllowBody = ModelDecimationDefaults.AllowBody; _performanceConfigService.Save(); } UiSharedService.AttachToolTip($"{bodyDesc}\nRight-click to reset to default ({(ModelDecimationDefaults.AllowBody ? "On" : "Off")})."); }, bodyDesc); const string faceDesc = "Face and head meshes."; DrawControlRow("Face/head", () => { var allowFaceHead = config.ModelDecimationAllowFaceHead; if (ImGui.Checkbox("##model-target-facehead", ref allowFaceHead)) { config.ModelDecimationAllowFaceHead = allowFaceHead; _performanceConfigService.Save(); } if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) { config.ModelDecimationAllowFaceHead = ModelDecimationDefaults.AllowFaceHead; _performanceConfigService.Save(); } UiSharedService.AttachToolTip($"{faceDesc}\nRight-click to reset to default ({(ModelDecimationDefaults.AllowFaceHead ? "On" : "Off")})."); }, faceDesc); const string tailDesc = "Tail, ear, and similar appendages."; DrawControlRow("Tails/Ears", () => { var allowTail = config.ModelDecimationAllowTail; if (ImGui.Checkbox("##model-target-tail", ref allowTail)) { config.ModelDecimationAllowTail = allowTail; _performanceConfigService.Save(); } if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) { config.ModelDecimationAllowTail = ModelDecimationDefaults.AllowTail; _performanceConfigService.Save(); } UiSharedService.AttachToolTip($"{tailDesc}\nRight-click to reset to default ({(ModelDecimationDefaults.AllowTail ? "On" : "Off")})."); }, tailDesc); const string clothingDesc = "Outfits, shoes, gloves, hats."; DrawControlRow("Clothing", () => { var allowClothing = config.ModelDecimationAllowClothing; if (ImGui.Checkbox("##model-target-clothing", ref allowClothing)) { config.ModelDecimationAllowClothing = allowClothing; _performanceConfigService.Save(); } if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) { config.ModelDecimationAllowClothing = ModelDecimationDefaults.AllowClothing; _performanceConfigService.Save(); } UiSharedService.AttachToolTip($"{clothingDesc}\nRight-click to reset to default ({(ModelDecimationDefaults.AllowClothing ? "On" : "Off")})."); }, clothingDesc); const string accessoryDesc = "Jewelry and small add-ons."; DrawControlRow("Accessories", () => { var allowAccessories = config.ModelDecimationAllowAccessories; if (ImGui.Checkbox("##model-target-accessories", ref allowAccessories)) { config.ModelDecimationAllowAccessories = allowAccessories; _performanceConfigService.Save(); } if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) { config.ModelDecimationAllowAccessories = ModelDecimationDefaults.AllowAccessories; _performanceConfigService.Save(); } UiSharedService.AttachToolTip($"{accessoryDesc}\nRight-click to reset to default ({(ModelDecimationDefaults.AllowAccessories ? "On" : "Off")})."); }, accessoryDesc); } } 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 DrawModelDecimationCard(PlayerPerformanceConfig performanceConfig) { var scale = ImGuiHelpers.GlobalScale; var accent = UIColors.Get("LightlessOrange"); var bg = new Vector4(accent.X, accent.Y, accent.Z, 0.12f); var border = new Vector4(accent.X, accent.Y, accent.Z, 0.32f); const string enableDesc = "Generates a decimated copy of models after download."; const string thresholdDesc = "Models below this triangle count are left untouched. Default: 15,000."; const string ratioDesc = "Ratio relative to original triangle count (80% keeps 80%). Default: 80%."; DrawPanelBox("model-decimation-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("model-opt-core-card", 2, SettingsTableFlags)) { if (!table) { return; } ImGui.TableSetupColumn("Label", ImGuiTableColumnFlags.WidthFixed, 220f * scale); ImGui.TableSetupColumn("Control", ImGuiTableColumnFlags.WidthStretch); DrawInlineDescriptionRow("Enable model decimation", () => { var enableDecimation = performanceConfig.EnableModelDecimation; if (DrawAccentCheckbox("##enable-model-decimation", ref enableDecimation, accent)) { performanceConfig.EnableModelDecimation = enableDecimation; _performanceConfigService.Save(); } if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) { performanceConfig.EnableModelDecimation = ModelDecimationDefaults.EnableAutoDecimation; _performanceConfigService.Save(); } UiSharedService.AttachToolTip($"{enableDesc}\nRight-click to reset to default ({(ModelDecimationDefaults.EnableAutoDecimation ? "On" : "Off")})."); }, enableDesc); DrawInlineDescriptionRow("Decimate above (triangles)", () => { var triangleThreshold = performanceConfig.ModelDecimationTriangleThreshold; ImGui.SetNextItemWidth(220f * scale); if (ImGui.SliderInt("##model-decimation-threshold", ref triangleThreshold, 1_000, 100_000)) { performanceConfig.ModelDecimationTriangleThreshold = Math.Clamp(triangleThreshold, 1_000, 100_000); _performanceConfigService.Save(); } if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) { performanceConfig.ModelDecimationTriangleThreshold = ModelDecimationDefaults.TriangleThreshold; _performanceConfigService.Save(); } UiSharedService.AttachToolTip($"{thresholdDesc}\nRight-click to reset to default ({ModelDecimationDefaults.TriangleThreshold:N0})."); }, thresholdDesc); DrawInlineDescriptionRow("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(220f * scale); if (ImGui.SliderFloat("##model-decimation-target", ref targetPercent, 60f, 99f, "%.0f%%")) { performanceConfig.ModelDecimationTargetRatio = Math.Clamp(targetPercent / 100f, 0.6f, 0.99f); _performanceConfigService.Save(); } if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) { performanceConfig.ModelDecimationTargetRatio = ModelDecimationDefaults.TargetRatio; _performanceConfigService.Save(); } UiSharedService.AttachToolTip($"{ratioDesc}\nRight-click to reset to default ({ModelDecimationDefaults.TargetRatio * 100:0}%)."); }, ratioDesc); } }); } private void DrawModelBehaviorCard(PlayerPerformanceConfig performanceConfig) { 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); const string normalizeDesc = "Normalizes tangents to reduce shading artifacts."; const string avoidBodyDesc = "Uses body materials as a collision guard to reduce clothing clipping. Slower and may reduce decimation."; const string keepOriginalDesc = "Keeps the original model alongside the decimated copy."; const string skipPreferredDesc = "Leaves models untouched for preferred/direct pairs."; DrawPanelBox("model-behavior-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("model-opt-behavior-card", 2, SettingsTableFlags)) { if (!table) { return; } ImGui.TableSetupColumn("Label", ImGuiTableColumnFlags.WidthFixed, 220f * scale); ImGui.TableSetupColumn("Control", ImGuiTableColumnFlags.WidthStretch); DrawInlineDescriptionRow("Normalize tangents", () => { var normalizeTangents = performanceConfig.ModelDecimationNormalizeTangents; if (UiSharedService.CheckboxWithBorder("##model-normalize-tangents", ref normalizeTangents, baseColor)) { performanceConfig.ModelDecimationNormalizeTangents = normalizeTangents; _performanceConfigService.Save(); } if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) { performanceConfig.ModelDecimationNormalizeTangents = ModelDecimationDefaults.NormalizeTangents; _performanceConfigService.Save(); } UiSharedService.AttachToolTip($"{normalizeDesc}\nRight-click to reset to default ({(ModelDecimationDefaults.NormalizeTangents ? "On" : "Off")})."); }, normalizeDesc); DrawInlineDescriptionRow("Avoid body intersection", () => { var avoidBodyIntersection = performanceConfig.ModelDecimationAvoidBodyIntersection; if (UiSharedService.CheckboxWithBorder("##model-body-collision", ref avoidBodyIntersection, baseColor)) { performanceConfig.ModelDecimationAvoidBodyIntersection = avoidBodyIntersection; _performanceConfigService.Save(); } if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) { performanceConfig.ModelDecimationAvoidBodyIntersection = ModelDecimationDefaults.AvoidBodyIntersection; _performanceConfigService.Save(); } UiSharedService.AttachToolTip($"{avoidBodyDesc}\nRight-click to reset to default ({(ModelDecimationDefaults.AvoidBodyIntersection ? "On" : "Off")})."); }, avoidBodyDesc); DrawInlineDescriptionRow("Keep original model files", () => { var keepOriginalModels = performanceConfig.KeepOriginalModelFiles; if (UiSharedService.CheckboxWithBorder("##model-keep-original", ref keepOriginalModels, baseColor)) { performanceConfig.KeepOriginalModelFiles = keepOriginalModels; _performanceConfigService.Save(); } if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) { performanceConfig.KeepOriginalModelFiles = ModelDecimationDefaults.KeepOriginalModelFiles; _performanceConfigService.Save(); } UiSharedService.AttachToolTip($"{keepOriginalDesc}\nRight-click to reset to default ({(ModelDecimationDefaults.KeepOriginalModelFiles ? "On" : "Off")})."); }, keepOriginalDesc); DrawInlineDescriptionRow("Skip preferred/direct pairs", () => { var skipPreferredDecimation = performanceConfig.SkipModelDecimationForPreferredPairs; if (UiSharedService.CheckboxWithBorder("##model-skip-preferred", ref skipPreferredDecimation, baseColor)) { performanceConfig.SkipModelDecimationForPreferredPairs = skipPreferredDecimation; _performanceConfigService.Save(); } if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) { performanceConfig.SkipModelDecimationForPreferredPairs = ModelDecimationDefaults.SkipPreferredPairs; _performanceConfigService.Save(); } UiSharedService.AttachToolTip($"{skipPreferredDesc}\nRight-click to reset to default ({(ModelDecimationDefaults.SkipPreferredPairs ? "On" : "Off")})."); }, skipPreferredDesc); } }); } 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"; } }