From 1177ec82a7ff6b4812520a74f7f3991c81c7b777 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 26 Apr 2026 19:41:26 -0700 Subject: [PATCH 1/6] feat(menu): add simple mode quality preset system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a shared Preset template on Feature plus a global QualityLevel enum for the simple menu. SSGI declares Low/Medium/High tiers (with VR variants) via the new system. WetnessEffects refactored so its existing ClimatePreset combo routes through the same Feature::ApplyPreset dispatch via one shared thunk. Simple mode hides the feature list and renders a single scrollable page. Each loaded feature renders inline as a slider (Off / preset names / Custom) when it has quality presets, or as a checkbox otherwise. The on/off uses the runtime GetEnabledFlag() when overridden, otherwise falls back to ToggleAtBootSetting() — features on the boot path get their label tinted with StatusPalette.RestartNeeded and a "(restart required)" hint. Features that failed to load appear under "Installation Issues" colored with StatusPalette.Error and the failure message in the tooltip. Co-Authored-By: Claude Opus 4.7 --- src/Feature.cpp | 100 +++++++++++++++++++++++++++++++ src/Feature.h | 81 +++++++++++++++++++++++++ src/Features/ScreenSpaceGI.cpp | 43 +++++++++++++ src/Features/ScreenSpaceGI.h | 3 + src/Features/WetnessEffects.cpp | 96 +++++++++++++---------------- src/Menu.cpp | 1 + src/Menu.h | 1 + src/Menu/FeatureListRenderer.cpp | 83 ++++++++++++++++++++++++- src/Menu/SettingsTabRenderer.cpp | 7 +++ 9 files changed, 361 insertions(+), 54 deletions(-) diff --git a/src/Feature.cpp b/src/Feature.cpp index 0a5f8010f1..d1620107db 100644 --- a/src/Feature.cpp +++ b/src/Feature.cpp @@ -334,6 +334,106 @@ bool Feature::ReapplyOverrideSettings() return false; } +void Feature::DrawSimpleSettings() +{ + bool* runtimeEnabled = GetEnabledFlag(); + auto presets = GetQualityPresets(); + const std::string shortName = GetShortName(); + + // On/off uses the runtime flag when available, otherwise the boot-toggle (every + // feature supports it). Boot path requires a restart; only mark it as such once + // the user actually flips it from the value captured at first render. + const bool runtimeAvailable = runtimeEnabled != nullptr; + const bool currentlyOn = runtimeAvailable ? + *runtimeEnabled : + !globals::state->IsFeatureDisabled(shortName); + + if (!runtimeAvailable && !simpleModeBootCaptured) { + simpleModeBootInitial = globals::state->IsFeatureDisabled(shortName); + simpleModeBootCaptured = true; + } + const bool restartPending = !runtimeAvailable && + globals::state->IsFeatureDisabled(shortName) != simpleModeBootInitial; + + const auto& palette = globals::menu->GetTheme().StatusPalette; + if (restartPending) + ImGui::PushStyleColor(ImGuiCol_Text, palette.RestartNeeded); + + // Slider label includes the version (e.g. "Screen Space GI v2.1.0"). + std::string label = GetName(); + if (!version.empty()) { + label += " v"; + label += version; + } + + const int n = static_cast(presets.size()); + const int sliderMax = std::max(1, n); + int currentValue; + std::string overlay; + if (!currentlyOn) { + currentValue = 0; + overlay = "Off"; + } else if (n == 0) { + currentValue = 1; + overlay = "On"; + } else if (lastAppliedQualityIdx >= 0 && lastAppliedQualityIdx < n) { + currentValue = lastAppliedQualityIdx + 1; + overlay.assign(presets[lastAppliedQualityIdx].label.data(), presets[lastAppliedQualityIdx].label.size()); + } else { + currentValue = 1; + overlay = "Custom"; + } + + int v = currentValue; + if (ImGui::SliderInt(label.c_str(), &v, 0, sliderMax, overlay.c_str(), ImGuiSliderFlags_AlwaysClamp)) { + const bool wantOn = v >= 1; + if (currentlyOn != wantOn) { + if (runtimeAvailable) + *runtimeEnabled = wantOn; + else + ToggleAtBootSetting(); + } + if (wantOn && n > 0) { + const int idx = v - 1; + if (idx >= 0 && idx < n) { + ApplyPreset(this, presets[idx]); + lastAppliedQualityIdx = idx; + } + } else if (!wantOn) { + lastAppliedQualityIdx = -1; + } + } + + if (auto _tt = Util::HoverTooltipWrapper()) { + auto [description, keyFeatures] = GetFeatureSummary(); + if (!description.empty()) + ImGui::TextWrapped("%s", description.c_str()); + if (!keyFeatures.empty()) { + if (!description.empty()) + ImGui::Spacing(); + ImGui::TextUnformatted("Key features:"); + for (const auto& k : keyFeatures) + ImGui::BulletText("%s", k.c_str()); + } + if (currentValue >= 1 && n > 0) { + const auto& desc = presets[currentValue - 1].description; + if (!desc.empty()) { + ImGui::Separator(); + ImGui::TextWrapped("%s: %.*s", overlay.c_str(), + static_cast(desc.size()), desc.data()); + } + } + if (!runtimeAvailable) { + ImGui::Separator(); + ImGui::TextColored(palette.RestartNeeded, + "Toggling Off/On for this feature requires a game restart."); + } + } + + if (restartPending) + ImGui::PopStyleColor(); +} + void Feature::DrawUnloadedUI() { // Prioritize detailed failure message if available diff --git a/src/Feature.h b/src/Feature.h index 95ca1741a7..1bf5430464 100644 --- a/src/Feature.h +++ b/src/Feature.h @@ -10,6 +10,60 @@ struct Feature { + // Generic named-preset entry. The enum type E is feature-specific; the surrounding + // shape is shared so SSGI's QualityPreset and WetnessEffects::ClimatePreset use + // the same dispatch. + template + struct Preset + { + E id; + std::string_view label; + std::string_view description; + // apply receives the preset id so one shared thunk can dispatch all entries + // of an enum. nullptr apply is a no-op (e.g. a "Custom" sentinel). + void (*apply)(Feature*, E) = nullptr; + // Optional VR variant; falls back to apply when null. + void (*vrApply)(Feature*, E) = nullptr; + }; + + // Quality tiers for the simple menu. Authors declare a sparse subset; missing + // tiers don't appear in the UI. + enum class QualityLevel : uint8_t + { + Low, + Medium, + High, + Ultra + }; + using QualityPreset = Preset; + + // Apply a preset, picking vrApply on VR when available. + template + static void ApplyPreset(Feature* feature, const Preset& preset) + { + if (preset.vrApply && REL::Module::IsVR()) + preset.vrApply(feature, preset.id); + else if (preset.apply) + preset.apply(feature, preset.id); + } + + // Apply a quality tier without going through ImGui (controller menu, scripts). + // Returns false if the feature does not expose that tier. + bool ApplyQualityPreset(QualityLevel level) + { + auto presets = GetQualityPresets(); + for (size_t i = 0; i < presets.size(); ++i) { + if (presets[i].id == level) { + ApplyPreset(this, presets[i]); + if (auto* en = GetEnabledFlag()) + *en = true; + lastAppliedQualityIdx = static_cast(i); + return true; + } + } + return false; + } + // For global settings search struct SettingSearchEntry { @@ -24,6 +78,13 @@ struct Feature // Nexus Mods base URL for Skyrim Special Edition static constexpr std::string_view NEXUS_BASE_URL = "https://www.nexusmods.com/skyrimspecialedition/mods/"; bool loaded = false; + // The two simple-mode bools fit in the natural padding between `loaded` and + // `lastAppliedQualityIdx`; the int then fills the remaining padding before the + // 8-byte-aligned std::string. Reordering this group triggers C4324 in alignas(16) + // derived classes. + bool simpleModeBootCaptured = false; + bool simpleModeBootInitial = false; // snapshot of IsFeatureDisabled at first render + int lastAppliedQualityIdx = -1; // -1 = unknown/Custom std::string version; std::string failedLoadedMessage; @@ -91,6 +152,26 @@ struct Feature virtual void DrawSettings() {} virtual void DrawUnloadedUI(); + /** + * Quality preset list for the global Simple menu. + * Default: empty — feature renders just an Enable toggle (or nothing if no enable flag). + * Override and return a span over a `static constexpr std::array` to + * expose Off/Low/Medium/High etc. Missing tiers are skipped gracefully. + */ + virtual std::span GetQualityPresets() const { return {}; } + + /** + * Pointer to the feature's `Enabled` bool, used by the Simple menu's Off button. + * Return nullptr if the feature has no enable toggle (in which case Off is hidden). + */ + virtual bool* GetEnabledFlag() { return nullptr; } + + /** + * Render the Simple-mode settings UI: Off button + quality preset row. + * Implemented in Feature.cpp; features generally do not need to override. + */ + void DrawSimpleSettings(); + virtual void ReflectionsPrepass() {}; virtual void Prepass() {} virtual void EarlyPrepass() {} diff --git a/src/Features/ScreenSpaceGI.cpp b/src/Features/ScreenSpaceGI.cpp index 5a021c3cd8..6186957365 100644 --- a/src/Features/ScreenSpaceGI.cpp +++ b/src/Features/ScreenSpaceGI.cpp @@ -40,6 +40,49 @@ void ScreenSpaceGI::RestoreDefaultSettings() recompileFlag = true; } +namespace +{ + // Per-tier values applied by ApplyTier. + struct QualityTier + { + uint numSlices; + uint numSteps; + int resolutionMode; + bool enableGI; + }; + + void ApplyTier(Feature* f, const QualityTier& q) + { + auto* ssgi = static_cast(f); + ssgi->settings.Enabled = true; + ssgi->settings.NumSlices = q.numSlices; + ssgi->settings.NumSteps = q.numSteps; + ssgi->settings.ResolutionMode = q.resolutionMode; + ssgi->settings.EnableBlur = true; + ssgi->settings.EnableGI = q.enableGI; + ssgi->recompileFlag = true; + } + + // VR variants force AO-only (EnableGI=false) for performance; vrApply is null where + // the SE/AE tuning is acceptable on VR and ApplyPreset falls back to apply. + static constexpr std::array kQualityPresets = { { + { Feature::QualityLevel::Low, "Low", "Quarter resolution, blurred. AO + basic GI.", + [](Feature* f, Feature::QualityLevel) { ApplyTier(f, { 10, 12, 2, true }); }, + [](Feature* f, Feature::QualityLevel) { ApplyTier(f, { 1, 6, 2, false }); } }, + { Feature::QualityLevel::Medium, "Medium", "Half resolution, balanced quality and performance.", + [](Feature* f, Feature::QualityLevel) { ApplyTier(f, { 4, 8, 1, true }); }, + [](Feature* f, Feature::QualityLevel) { ApplyTier(f, { 3, 8, 1, false }); } }, + { Feature::QualityLevel::High, "High", "Full resolution, clean output with full GI.", + [](Feature* f, Feature::QualityLevel) { ApplyTier(f, { 4, 8, 0, true }); }, + nullptr }, + } }; +} + +std::span ScreenSpaceGI::GetQualityPresets() const +{ + return kQualityPresets; +} + void ScreenSpaceGI::DrawSettings() { static bool showAdvanced; diff --git a/src/Features/ScreenSpaceGI.h b/src/Features/ScreenSpaceGI.h index 45c0c1a03e..ed8ceecf0d 100644 --- a/src/Features/ScreenSpaceGI.h +++ b/src/Features/ScreenSpaceGI.h @@ -40,6 +40,9 @@ struct ScreenSpaceGI : Feature virtual void RestoreDefaultSettings() override; virtual void DrawSettings() override; + virtual std::span GetQualityPresets() const override; + virtual bool* GetEnabledFlag() override { return &settings.Enabled; } + virtual void LoadSettings(json& o_json) override; virtual void SaveSettings(json& o_json) override; diff --git a/src/Features/WetnessEffects.cpp b/src/Features/WetnessEffects.cpp index dd077abf04..c6b695da26 100644 --- a/src/Features/WetnessEffects.cpp +++ b/src/Features/WetnessEffects.cpp @@ -45,10 +45,17 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( // Climate preset data - defines regional weather characteristics // Precipitation rates calculated from actual shader mechanics: grid size, interval, and raindrop chance +// Shared dispatch for all CLIMATE_PRESET_INFO entries. +static void ClimateApplyThunk(Feature* f, WetnessEffects::ClimatePreset id) +{ + static_cast(f)->ApplyClimatePreset(id); +} + +// Embeds Feature::Preset for shared label/description/apply, alongside +// Wetness-specific rich metadata used by the climate analysis UI. struct ClimatePresetInfo { - const char* name; - const char* shortDescription; + Feature::Preset preset; const char* const* detailedDescription; const char* const* effectDescription; WetnessEffects::ClimateSettings settings; @@ -137,48 +144,33 @@ static constexpr const char* MONSOON_EFFECTS[] = { nullptr }; -static constexpr std::array CLIMATE_PRESET_INFO = { - { { "Custom", - "User-defined custom settings", - nullptr, - nullptr, - { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f } }, - // Legacy (Original Skyrim) - { - "Legacy", - "Original rain effect values (very light)", - LEGACY_DETAILED, - LEGACY_EFFECTS, - { 1.0f, 1.0f, 1.0f, 0.3f, 4.0f, 0.5f } }, - // Nordic Standard - { - "Nordic (Default)", - "Balanced Nordic climate (moderate rain)", - NORDIC_DETAILED, - NORDIC_EFFECTS, - { 1.0f, 1.0f, 1.0f, 1.0f, 3.0f, 1.0f } }, - // Arctic Tundra - { - "Arctic Tundra", - "Cold, dry Arctic climate (light rain)", - ARCTIC_DETAILED, - ARCTIC_EFFECTS, - { 0.5f, 0.3f, 0.5f, 0.3f, 3.5f, 0.4f } }, - // Temperate Coastal - { - "Temperate Coastal", - "Maritime climate (heavy rain)", - COASTAL_DETAILED, - COASTAL_EFFECTS, - { 1.5f, 1.7f, 1.7f, 0.8f, 2.5f, 0.25f } }, - // Monsoon/Extreme - { - "Monsoon/Extreme", - "Extreme monsoon climate (extreme rain)", - MONSOON_DETAILED, - MONSOON_EFFECTS, - { 2.0f, 2.5f, 2.0f, 1.0f, 2.0f, 0.2f } } } -}; +static constexpr std::array CLIMATE_PRESET_INFO = { { + // Custom is a sentinel — apply=nullptr means "no-op" (user has customized values) + { { WetnessEffects::ClimatePreset::Custom, "Custom", "User-defined custom settings", nullptr, nullptr }, + nullptr, + nullptr, + { 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f } }, + { { WetnessEffects::ClimatePreset::Legacy, "Legacy", "Original rain effect values (very light)", &ClimateApplyThunk, nullptr }, + LEGACY_DETAILED, + LEGACY_EFFECTS, + { 1.0f, 1.0f, 1.0f, 0.3f, 4.0f, 0.5f } }, + { { WetnessEffects::ClimatePreset::NordicStandard, "Nordic (Default)", "Balanced Nordic climate (moderate rain)", &ClimateApplyThunk, nullptr }, + NORDIC_DETAILED, + NORDIC_EFFECTS, + { 1.0f, 1.0f, 1.0f, 1.0f, 3.0f, 1.0f } }, + { { WetnessEffects::ClimatePreset::ArcticTundra, "Arctic Tundra", "Cold, dry Arctic climate (light rain)", &ClimateApplyThunk, nullptr }, + ARCTIC_DETAILED, + ARCTIC_EFFECTS, + { 0.5f, 0.3f, 0.5f, 0.3f, 3.5f, 0.4f } }, + { { WetnessEffects::ClimatePreset::TemperateCoastal, "Temperate Coastal", "Maritime climate (heavy rain)", &ClimateApplyThunk, nullptr }, + COASTAL_DETAILED, + COASTAL_EFFECTS, + { 1.5f, 1.7f, 1.7f, 0.8f, 2.5f, 0.25f } }, + { { WetnessEffects::ClimatePreset::MonsoonExtreme, "Monsoon/Extreme", "Extreme monsoon climate (extreme rain)", &ClimateApplyThunk, nullptr }, + MONSOON_DETAILED, + MONSOON_EFFECTS, + { 2.0f, 2.5f, 2.0f, 1.0f, 2.0f, 0.2f } }, +} }; // Extract just the settings for the actual climate preset array static const std::array CLIMATE_PRESETS = { { @@ -257,10 +249,10 @@ void WetnessEffects::DrawSettings() ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0.2f, 0.3f, 0.4f, 0.6f)); // Subtle blue background ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.25f, 0.35f, 0.45f, 0.8f)); // Slightly darker for button - // Extract names for combo box + // Extract names for combo box. Labels come from string literals so .data() is null-terminated. const char* presetNames[CLIMATE_PRESET_INFO.size()]; for (size_t i = 0; i < CLIMATE_PRESET_INFO.size(); ++i) { - presetNames[i] = CLIMATE_PRESET_INFO[i].name; + presetNames[i] = CLIMATE_PRESET_INFO[i].preset.label.data(); } // Map preset enum to combo index (Custom=0, Legacy=1, Nordic=2, Arctic=3, Coastal=4, Monsoon=5) int currentComboIndex = static_cast(climatePreset); @@ -272,10 +264,8 @@ void WetnessEffects::DrawSettings() // Update the preset selection climatePreset = newPreset; - // Apply preset settings (but not for Custom, which just means user-modified) - if (newPreset != ClimatePreset::Custom) { - ApplyClimatePreset(newPreset); - } + // Route through shared dispatch — Custom's apply is null and becomes a no-op. + Feature::ApplyPreset(this, CLIMATE_PRESET_INFO[static_cast(newPreset)].preset); } ImGui::PopStyleColor(2); // Pop both style colors @@ -290,7 +280,7 @@ void WetnessEffects::DrawSettings() } else { // Build combined description lines for actual presets std::vector tooltipLines; - tooltipLines.push_back(info.shortDescription); + tooltipLines.push_back(info.preset.description.data()); // Add detailed description for (const char* const* line = info.detailedDescription; *line != nullptr; ++line) { tooltipLines.push_back(*line); @@ -875,9 +865,9 @@ void WetnessEffects::DrawWeatherAnalysis() const // const auto& climate = GetClimateSettings(climatePreset); // Unused, remove to fix warning treated as error const auto& presetInfo = CLIMATE_PRESET_INFO[static_cast(climatePreset)]; - ImGui::Text("Active Preset: %s", presetInfo.name); + ImGui::Text("Active Preset: %s", presetInfo.preset.label.data()); if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("%s", presetInfo.shortDescription); + ImGui::Text("%s", presetInfo.preset.description.data()); } ImGui::Text("Precipitation Rate Calculation"); diff --git a/src/Menu.cpp b/src/Menu.cpp index d12a9f5aa0..8a48bbf0bb 100644 --- a/src/Menu.cpp +++ b/src/Menu.cpp @@ -166,6 +166,7 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( FirstTimeSetupCompleted, SkipClearCacheConfirmation, AutoHideFeatureList, + SimpleMode, SkipConstraintWarning, RequireShiftToDock, UseResolutionFont, diff --git a/src/Menu.h b/src/Menu.h index 3aedbd2bbc..3658684e94 100644 --- a/src/Menu.h +++ b/src/Menu.h @@ -409,6 +409,7 @@ class Menu bool FirstTimeSetupCompleted = false; // Track if first-time setup has been completed bool SkipClearCacheConfirmation = false; // Skip confirmation dialog when clearing shader cache bool AutoHideFeatureList = false; // Auto-hide left feature list panel, show on hover + bool SimpleMode = false; // Render features' simplified Off/quality-preset UI instead of full advanced settings bool SkipConstraintWarning = false; // Skip popup when a setting change creates new constraints bool RequireShiftToDock = true; // Require holding Shift to dock windows bool UseResolutionFont = true; // When true, runtime font size scales with screen resolution; when persisted to theme files, FontSize is zeroed for backward compatibility diff --git a/src/Menu/FeatureListRenderer.cpp b/src/Menu/FeatureListRenderer.cpp index 978984eb7d..4b3627f1d1 100644 --- a/src/Menu/FeatureListRenderer.cpp +++ b/src/Menu/FeatureListRenderer.cpp @@ -219,6 +219,76 @@ namespace bool g_dontShowAgainCheckbox = false; } +// Single scrollable page of all in-menu features. Loaded features with simple-mode +// controls render their slider/toggle inline; remaining features appear as labeled +// rows so users can see what's installed. +static void RenderSimpleModePage() +{ + bool& simpleMode = globals::menu->GetSettings().SimpleMode; + ImGui::Checkbox("Simple Mode", &simpleMode); + if (auto _tt = Util::HoverTooltipWrapper()) + ImGui::TextUnformatted("Turn off to return to the full advanced settings menu."); + ImGui::Separator(); + ImGui::Spacing(); + + std::vector sorted; + for (auto* f : Feature::GetFeatureList()) { + if (f->IsInMenu()) + sorted.push_back(f); + } + std::ranges::sort(sorted, [](Feature* a, Feature* b) { return a->GetName() < b->GetName(); }); + + // Every loaded feature gets an inline control via DrawSimpleSettings (boot-toggle + // fallback when no runtime enable flag); only features that failed to load drop + // into the issues list. + std::vector issues; + for (auto* feat : sorted) { + if (feat->loaded) { + ImGui::PushID(feat); + feat->DrawSimpleSettings(); + ImGui::PopID(); + } else { + issues.push_back(feat); + } + } + + if (issues.empty()) + return; + + ImGui::Spacing(); + ImGui::SeparatorText("Installation Issues"); + + const auto& palette = globals::menu->GetTheme().StatusPalette; + for (auto* feat : issues) { + ImGui::PushID(feat); + std::string row = feat->GetName(); + if (!feat->version.empty()) + row += " v" + feat->version; + ImGui::TextColored(palette.Error, "%s", row.c_str()); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::TextColored(palette.Error, "%s", + feat->failedLoadedMessage.empty() ? + "This feature failed to load." : + feat->failedLoadedMessage.c_str()); + auto [description, keyFeatures] = feat->GetFeatureSummary(); + if (!description.empty()) { + ImGui::Separator(); + ImGui::TextWrapped("%s", description.c_str()); + } + if (!keyFeatures.empty()) { + if (description.empty()) + ImGui::Separator(); + else + ImGui::Spacing(); + ImGui::TextUnformatted("Key features:"); + for (const auto& k : keyFeatures) + ImGui::BulletText("%s", k.c_str()); + } + } + ImGui::PopID(); + } +} + void FeatureListRenderer::RenderFeatureList( float footerHeight, size_t& selectedMenu, @@ -228,6 +298,14 @@ void FeatureListRenderer::RenderFeatureList( const std::function& drawGeneralSettings, const std::function& drawAdvancedSettings) { + // Simple mode: hide tabs/category list entirely and render a single page. + if (globals::menu->GetSettings().SimpleMode) { + ImGui::BeginChild("SimpleModePage", ImVec2(0, -footerHeight)); + RenderSimpleModePage(); + ImGui::EndChild(); + return; + } + ImGui::BeginChild("Menus Table", ImVec2(0, -footerHeight)); auto menuList = BuildMenuList(featureSearch, categoryExpansionStates, drawGeneralSettings, drawAdvancedSettings); @@ -750,7 +828,10 @@ void FeatureListRenderer::DrawMenuVisitor::RenderFeatureSettings(Feature* feat, ImGui::BeginDisabled(); ImVec2 cursorPosBefore = ImGui::GetCursorPos(); - feat->DrawSettings(); + if (globals::menu->GetSettings().SimpleMode) + feat->DrawSimpleSettings(); + else + feat->DrawSettings(); ImVec2 cursorPosAfter = ImGui::GetCursorPos(); if (sceneControlled) diff --git a/src/Menu/SettingsTabRenderer.cpp b/src/Menu/SettingsTabRenderer.cpp index c1b035d704..b7bb591e44 100644 --- a/src/Menu/SettingsTabRenderer.cpp +++ b/src/Menu/SettingsTabRenderer.cpp @@ -444,6 +444,13 @@ void SettingsTabRenderer::RenderBehaviorTab() ImGui::Text("Automatically hides the left feature list panel. Move cursor to the left edge to show it."); } + ImGui::Checkbox("Simple Mode", &globals::menu->GetSettings().SimpleMode); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Show a simplified Off / quality-preset UI for each feature instead of the full advanced settings.\n" + "Features without quality presets fall back to a single Enable toggle."); + } + if (ImGui::Checkbox("Require Shift to Dock", &globals::menu->GetSettings().RequireShiftToDock)) { ImGui::GetIO().ConfigDockingWithShift = globals::menu->GetSettings().RequireShiftToDock; } From 126b8421ab87a3e82d72bb2cad112ce7becef307 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 26 Apr 2026 21:14:58 -0700 Subject: [PATCH 2/6] fix(menu): address PR review on simple-mode preset slider - Add #include to Feature.h so the header is self-contained. - Add Feature::DetectCurrentQuality() virtual hook; DrawSimpleSettings calls it on first render so a feature whose JSON-loaded settings match a preset starts at that preset instead of "Custom". Override on ScreenSpaceGI by matching the (slices, steps, resolutionMode, enableGI) tuple against the SE/AE and VR tier values. - Make "Custom" a separate slider stop at n+1 (only present while actually custom). Without this, Custom and the first preset shared position 1 and clicking the first preset failed to fire SliderInt's change event. - Skip obsolete features whose INI was removed from the Installation Issues list, matching the advanced menu's filter; surfaced again in developer mode. Co-Authored-By: Claude Opus 4.7 --- src/Feature.cpp | 32 ++++++++++++++++++++++---------- src/Feature.h | 10 ++++++++++ src/Features/ScreenSpaceGI.cpp | 22 ++++++++++++++++++++++ src/Features/ScreenSpaceGI.h | 1 + src/Menu/FeatureListRenderer.cpp | 8 +++++--- 5 files changed, 60 insertions(+), 13 deletions(-) diff --git a/src/Feature.cpp b/src/Feature.cpp index d1620107db..2847578adb 100644 --- a/src/Feature.cpp +++ b/src/Feature.cpp @@ -367,7 +367,20 @@ void Feature::DrawSimpleSettings() } const int n = static_cast(presets.size()); - const int sliderMax = std::max(1, n); + + // Resolve "Custom" on first render by asking the feature to match its settings + // against its presets; saves the user from a "Custom" label when settings still + // match a preset (e.g. after JSON load). + if (currentlyOn && n > 0 && lastAppliedQualityIdx < 0) + lastAppliedQualityIdx = DetectCurrentQuality(); + + // When in Custom state with presets available, give the slider an extra stop at + // n+1 so dragging back to position 1 still fires SliderInt's change event and + // applies the first preset. Without this, Custom and "preset 0" would share + // position 1 and clicking 1 would be a no-op. + const bool inCustom = currentlyOn && n > 0 && (lastAppliedQualityIdx < 0 || lastAppliedQualityIdx >= n); + const int sliderMax = inCustom ? n + 1 : std::max(1, n); + int currentValue; std::string overlay; if (!currentlyOn) { @@ -376,12 +389,12 @@ void Feature::DrawSimpleSettings() } else if (n == 0) { currentValue = 1; overlay = "On"; - } else if (lastAppliedQualityIdx >= 0 && lastAppliedQualityIdx < n) { + } else if (inCustom) { + currentValue = n + 1; + overlay = "Custom"; + } else { currentValue = lastAppliedQualityIdx + 1; overlay.assign(presets[lastAppliedQualityIdx].label.data(), presets[lastAppliedQualityIdx].label.size()); - } else { - currentValue = 1; - overlay = "Custom"; } int v = currentValue; @@ -393,15 +406,14 @@ void Feature::DrawSimpleSettings() else ToggleAtBootSetting(); } - if (wantOn && n > 0) { + if (wantOn && n > 0 && v <= n) { const int idx = v - 1; - if (idx >= 0 && idx < n) { - ApplyPreset(this, presets[idx]); - lastAppliedQualityIdx = idx; - } + ApplyPreset(this, presets[idx]); + lastAppliedQualityIdx = idx; } else if (!wantOn) { lastAppliedQualityIdx = -1; } + // v == n+1 (Custom slot): leave lastAppliedQualityIdx as-is. } if (auto _tt = Util::HoverTooltipWrapper()) { diff --git a/src/Feature.h b/src/Feature.h index 1bf5430464..5e656d6460 100644 --- a/src/Feature.h +++ b/src/Feature.h @@ -3,6 +3,7 @@ #include "FeatureCategories.h" #include "FeatureConstraints.h" #include "FeatureVersions.h" +#include // GetQualityPresets() return type #ifdef TRACY_ENABLE # include # include @@ -166,6 +167,15 @@ struct Feature */ virtual bool* GetEnabledFlag() { return nullptr; } + /** + * Returns the index into GetQualityPresets() that matches the feature's current + * settings, or -1 when the settings don't match any preset (Custom). Used by the + * Simple menu to position the slider correctly on first render after a JSON load. + * Default returns -1; features that can compare their settings to their tiers + * should override. + */ + virtual int DetectCurrentQuality() const { return -1; } + /** * Render the Simple-mode settings UI: Off button + quality preset row. * Implemented in Feature.cpp; features generally do not need to override. diff --git a/src/Features/ScreenSpaceGI.cpp b/src/Features/ScreenSpaceGI.cpp index 6186957365..52e5675123 100644 --- a/src/Features/ScreenSpaceGI.cpp +++ b/src/Features/ScreenSpaceGI.cpp @@ -83,6 +83,28 @@ std::span ScreenSpaceGI::GetQualityPresets() const return kQualityPresets; } +int ScreenSpaceGI::DetectCurrentQuality() const +{ + // SSGI tiers vary by platform — VR uses AO-only variants. Each tier matches when + // its critical knobs (slices, steps, resolution mode, GI on/off) all match. + const bool isVR = REL::Module::IsVR(); + static constexpr std::array, 3> tiers = { { + { { 10, 12, 2, true }, { 1, 6, 2, false } }, // Low (flat / VR) + { { 4, 8, 1, true }, { 3, 8, 1, false } }, // Medium + { { 4, 8, 0, true }, { 4, 8, 0, true } }, // High (no VR variant — uses flat) + } }; + for (size_t i = 0; i < tiers.size(); ++i) { + const auto& q = isVR ? tiers[i].second : tiers[i].first; + if (settings.NumSlices == q.numSlices && + settings.NumSteps == q.numSteps && + settings.ResolutionMode == q.resolutionMode && + settings.EnableGI == q.enableGI && + settings.EnableBlur) + return static_cast(i); + } + return -1; +} + void ScreenSpaceGI::DrawSettings() { static bool showAdvanced; diff --git a/src/Features/ScreenSpaceGI.h b/src/Features/ScreenSpaceGI.h index ed8ceecf0d..f43ba384ae 100644 --- a/src/Features/ScreenSpaceGI.h +++ b/src/Features/ScreenSpaceGI.h @@ -42,6 +42,7 @@ struct ScreenSpaceGI : Feature virtual std::span GetQualityPresets() const override; virtual bool* GetEnabledFlag() override { return &settings.Enabled; } + virtual int DetectCurrentQuality() const override; virtual void LoadSettings(json& o_json) override; virtual void SaveSettings(json& o_json) override; diff --git a/src/Menu/FeatureListRenderer.cpp b/src/Menu/FeatureListRenderer.cpp index 4b3627f1d1..3f6f35008e 100644 --- a/src/Menu/FeatureListRenderer.cpp +++ b/src/Menu/FeatureListRenderer.cpp @@ -239,15 +239,17 @@ static void RenderSimpleModePage() std::ranges::sort(sorted, [](Feature* a, Feature* b) { return a->GetName() < b->GetName(); }); // Every loaded feature gets an inline control via DrawSimpleSettings (boot-toggle - // fallback when no runtime enable flag); only features that failed to load drop - // into the issues list. + // fallback when no runtime enable flag). Unloaded features go to the issues list, + // except obsolete ones with their INI removed — those are silently dropped to + // match the advanced menu's filtering, surfaced again in developer mode. + const bool devMode = globals::state->IsDeveloperMode(); std::vector issues; for (auto* feat : sorted) { if (feat->loaded) { ImGui::PushID(feat); feat->DrawSimpleSettings(); ImGui::PopID(); - } else { + } else if (devMode || !FeatureIssues::IsObsoleteFeature(feat->GetShortName())) { issues.push_back(feat); } } From fcae96a9dd03965f5804143bbac1c53628cecd1e Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Wed, 6 May 2026 00:21:58 -0700 Subject: [PATCH 3/6] fix(menu): address remaining PR review on simple mode - Replace SliderInt with Checkbox for features with no quality presets (n == 0 path), matching the documented intent of the simple mode UI - Guard preset tooltip lookup against out-of-bounds access when slider is in Custom state (currentValue == n+1) - Bump ScreenSpaceGI to 4.2.0, WetnessEffects to 3.2.0 per bot audit Co-Authored-By: Claude Sonnet 4.6 --- .../Shaders/Features/ScreenSpaceGI.ini | 2 +- .../Shaders/Features/WetnessEffects.ini | 2 +- src/Feature.cpp | 37 ++++++++++++------- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/features/Screen Space GI/Shaders/Features/ScreenSpaceGI.ini b/features/Screen Space GI/Shaders/Features/ScreenSpaceGI.ini index 0accf2f262..5f19c243ea 100644 --- a/features/Screen Space GI/Shaders/Features/ScreenSpaceGI.ini +++ b/features/Screen Space GI/Shaders/Features/ScreenSpaceGI.ini @@ -1,5 +1,5 @@ [Info] -Version = 4-1-0 +Version = 4-2-0 [Nexus] nexusmodid = 130375 diff --git a/features/Wetness Effects/Shaders/Features/WetnessEffects.ini b/features/Wetness Effects/Shaders/Features/WetnessEffects.ini index ee1e92d27d..c09756815a 100644 --- a/features/Wetness Effects/Shaders/Features/WetnessEffects.ini +++ b/features/Wetness Effects/Shaders/Features/WetnessEffects.ini @@ -1,5 +1,5 @@ [Info] -Version = 3-1-0 +Version = 3-2-0 [Nexus] nexusmodid = 112739 diff --git a/src/Feature.cpp b/src/Feature.cpp index 2847578adb..b81f0fffb0 100644 --- a/src/Feature.cpp +++ b/src/Feature.cpp @@ -397,23 +397,34 @@ void Feature::DrawSimpleSettings() overlay.assign(presets[lastAppliedQualityIdx].label.data(), presets[lastAppliedQualityIdx].label.size()); } - int v = currentValue; - if (ImGui::SliderInt(label.c_str(), &v, 0, sliderMax, overlay.c_str(), ImGuiSliderFlags_AlwaysClamp)) { - const bool wantOn = v >= 1; - if (currentlyOn != wantOn) { + if (n == 0) { + // No quality presets — render as a simple on/off checkbox. + bool isOn = currentlyOn; + if (ImGui::Checkbox(label.c_str(), &isOn) && currentlyOn != isOn) { if (runtimeAvailable) - *runtimeEnabled = wantOn; + *runtimeEnabled = isOn; else ToggleAtBootSetting(); } - if (wantOn && n > 0 && v <= n) { - const int idx = v - 1; - ApplyPreset(this, presets[idx]); - lastAppliedQualityIdx = idx; - } else if (!wantOn) { - lastAppliedQualityIdx = -1; + } else { + int v = currentValue; + if (ImGui::SliderInt(label.c_str(), &v, 0, sliderMax, overlay.c_str(), ImGuiSliderFlags_AlwaysClamp)) { + const bool wantOn = v >= 1; + if (currentlyOn != wantOn) { + if (runtimeAvailable) + *runtimeEnabled = wantOn; + else + ToggleAtBootSetting(); + } + if (wantOn && v <= n) { + const int idx = v - 1; + ApplyPreset(this, presets[idx]); + lastAppliedQualityIdx = idx; + } else if (!wantOn) { + lastAppliedQualityIdx = -1; + } + // v == n+1 (Custom slot): leave lastAppliedQualityIdx as-is. } - // v == n+1 (Custom slot): leave lastAppliedQualityIdx as-is. } if (auto _tt = Util::HoverTooltipWrapper()) { @@ -427,7 +438,7 @@ void Feature::DrawSimpleSettings() for (const auto& k : keyFeatures) ImGui::BulletText("%s", k.c_str()); } - if (currentValue >= 1 && n > 0) { + if (currentValue >= 1 && currentValue <= n) { const auto& desc = presets[currentValue - 1].description; if (!desc.empty()) { ImGui::Separator(); From e1e8608cd90d933fb964f191c64b0835d4a38f43 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Wed, 6 May 2026 00:43:20 -0700 Subject: [PATCH 4/6] revert(menu): keep SliderInt for preset-less features Reverts the checkbox change from the previous commit. The slider provides a uniform look across all features in simple mode regardless of preset count, which is preferable to mixing widget types. Co-Authored-By: Claude Sonnet 4.6 --- src/Feature.cpp | 35 ++++++++++++----------------------- 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/src/Feature.cpp b/src/Feature.cpp index b81f0fffb0..0658635615 100644 --- a/src/Feature.cpp +++ b/src/Feature.cpp @@ -397,34 +397,23 @@ void Feature::DrawSimpleSettings() overlay.assign(presets[lastAppliedQualityIdx].label.data(), presets[lastAppliedQualityIdx].label.size()); } - if (n == 0) { - // No quality presets — render as a simple on/off checkbox. - bool isOn = currentlyOn; - if (ImGui::Checkbox(label.c_str(), &isOn) && currentlyOn != isOn) { + int v = currentValue; + if (ImGui::SliderInt(label.c_str(), &v, 0, sliderMax, overlay.c_str(), ImGuiSliderFlags_AlwaysClamp)) { + const bool wantOn = v >= 1; + if (currentlyOn != wantOn) { if (runtimeAvailable) - *runtimeEnabled = isOn; + *runtimeEnabled = wantOn; else ToggleAtBootSetting(); } - } else { - int v = currentValue; - if (ImGui::SliderInt(label.c_str(), &v, 0, sliderMax, overlay.c_str(), ImGuiSliderFlags_AlwaysClamp)) { - const bool wantOn = v >= 1; - if (currentlyOn != wantOn) { - if (runtimeAvailable) - *runtimeEnabled = wantOn; - else - ToggleAtBootSetting(); - } - if (wantOn && v <= n) { - const int idx = v - 1; - ApplyPreset(this, presets[idx]); - lastAppliedQualityIdx = idx; - } else if (!wantOn) { - lastAppliedQualityIdx = -1; - } - // v == n+1 (Custom slot): leave lastAppliedQualityIdx as-is. + if (wantOn && n > 0 && v <= n) { + const int idx = v - 1; + ApplyPreset(this, presets[idx]); + lastAppliedQualityIdx = idx; + } else if (!wantOn) { + lastAppliedQualityIdx = -1; } + // v == n+1 (Custom slot): leave lastAppliedQualityIdx as-is. } if (auto _tt = Util::HoverTooltipWrapper()) { From 6892c522d0e29f5dffa9933cbb0a541941445a2a Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Wed, 6 May 2026 00:52:18 -0700 Subject: [PATCH 5/6] refactor(ssgi): deduplicate tier values into shared kTierValues Extract a single constexpr kTierValues array shared by both kQualityPresets lambdas and DetectCurrentQuality, so a change to one tier cannot silently diverge and cause preset detection to return Custom for a setting that still matches a real preset. Co-Authored-By: Claude Sonnet 4.6 --- src/Features/ScreenSpaceGI.cpp | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Features/ScreenSpaceGI.cpp b/src/Features/ScreenSpaceGI.cpp index 52e5675123..5bce649602 100644 --- a/src/Features/ScreenSpaceGI.cpp +++ b/src/Features/ScreenSpaceGI.cpp @@ -63,17 +63,24 @@ namespace ssgi->recompileFlag = true; } - // VR variants force AO-only (EnableGI=false) for performance; vrApply is null where - // the SE/AE tuning is acceptable on VR and ApplyPreset falls back to apply. + // Shared tier table — both the apply lambdas and DetectCurrentQuality read from here + // so a single edit keeps application and detection in sync. + // VR High intentionally mirrors flat (vrApply=nullptr -> ApplyPreset falls back to apply). + static constexpr std::array, 3> kTierValues = { { + { { 10, 12, 2, true }, { 1, 6, 2, false } }, // Low (flat / VR) + { { 4, 8, 1, true }, { 3, 8, 1, false } }, // Medium + { { 4, 8, 0, true }, { 4, 8, 0, true } }, // High (no VR variant — uses flat) + } }; + static constexpr std::array kQualityPresets = { { { Feature::QualityLevel::Low, "Low", "Quarter resolution, blurred. AO + basic GI.", - [](Feature* f, Feature::QualityLevel) { ApplyTier(f, { 10, 12, 2, true }); }, - [](Feature* f, Feature::QualityLevel) { ApplyTier(f, { 1, 6, 2, false }); } }, + [](Feature* f, Feature::QualityLevel) { ApplyTier(f, kTierValues[0].first); }, + [](Feature* f, Feature::QualityLevel) { ApplyTier(f, kTierValues[0].second); } }, { Feature::QualityLevel::Medium, "Medium", "Half resolution, balanced quality and performance.", - [](Feature* f, Feature::QualityLevel) { ApplyTier(f, { 4, 8, 1, true }); }, - [](Feature* f, Feature::QualityLevel) { ApplyTier(f, { 3, 8, 1, false }); } }, + [](Feature* f, Feature::QualityLevel) { ApplyTier(f, kTierValues[1].first); }, + [](Feature* f, Feature::QualityLevel) { ApplyTier(f, kTierValues[1].second); } }, { Feature::QualityLevel::High, "High", "Full resolution, clean output with full GI.", - [](Feature* f, Feature::QualityLevel) { ApplyTier(f, { 4, 8, 0, true }); }, + [](Feature* f, Feature::QualityLevel) { ApplyTier(f, kTierValues[2].first); }, nullptr }, } }; } @@ -85,16 +92,9 @@ std::span ScreenSpaceGI::GetQualityPresets() const int ScreenSpaceGI::DetectCurrentQuality() const { - // SSGI tiers vary by platform — VR uses AO-only variants. Each tier matches when - // its critical knobs (slices, steps, resolution mode, GI on/off) all match. const bool isVR = REL::Module::IsVR(); - static constexpr std::array, 3> tiers = { { - { { 10, 12, 2, true }, { 1, 6, 2, false } }, // Low (flat / VR) - { { 4, 8, 1, true }, { 3, 8, 1, false } }, // Medium - { { 4, 8, 0, true }, { 4, 8, 0, true } }, // High (no VR variant — uses flat) - } }; - for (size_t i = 0; i < tiers.size(); ++i) { - const auto& q = isVR ? tiers[i].second : tiers[i].first; + for (size_t i = 0; i < kTierValues.size(); ++i) { + const auto& q = isVR ? kTierValues[i].second : kTierValues[i].first; if (settings.NumSlices == q.numSlices && settings.NumSteps == q.numSteps && settings.ResolutionMode == q.resolutionMode && From 2f261ad10b90a5b0f0da0c86deb7270cc25d5a7d Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Wed, 6 May 2026 01:36:57 -0700 Subject: [PATCH 6/6] fix(menu): address latest CodeRabbit review on simple mode - Remove simpleModeBootCaptured/simpleModeBootInitial members; loaded features were enabled at boot by definition, so restartPending simplifies to !runtimeAvailable && IsFeatureDisabled(shortName) - Show boot-disabled features as an Off slider in simple mode instead of incorrectly routing them to the Installation Issues section - Advanced SSGI Low/Standard/Extreme preset buttons now delegate to ApplyTier(kTierValues[N]) instead of inlining the same literals Co-Authored-By: Claude Sonnet 4.6 --- src/Feature.cpp | 9 +++------ src/Feature.h | 8 +------- src/Features/ScreenSpaceGI.cpp | 21 +++------------------ src/Menu/FeatureListRenderer.cpp | 10 +++++----- 4 files changed, 12 insertions(+), 36 deletions(-) diff --git a/src/Feature.cpp b/src/Feature.cpp index 0658635615..cd0987fd60 100644 --- a/src/Feature.cpp +++ b/src/Feature.cpp @@ -348,12 +348,9 @@ void Feature::DrawSimpleSettings() *runtimeEnabled : !globals::state->IsFeatureDisabled(shortName); - if (!runtimeAvailable && !simpleModeBootCaptured) { - simpleModeBootInitial = globals::state->IsFeatureDisabled(shortName); - simpleModeBootCaptured = true; - } - const bool restartPending = !runtimeAvailable && - globals::state->IsFeatureDisabled(shortName) != simpleModeBootInitial; + // Loaded features were enabled at boot by definition; a restart is needed only + // if the user has since toggled the boot setting to disabled. + const bool restartPending = !runtimeAvailable && globals::state->IsFeatureDisabled(shortName); const auto& palette = globals::menu->GetTheme().StatusPalette; if (restartPending) diff --git a/src/Feature.h b/src/Feature.h index 5e656d6460..d6dcfc7aed 100644 --- a/src/Feature.h +++ b/src/Feature.h @@ -79,13 +79,7 @@ struct Feature // Nexus Mods base URL for Skyrim Special Edition static constexpr std::string_view NEXUS_BASE_URL = "https://www.nexusmods.com/skyrimspecialedition/mods/"; bool loaded = false; - // The two simple-mode bools fit in the natural padding between `loaded` and - // `lastAppliedQualityIdx`; the int then fills the remaining padding before the - // 8-byte-aligned std::string. Reordering this group triggers C4324 in alignas(16) - // derived classes. - bool simpleModeBootCaptured = false; - bool simpleModeBootInitial = false; // snapshot of IsFeatureDisabled at first render - int lastAppliedQualityIdx = -1; // -1 = unknown/Custom + int lastAppliedQualityIdx = -1; // -1 = unknown/Custom std::string version; std::string failedLoadedMessage; diff --git a/src/Features/ScreenSpaceGI.cpp b/src/Features/ScreenSpaceGI.cpp index 5bce649602..6d6d2256d6 100644 --- a/src/Features/ScreenSpaceGI.cpp +++ b/src/Features/ScreenSpaceGI.cpp @@ -173,36 +173,21 @@ void ScreenSpaceGI::DrawSettings() ImGui::TableNextColumn(); if (ImGui::Button("Low", { -1, 0 })) { - settings.NumSlices = 10; - settings.NumSteps = 12; - settings.ResolutionMode = 2; - settings.EnableBlur = true; - settings.EnableGI = true; - recompileFlag = true; + ApplyTier(this, kTierValues[0].first); } if (auto _tt = Util::HoverTooltipWrapper()) ImGui::Text("Quarter res and blurry."); ImGui::TableNextColumn(); if (ImGui::Button("Standard", { -1, 0 })) { - settings.NumSlices = 4; - settings.NumSteps = 8; - settings.ResolutionMode = 1; - settings.EnableBlur = true; - settings.EnableGI = true; - recompileFlag = true; + ApplyTier(this, kTierValues[1].first); } if (auto _tt = Util::HoverTooltipWrapper()) ImGui::Text("Half res and somewhat stable."); ImGui::TableNextColumn(); if (ImGui::Button("Extreme", { -1, 0 })) { - settings.NumSlices = 4; - settings.NumSteps = 8; - settings.ResolutionMode = 0; - settings.EnableBlur = true; - settings.EnableGI = true; - recompileFlag = true; + ApplyTier(this, kTierValues[2].first); } if (auto _tt = Util::HoverTooltipWrapper()) ImGui::Text("Full res and clean."); diff --git a/src/Menu/FeatureListRenderer.cpp b/src/Menu/FeatureListRenderer.cpp index 3f6f35008e..e7c4456ced 100644 --- a/src/Menu/FeatureListRenderer.cpp +++ b/src/Menu/FeatureListRenderer.cpp @@ -238,14 +238,14 @@ static void RenderSimpleModePage() } std::ranges::sort(sorted, [](Feature* a, Feature* b) { return a->GetName() < b->GetName(); }); - // Every loaded feature gets an inline control via DrawSimpleSettings (boot-toggle - // fallback when no runtime enable flag). Unloaded features go to the issues list, - // except obsolete ones with their INI removed — those are silently dropped to - // match the advanced menu's filtering, surfaced again in developer mode. + // Loaded features and features intentionally disabled at boot both get a slider + // (the latter shows as Off). Only genuinely failed/missing features go to issues, + // except obsolete ones — those are silently dropped unless developer mode is on. const bool devMode = globals::state->IsDeveloperMode(); std::vector issues; for (auto* feat : sorted) { - if (feat->loaded) { + const bool bootDisabled = globals::state->IsFeatureDisabled(feat->GetShortName()); + if (feat->loaded || bootDisabled) { ImGui::PushID(feat); feat->DrawSimpleSettings(); ImGui::PopID();