From d2b0d7843fc5bf7787819bb118722f84f8bddeaa Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Wed, 20 May 2026 00:00:33 -0700 Subject: [PATCH 1/6] refactor(menu): regroup Advanced tabs by purpose Old Advanced tabs (Developer / Logging / Shader Debug / Disable at Boot / Testing) mixed audience and topic. Regrouped into four alphabetical, purpose-based tabs: - Shaders: Compile Flags, Threading, Cache & File Watcher, Replace Original Shaders, Statistics - Diagnostics: Logging, Runtime Debug, Shader Blocking (dev-only) - Disable at Boot: unchanged - Testing: A/B harness + dev-mode test scaffolding Also: - Drop redundant CollapsingHeader("Testing") inside the Testing tab and CollapsingHeader("Active Shaders") inside the Shader Blocking panel -- both nested headings under their own tab/panel. - Move "Skip Clear Cache Dialogue" from Shaders tab to Behavior (it's UI behavior, not a shader setting). - Update docs/development/vscode-setup.md menu path to match. Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/development/vscode-setup.md | 2 +- src/FeatureIssues.cpp | 120 +++--- src/Menu/AdvancedSettingsRenderer.cpp | 584 +++++++++++++++----------- src/Menu/AdvancedSettingsRenderer.h | 18 +- src/Menu/SettingsTabRenderer.cpp | 20 +- 5 files changed, 412 insertions(+), 332 deletions(-) diff --git a/docs/development/vscode-setup.md b/docs/development/vscode-setup.md index 7122622582..8dac26867a 100644 --- a/docs/development/vscode-setup.md +++ b/docs/development/vscode-setup.md @@ -60,7 +60,7 @@ Automatically deploy shaders when you save `.hlsl` or `.hlsli` files. **Interaction with built-in filewatcher:** -Community Shaders has a built-in filewatcher (**Settings → Advanced → Shader Compilation → Enable File Watcher**) that hot-reloads shaders when files change in the game's `Data/Shaders/` directory. The workflow is: +Community Shaders has a built-in filewatcher (**Settings → Advanced → Shaders → Cache & File Watcher → Enable File Watcher**) that hot-reloads shaders when files change in the game's `Data/Shaders/` directory. The workflow is: 1. Edit shader in VSCode 2. Save → RunOnSave deploys to `Data/Shaders/` diff --git a/src/FeatureIssues.cpp b/src/FeatureIssues.cpp index e744cad4db..2a600ebf41 100644 --- a/src/FeatureIssues.cpp +++ b/src/FeatureIssues.cpp @@ -1462,74 +1462,70 @@ namespace FeatureIssues auto* menu = Menu::GetSingleton(); const auto& themeSettings = menu->GetTheme(); - if (ImGui::CollapsingHeader("Testing", ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick)) { - { - auto sectionWrapper = Util::SectionWrapper("Feature Issue Testing", - "These tools create test INI files to trigger all known feature issue types for testing purposes.", - themeSettings.Palette.Text); - - if (sectionWrapper) { - const bool hasActiveTests = HasActiveTestInis(); - if (hasActiveTests) { // Warning section using theme colors - ImGui::PushStyleColor(ImGuiCol_Text, themeSettings.StatusPalette.RestartNeeded); - ImGui::TextWrapped("Test INI files are currently active. Restart CS to see feature issues."); - ImGui::PopStyleColor(); // Show detailed test state information - ImGui::Spacing(); - ImGui::PushStyleColor(ImGuiCol_Text, themeSettings.StatusPalette.RestartNeeded); - ImGui::TextWrapped(GetTestStateDescription().c_str()); - ImGui::PopStyleColor(); - ImGui::Spacing(); - } - - // Create Test INIs button - { - auto disableGuard = Util::DisableGuard(hasActiveTests); - auto buttonStyle = Util::StyledButtonWrapper( - themeSettings.Palette.FrameBorder, - themeSettings.StatusPalette.RestartNeeded, - themeSettings.StatusPalette.CurrentHotkey); - - if (ImGui::Button("Create Test Inis", { -1, 0 })) { - auto testInis = CreateTestInis(); - logger::info("Created {} test INI files for feature issue testing", testInis.size()); - } - } + auto sectionWrapper = Util::SectionWrapper("Feature Issue Testing", + "These tools create test INI files to trigger all known feature issue types for testing purposes.", + themeSettings.Palette.Text); + + if (sectionWrapper) { + const bool hasActiveTests = HasActiveTestInis(); + if (hasActiveTests) { // Warning section using theme colors + ImGui::PushStyleColor(ImGuiCol_Text, themeSettings.StatusPalette.RestartNeeded); + ImGui::TextWrapped("Test INI files are currently active. Restart CS to see feature issues."); + ImGui::PopStyleColor(); // Show detailed test state information + ImGui::Spacing(); + ImGui::PushStyleColor(ImGuiCol_Text, themeSettings.StatusPalette.RestartNeeded); + ImGui::TextWrapped(GetTestStateDescription().c_str()); + ImGui::PopStyleColor(); + ImGui::Spacing(); + } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Creates test INI files that trigger all known feature issue cases:\n" - "- Obsolete features (ComplexParallaxMaterials, TerrainBlending, etc.)\n" - "- Unknown features (fake non-existent features)\n" - "- Version mismatch (modifies existing feature version)\n" - "Restart CS after creating to see the issues in action."); - } + // Create Test INIs button + { + auto disableGuard = Util::DisableGuard(hasActiveTests); + auto buttonStyle = Util::StyledButtonWrapper( + themeSettings.Palette.FrameBorder, + themeSettings.StatusPalette.RestartNeeded, + themeSettings.StatusPalette.CurrentHotkey); + + if (ImGui::Button("Create Test Inis", { -1, 0 })) { + auto testInis = CreateTestInis(); + logger::info("Created {} test INI files for feature issue testing", testInis.size()); + } + } - // Restore button - { - auto disableGuard = Util::DisableGuard(!hasActiveTests); - auto buttonStyle = Util::StyledButtonWrapper( - themeSettings.Palette.FrameBorder, - themeSettings.StatusPalette.Error, - themeSettings.StatusPalette.CurrentHotkey); - - if (ImGui::Button("Restore", { -1, 0 })) { - auto& testInis = GetCurrentTestInis(); - if (RestoreOriginalState(testInis)) { - logger::info("Successfully restored original state"); - } else { - logger::warn("Some restoration operations failed"); - } - } - } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Creates test INI files that trigger all known feature issue cases:\n" + "- Obsolete features (ComplexParallaxMaterials, TerrainBlending, etc.)\n" + "- Unknown features (fake non-existent features)\n" + "- Version mismatch (modifies existing feature version)\n" + "Restart CS after creating to see the issues in action."); + } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Removes all test INI files and restores any modified INI files to their original state.\n" - "This undoes all changes made by 'Create Test Inis'.\n" - "Restart CS after restoring to see normal operation."); + // Restore button + { + auto disableGuard = Util::DisableGuard(!hasActiveTests); + auto buttonStyle = Util::StyledButtonWrapper( + themeSettings.Palette.FrameBorder, + themeSettings.StatusPalette.Error, + themeSettings.StatusPalette.CurrentHotkey); + + if (ImGui::Button("Restore", { -1, 0 })) { + auto& testInis = GetCurrentTestInis(); + if (RestoreOriginalState(testInis)) { + logger::info("Successfully restored original state"); + } else { + logger::warn("Some restoration operations failed"); } } } + + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Removes all test INI files and restores any modified INI files to their original state.\n" + "This undoes all changes made by 'Create Test Inis'.\n" + "Restart CS after restoring to see normal operation."); + } } } bool RefreshTestState() diff --git a/src/Menu/AdvancedSettingsRenderer.cpp b/src/Menu/AdvancedSettingsRenderer.cpp index 8bd217b20d..7a121475eb 100644 --- a/src/Menu/AdvancedSettingsRenderer.cpp +++ b/src/Menu/AdvancedSettingsRenderer.cpp @@ -20,18 +20,20 @@ void AdvancedSettingsRenderer::RenderAdvancedSettings( const std::function& drawDisableAtBootSettings) { - // Use TabBar system - tabs sorted alphabetically + // Tabs ordered alphabetically; each tab is grouped by purpose, not audience. + // Shaders = configure & inspect shader compilation + // Diagnostics = log/inspect runtime state & block individual shaders + // Disable at Boot = user-facing failsafe toggles + // Testing = A/B harness + dev-mode test scaffolding if (ImGui::BeginTabBar("##AdvancedSettingsTabs", ImGuiTabBarFlags_None)) { - // Developer Tab - if (MenuFonts::BeginTabItemWithFont("Developer", Menu::FontRole::Subheading)) { - if (ImGui::BeginChild("##DeveloperContent", ImVec2(0, 0), false)) { - RenderDeveloperSection(); + if (MenuFonts::BeginTabItemWithFont("Diagnostics", Menu::FontRole::Subheading)) { + if (ImGui::BeginChild("##DiagnosticsContent", ImVec2(0, 0), false)) { + RenderDiagnosticsSection(); } ImGui::EndChild(); ImGui::EndTabItem(); } - // Disable at Boot Tab if (MenuFonts::BeginTabItemWithFont("Disable at Boot", Menu::FontRole::Subheading)) { if (ImGui::BeginChild("##DisableAtBootContent", ImVec2(0, 0), false)) { RenderDisableAtBootSection(drawDisableAtBootSettings); @@ -40,27 +42,16 @@ void AdvancedSettingsRenderer::RenderAdvancedSettings( ImGui::EndTabItem(); } - // Logging Tab - if (MenuFonts::BeginTabItemWithFont("Logging", Menu::FontRole::Subheading)) { - if (ImGui::BeginChild("##LoggingContent", ImVec2(0, 0), false)) { - RenderLoggingSection(); + if (MenuFonts::BeginTabItemWithFont("Shaders", Menu::FontRole::Subheading)) { + if (ImGui::BeginChild("##ShadersContent", ImVec2(0, 0), false)) { + RenderShadersSection(); } ImGui::EndChild(); ImGui::EndTabItem(); } - // Shader Debug Tab - if (MenuFonts::BeginTabItemWithFont("Shader Debug", Menu::FontRole::Subheading)) { - if (ImGui::BeginChild("##ShaderDebugContent", ImVec2(0, 0), false)) { - RenderShaderDebugSection(); - } - ImGui::EndChild(); - ImGui::EndTabItem(); - } - - // Testing Tab (for A/B Testing and related settings) if (MenuFonts::BeginTabItemWithFont("Testing", Menu::FontRole::Subheading)) { - if (ImGui::BeginChild("##Testing", ImVec2(0, 0), false)) { + if (ImGui::BeginChild("##TestingContent", ImVec2(0, 0), false)) { RenderTestingSection(); } ImGui::EndChild(); @@ -71,29 +62,44 @@ void AdvancedSettingsRenderer::RenderAdvancedSettings( } } -void AdvancedSettingsRenderer::RenderLoggingSection() +// ----------------------------------------------------------------------------- +// Shaders tab +// ----------------------------------------------------------------------------- + +void AdvancedSettingsRenderer::RenderShadersSection() +{ + RenderShaderCompileFlags(); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + RenderShaderThreading(); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + RenderShaderCacheControls(); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + RenderShaderReplacementTable(); + + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + RenderShaderCompileStatistics(); +} + +void AdvancedSettingsRenderer::RenderShaderCompileFlags() { auto shaderCache = globals::shaderCache; - // Log Level selection - spdlog::level::level_enum logLevel = globals::state->GetLogLevel(); - const char* items[] = { - "trace", - "debug", - "info", - "warn", - "err", - "critical", - "off" - }; - static int item_current = static_cast(logLevel); - if (ImGui::Combo("Log Level", &item_current, items, IM_ARRAYSIZE(items))) { - ImGui::SameLine(); - globals::state->SetLogLevel(static_cast(item_current)); - } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Log level. Trace is most verbose. Default is info."); - } + Util::DrawSectionHeader("Compile Flags"); // Shader Defines input auto& shaderDefines = globals::state->shaderDefinesString; @@ -110,9 +116,50 @@ void AdvancedSettingsRenderer::RenderLoggingSection() ImGui::Text("Defines for Shader Compiler. Semicolon \";\" separated. Clear with space. Rebuild shaders after making change. Compute Shaders require a restart to recompile."); } - ImGui::Spacing(); + // Half-precision (partial precision) shader compile flag + bool partialPrecision = globals::state->enablePartialPrecision.load(std::memory_order_relaxed); + if (ImGui::Checkbox("Half Precision (Partial Precision)", &partialPrecision)) { + globals::state->enablePartialPrecision.store(partialPrecision, std::memory_order_relaxed); + // Force a recompile so the flag actually takes effect on subsequent shader builds. + globals::shaderCache->Clear(); + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Adds D3DCOMPILE_PARTIAL_PRECISION to the shader compiler flags.\n" + "Lets fxc downgrade unmarked float ops to FP16 where it can prove safety, " + "on top of the existing min16float type hints.\n" + "On FP16-capable GPUs (Pascal+ / GCN+ / Skylake+) this can halve register " + "pressure and double ALU throughput, but it can also introduce minor visual " + "differences in shaders that haven't been audited for precision sensitivity.\n" + "Toggling this clears the shader cache and triggers a full recompile."); + } + + // Avoid flow control compiler flag (transient — not saved to config because the + // right setting depends on the current scene, not the user). + bool avoidFlowControl = globals::state->enableAvoidFlowControl.load(std::memory_order_relaxed); + if (ImGui::Checkbox("Avoid Flow Control", &avoidFlowControl)) { + globals::state->enableAvoidFlowControl.store(avoidFlowControl, std::memory_order_relaxed); + // Force a recompile so the flag actually takes effect on subsequent shader builds. + globals::shaderCache->Clear(); + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Adds D3DCOMPILE_AVOID_FLOW_CONTROL to the shader compiler flags.\n" + "Forces fxc to flatten branches into predicated ops rather than emitting " + "dynamic flow control. Often a win for short branch bodies and uniformly-" + "taken branches; usually a loss for long divergent branches that vanilla " + "flow control would skip entirely.\n" + "Resets every launch. Toggling this clears the shader cache and triggers a " + "full recompile."); + } +} + +void AdvancedSettingsRenderer::RenderShaderThreading() +{ + auto shaderCache = globals::shaderCache; + + Util::DrawSectionHeader("Threading"); - // Compiler Thread controls ImGui::SliderInt("Compiler Threads", &shaderCache->compilationThreadCount, 1, static_cast(std::thread::hardware_concurrency())); if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text( @@ -127,29 +174,24 @@ void AdvancedSettingsRenderer::RenderLoggingSection() "Defaults to half of performance cores to avoid impacting the render thread. " "Higher values finish compilation faster but may cause stuttering."); } - - ImGui::Columns(2, nullptr, false); - - // Dump Ini Settings button - if (ImGui::Button("Dump Ini Settings", { -1, 0 })) { - Util::DumpSettingsOptions(); - } - - ImGui::NextColumn(); - - // Open Logs button - std::filesystem::path logPath = Util::PathHelpers::GetLogPath(); - if (!logPath.empty() && ImGui::Button("Open Logs", { -1, 0 })) { - ShellExecuteA(NULL, "open", logPath.string().c_str(), NULL, NULL, SW_SHOWNORMAL); - } - - ImGui::Columns(1); } -void AdvancedSettingsRenderer::RenderShaderDebugSection() +void AdvancedSettingsRenderer::RenderShaderCacheControls() { auto shaderCache = globals::shaderCache; - auto state = globals::state; + + Util::DrawSectionHeader("Cache & File Watcher"); + + // File Watcher option + bool useFileWatcher = shaderCache->UseFileWatcher(); + if (ImGui::Checkbox("Enable File Watcher", &useFileWatcher)) { + shaderCache->SetFileWatcher(useFileWatcher); + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Automatically recompile shaders on file change. " + "Intended for developing."); + } // Dump Shaders option bool useDump = shaderCache->IsDump(); @@ -167,12 +209,12 @@ void AdvancedSettingsRenderer::RenderShaderDebugSection() if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text("Clear all compiled shaders from memory. Forces recompilation of all shaders on next use."); } +} - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); +void AdvancedSettingsRenderer::RenderShaderReplacementTable() +{ + auto state = globals::state; - // Shader Replacement section Util::DrawSectionHeader("Replace Original Shaders"); if (ImGui::BeginTable("##ReplaceToggles", 3, ImGuiTableFlags_SizingStretchSame)) { @@ -213,16 +255,211 @@ void AdvancedSettingsRenderer::RenderShaderDebugSection() } ImGui::EndTable(); } +} + +void AdvancedSettingsRenderer::RenderShaderCompileStatistics() +{ + auto shaderCache = globals::shaderCache; - // Only show shader blocking section in developer mode - if (!globals::state->IsDeveloperMode()) { + if (!ImGui::TreeNodeEx("Statistics", ImGuiTreeNodeFlags_DefaultOpen)) { return; } + ImGui::Text(std::format("Shader Compiler : {}", shaderCache->GetShaderStatsString()).c_str()); + + // Derived parallelism metrics are computed lazily on demand and only shown + // once compilation has completed to avoid per-frame analysis while compiling. + if (!shaderCache->IsCompiling()) { + auto parallelism = shaderCache->GetParallelismStats(); + if (parallelism.has_value()) { + const auto& p = parallelism.value(); + ImGui::Spacing(); + ImGui::TextDisabled("Parallelism (derived from %zu compiled tasks)", p.sampleCount); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Computed lazily from the last completed build."); + ImGui::Text("Only evaluated when this Statistics section is open."); + } + ImGui::Text("Work (W, sum of task wall times): %s", Util::FormatDuration(p.workMs).c_str()); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Total compile work: sum of all per-shader wall-clock compile times."); + ImGui::Text("This is not CPU time; it is accumulated task elapsed time."); + ImGui::Text("Equivalent serial time on one worker if overhead stayed the same."); + } + ImGui::Text("Span (S, longest): %s", Util::FormatDuration(p.spanMs).c_str()); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Critical-path lower bound, approximated by the single slowest shader."); + ImGui::Text("Even infinite cores cannot finish faster than this."); + } + ImGui::Text("Makespan (T_p): %s", Util::FormatDuration(p.makespanMs).c_str()); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Observed wall-clock duration for the full shader build."); + } + ImGui::Text("Queue wait (avg/max): %s / %s", + Util::FormatDuration(p.avgQueueWaitMs).c_str(), + Util::FormatDuration(p.maxQueueWaitMs).c_str()); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Time spent waiting in the ready queue before a worker started compilation."); + ImGui::Text("Useful for identifying scheduler-induced delay separate from compile cost."); + } + ImGui::Text("Average parallelism (W/S): %.2fx", p.avgParallelism); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Average useful concurrency in this workload."); + ImGui::Text("Roughly the worker count where adding more cores gives diminishing returns."); + } + ImGui::Text("Infinite-core efficiency (S/T_p): %.1f%%", 100.0 * p.infiniteCoreEfficiency); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("How close runtime is to the infinite-core lower bound."); + ImGui::Text("100%% means T_p == S."); + } + ImGui::Text("Infinite-core gap: %.1f%%", p.infiniteCoreGapPercent); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Distance from ideal infinite-core time."); + ImGui::Text("Defined as 100 * (1 - S / T_p). Lower is better."); + } + + ImGui::Spacing(); + ImGui::TextDisabled("Infinite-core efficiency"); + float efficiency = static_cast(std::clamp(p.infiniteCoreEfficiency, 0.0, 1.0)); + ImGui::ProgressBar(efficiency, ImVec2(-1.0f, 0.0f), std::format("{:.1f}% efficient / {:.1f}% gap", 100.0 * p.infiniteCoreEfficiency, p.infiniteCoreGapPercent).c_str()); + + ImGui::Spacing(); + ImGui::TextDisabled("Relative durations (normalized)"); + double maxMs = std::max({ p.workMs, p.spanMs, p.makespanMs, 1.0 }); + auto drawRelativeBar = [maxMs](const char* label, double value) { + float ratio = static_cast(std::clamp(value / maxMs, 0.0, 1.0)); + ImGui::TextUnformatted(label); + ImGui::SameLine(); + ImGui::ProgressBar(ratio, ImVec2(-1.0f, 0.0f), std::format("{} ({:.1f}%)", Util::FormatDuration(value), 100.0 * ratio).c_str()); + }; + drawRelativeBar("Span (S)", p.spanMs); + drawRelativeBar("Makespan (T_p)", p.makespanMs); + drawRelativeBar("Work (W)", p.workMs); + } + } + + // Top-3 slowest shaders from the last build + auto topSlow = shaderCache->GetTopSlowTasks(3); + if (!topSlow.empty()) { + ImGui::Spacing(); + ImGui::TextDisabled("Top %zu Slowest Shaders (last build)", topSlow.size()); + for (size_t i = 0; i < topSlow.size(); ++i) { + const auto& rec = topSlow[i]; + ImGui::Text("#%zu %s (weight %d)", i + 1, + Util::FormatDuration(rec.elapsedMs).c_str(), rec.priority); + ImGui::SameLine(); + ImGui::TextDisabled("%s", rec.key.c_str()); + if (ImGui::IsItemHovered()) { + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("%s", rec.key.c_str()); + } + } + // Allow copying the full key with a right-click + if (ImGui::BeginPopupContextItem(std::format("##slowcopy{}", i).c_str())) { + if (ImGui::MenuItem("Copy key")) { + ImGui::SetClipboardText(rec.key.c_str()); + } + ImGui::EndPopup(); + } + } + } + + ImGui::TreePop(); +} + +// ----------------------------------------------------------------------------- +// Diagnostics tab +// ----------------------------------------------------------------------------- + +void AdvancedSettingsRenderer::RenderDiagnosticsSection() +{ + RenderLoggingControls(); + ImGui::Spacing(); ImGui::Separator(); ImGui::Spacing(); + RenderRuntimeDebugControls(); + + // Shader blocking only meaningful in developer mode (matches prior behavior). + if (globals::state->IsDeveloperMode()) { + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + + RenderShaderBlockingPanel(); + } +} + +void AdvancedSettingsRenderer::RenderLoggingControls() +{ + Util::DrawSectionHeader("Logging"); + + // Log Level selection + spdlog::level::level_enum logLevel = globals::state->GetLogLevel(); + const char* items[] = { + "trace", + "debug", + "info", + "warn", + "err", + "critical", + "off" + }; + static int item_current = static_cast(logLevel); + if (ImGui::Combo("Log Level", &item_current, items, IM_ARRAYSIZE(items))) { + ImGui::SameLine(); + globals::state->SetLogLevel(static_cast(item_current)); + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Log level. Trace is most verbose. Default is info."); + } + + ImGui::Columns(2, nullptr, false); + + // Dump Ini Settings button + if (ImGui::Button("Dump Ini Settings", { -1, 0 })) { + Util::DumpSettingsOptions(); + } + + ImGui::NextColumn(); + + // Open Logs button + std::filesystem::path logPath = Util::PathHelpers::GetLogPath(); + if (!logPath.empty() && ImGui::Button("Open Logs", { -1, 0 })) { + ShellExecuteA(NULL, "open", logPath.string().c_str(), NULL, NULL, SW_SHOWNORMAL); + } + + ImGui::Columns(1); +} + +void AdvancedSettingsRenderer::RenderRuntimeDebugControls() +{ + Util::DrawSectionHeader("Runtime Debug"); + + // Frame annotations toggle + ImGui::Checkbox("Frame Annotations", &globals::state->frameAnnotations); + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("Enable detailed frame annotations for debugging render passes and draw calls."); + } + + // Debug addresses section + if (ImGui::TreeNodeEx("Addresses")) { + auto Renderer = globals::game::renderer; + auto BSShaderAccumulator = *globals::game::currentAccumulator.get(); + auto RendererShadowState = globals::game::shadowState; + ADDRESS_NODE(Renderer) + ADDRESS_NODE(BSShaderAccumulator) + ADDRESS_NODE(RendererShadowState) + ImGui::TreePop(); + } +} + +void AdvancedSettingsRenderer::RenderShaderBlockingPanel() +{ + auto shaderCache = globals::shaderCache; + + Util::DrawSectionHeader("Shader Blocking"); + // Show blocked shader status as a regular section if (!shaderCache->blockedKey.empty()) { // Create a visually distinct box for the blocked shader info with rounded corners and border @@ -286,8 +523,8 @@ void AdvancedSettingsRenderer::RenderShaderDebugSection() ImGui::PopStyleColor(); // ChildBg } - // Shader Debug section - if (ImGui::CollapsingHeader("Shader Debug")) { + // Blocking hotkeys + enable toggle + { auto menu = globals::menu; auto& menuSettings = menu->GetSettings(); auto& themeSettings = menuSettings.Theme; @@ -338,9 +575,11 @@ void AdvancedSettingsRenderer::RenderShaderDebugSection() } } - // Active shaders list - if (ImGui::CollapsingHeader("Active Shaders", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Text("Active Shaders (Used Recently)"); + // Active shaders list — rendered inline; the parent panel already says + // "Shader Blocking", so a nested CollapsingHeader was redundant noise. + { + ImGui::Spacing(); + Util::DrawSectionHeader("Active Shaders (Used Recently)"); if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text( "List of shaders that have been used in recent frames. " @@ -504,189 +743,31 @@ void AdvancedSettingsRenderer::RenderShaderDebugSection() } } +// ----------------------------------------------------------------------------- +// Disable at Boot tab +// ----------------------------------------------------------------------------- + void AdvancedSettingsRenderer::RenderDisableAtBootSection(const std::function& drawDisableAtBootSettings) { drawDisableAtBootSettings(); } -void AdvancedSettingsRenderer::RenderDeveloperSection() -{ - auto shaderCache = globals::shaderCache; - - // File Watcher option (moved from Advanced/Logging) - bool useFileWatcher = shaderCache->UseFileWatcher(); - if (ImGui::Checkbox("Enable File Watcher", &useFileWatcher)) { - shaderCache->SetFileWatcher(useFileWatcher); - } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Automatically recompile shaders on file change. " - "Intended for developing."); - } - - // Debug addresses section (moved from Advanced/Logging) - if (ImGui::TreeNodeEx("Addresses")) { - auto Renderer = globals::game::renderer; - auto BSShaderAccumulator = *globals::game::currentAccumulator.get(); - auto RendererShadowState = globals::game::shadowState; - ADDRESS_NODE(Renderer) - ADDRESS_NODE(BSShaderAccumulator) - ADDRESS_NODE(RendererShadowState) - ImGui::TreePop(); - } - - // Statistics section (moved from Advanced/Logging) - if (ImGui::TreeNodeEx("Statistics", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Text(std::format("Shader Compiler : {}", shaderCache->GetShaderStatsString()).c_str()); - - // Derived parallelism metrics are computed lazily on demand and only shown - // once compilation has completed to avoid per-frame analysis while compiling. - if (!shaderCache->IsCompiling()) { - auto parallelism = shaderCache->GetParallelismStats(); - if (parallelism.has_value()) { - const auto& p = parallelism.value(); - ImGui::Spacing(); - ImGui::TextDisabled("Parallelism (derived from %zu compiled tasks)", p.sampleCount); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Computed lazily from the last completed build."); - ImGui::Text("Only evaluated when this Statistics section is open."); - } - ImGui::Text("Work (W, sum of task wall times): %s", Util::FormatDuration(p.workMs).c_str()); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Total compile work: sum of all per-shader wall-clock compile times."); - ImGui::Text("This is not CPU time; it is accumulated task elapsed time."); - ImGui::Text("Equivalent serial time on one worker if overhead stayed the same."); - } - ImGui::Text("Span (S, longest): %s", Util::FormatDuration(p.spanMs).c_str()); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Critical-path lower bound, approximated by the single slowest shader."); - ImGui::Text("Even infinite cores cannot finish faster than this."); - } - ImGui::Text("Makespan (T_p): %s", Util::FormatDuration(p.makespanMs).c_str()); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Observed wall-clock duration for the full shader build."); - } - ImGui::Text("Queue wait (avg/max): %s / %s", - Util::FormatDuration(p.avgQueueWaitMs).c_str(), - Util::FormatDuration(p.maxQueueWaitMs).c_str()); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Time spent waiting in the ready queue before a worker started compilation."); - ImGui::Text("Useful for identifying scheduler-induced delay separate from compile cost."); - } - ImGui::Text("Average parallelism (W/S): %.2fx", p.avgParallelism); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Average useful concurrency in this workload."); - ImGui::Text("Roughly the worker count where adding more cores gives diminishing returns."); - } - ImGui::Text("Infinite-core efficiency (S/T_p): %.1f%%", 100.0 * p.infiniteCoreEfficiency); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("How close runtime is to the infinite-core lower bound."); - ImGui::Text("100%% means T_p == S."); - } - ImGui::Text("Infinite-core gap: %.1f%%", p.infiniteCoreGapPercent); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Distance from ideal infinite-core time."); - ImGui::Text("Defined as 100 * (1 - S / T_p). Lower is better."); - } - - ImGui::Spacing(); - ImGui::TextDisabled("Infinite-core efficiency"); - float efficiency = static_cast(std::clamp(p.infiniteCoreEfficiency, 0.0, 1.0)); - ImGui::ProgressBar(efficiency, ImVec2(-1.0f, 0.0f), std::format("{:.1f}% efficient / {:.1f}% gap", 100.0 * p.infiniteCoreEfficiency, p.infiniteCoreGapPercent).c_str()); - - ImGui::Spacing(); - ImGui::TextDisabled("Relative durations (normalized)"); - double maxMs = std::max({ p.workMs, p.spanMs, p.makespanMs, 1.0 }); - auto drawRelativeBar = [maxMs](const char* label, double value) { - float ratio = static_cast(std::clamp(value / maxMs, 0.0, 1.0)); - ImGui::TextUnformatted(label); - ImGui::SameLine(); - ImGui::ProgressBar(ratio, ImVec2(-1.0f, 0.0f), std::format("{} ({:.1f}%)", Util::FormatDuration(value), 100.0 * ratio).c_str()); - }; - drawRelativeBar("Span (S)", p.spanMs); - drawRelativeBar("Makespan (T_p)", p.makespanMs); - drawRelativeBar("Work (W)", p.workMs); - } - } +// ----------------------------------------------------------------------------- +// Testing tab +// ----------------------------------------------------------------------------- - // Top-3 slowest shaders from the last build - auto topSlow = shaderCache->GetTopSlowTasks(3); - if (!topSlow.empty()) { - ImGui::Spacing(); - ImGui::TextDisabled("Top %zu Slowest Shaders (last build)", topSlow.size()); - for (size_t i = 0; i < topSlow.size(); ++i) { - const auto& rec = topSlow[i]; - ImGui::Text("#%zu %s (weight %d)", i + 1, - Util::FormatDuration(rec.elapsedMs).c_str(), rec.priority); - ImGui::SameLine(); - ImGui::TextDisabled("%s", rec.key.c_str()); - if (ImGui::IsItemHovered()) { - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("%s", rec.key.c_str()); - } - } - // Allow copying the full key with a right-click - if (ImGui::BeginPopupContextItem(std::format("##slowcopy{}", i).c_str())) { - if (ImGui::MenuItem("Copy key")) { - ImGui::SetClipboardText(rec.key.c_str()); - } - ImGui::EndPopup(); - } - } - } - - ImGui::TreePop(); - } - - // Frame annotations toggle (moved from Advanced/Logging) - ImGui::Checkbox("Frame Annotations", &globals::state->frameAnnotations); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Enable detailed frame annotations for debugging render passes and draw calls."); - } - - // Half-precision (partial precision) shader compile flag - bool partialPrecision = globals::state->enablePartialPrecision.load(std::memory_order_relaxed); - if (ImGui::Checkbox("Half Precision (Partial Precision)", &partialPrecision)) { - globals::state->enablePartialPrecision.store(partialPrecision, std::memory_order_relaxed); - // Force a recompile so the flag actually takes effect on subsequent shader builds. - globals::shaderCache->Clear(); - } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Adds D3DCOMPILE_PARTIAL_PRECISION to the shader compiler flags.\n" - "Lets fxc downgrade unmarked float ops to FP16 where it can prove safety, " - "on top of the existing min16float type hints.\n" - "On FP16-capable GPUs (Pascal+ / GCN+ / Skylake+) this can halve register " - "pressure and double ALU throughput, but it can also introduce minor visual " - "differences in shaders that haven't been audited for precision sensitivity.\n" - "Toggling this clears the shader cache and triggers a full recompile."); - } - - // Avoid flow control compiler flag (transient — not saved to config because the - // right setting depends on the current scene, not the user). - bool avoidFlowControl = globals::state->enableAvoidFlowControl.load(std::memory_order_relaxed); - if (ImGui::Checkbox("Avoid Flow Control", &avoidFlowControl)) { - globals::state->enableAvoidFlowControl.store(avoidFlowControl, std::memory_order_relaxed); - // Force a recompile so the flag actually takes effect on subsequent shader builds. - globals::shaderCache->Clear(); - } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text( - "Adds D3DCOMPILE_AVOID_FLOW_CONTROL to the shader compiler flags.\n" - "Forces fxc to flatten branches into predicated ops rather than emitting " - "dynamic flow control. Often a win for short branch bodies and uniformly-" - "taken branches; usually a loss for long divergent branches that vanilla " - "flow control would skip entirely.\n" - "Resets every launch. Toggling this clears the shader cache and triggers a " - "full recompile."); - } - - ImGui::Spacing(); - ImGui::Separator(); - ImGui::Spacing(); +void AdvancedSettingsRenderer::RenderTestingSection() +{ + // A/B Testing settings + auto* abTestingManager = ABTestingManager::GetSingleton(); + abTestingManager->DrawSettingsUI(); - // Developer Mode Testing Section + // Developer Mode Testing UI + scene-prep button (previously on the "Developer" tab) if (globals::state->IsDeveloperMode()) { + ImGui::Spacing(); + ImGui::Separator(); + ImGui::Spacing(); + FeatureIssues::Test::DrawDeveloperModeTestingUI(); ImGui::Spacing(); @@ -704,10 +785,3 @@ void AdvancedSettingsRenderer::RenderDeveloperSection() } } } - -void AdvancedSettingsRenderer::RenderTestingSection() -{ - // A/B Testing settings - auto* abTestingManager = ABTestingManager::GetSingleton(); - abTestingManager->DrawSettingsUI(); -} diff --git a/src/Menu/AdvancedSettingsRenderer.h b/src/Menu/AdvancedSettingsRenderer.h index 086486c246..eaf6cac0e4 100644 --- a/src/Menu/AdvancedSettingsRenderer.h +++ b/src/Menu/AdvancedSettingsRenderer.h @@ -13,9 +13,19 @@ class AdvancedSettingsRenderer const std::function& drawDisableAtBootSettings); private: - static void RenderLoggingSection(); - static void RenderShaderDebugSection(); + static void RenderShadersSection(); + static void RenderDiagnosticsSection(); static void RenderDisableAtBootSection(const std::function& drawDisableAtBootSettings); - static void RenderDeveloperSection(); static void RenderTestingSection(); -}; \ No newline at end of file + + // Helpers used by the sections above + static void RenderShaderCompileFlags(); + static void RenderShaderThreading(); + static void RenderShaderCacheControls(); + static void RenderShaderReplacementTable(); + static void RenderShaderCompileStatistics(); + + static void RenderLoggingControls(); + static void RenderRuntimeDebugControls(); + static void RenderShaderBlockingPanel(); +}; diff --git a/src/Menu/SettingsTabRenderer.cpp b/src/Menu/SettingsTabRenderer.cpp index 61c85958c3..43bc5fdb14 100644 --- a/src/Menu/SettingsTabRenderer.cpp +++ b/src/Menu/SettingsTabRenderer.cpp @@ -249,16 +249,6 @@ void SettingsTabRenderer::RenderShadersTab() ImGui::Text("Skips a shader being replaced if it hasn't been compiled yet. Also makes compilation blazingly fast!"); } - // Skip confirmation when clearing shader cache - auto& menuSettings = globals::menu->GetSettings(); - bool skipConfirmation = menuSettings.SkipClearCacheConfirmation; - if (ImGui::Checkbox("Skip Clear Cache Dialogue", &skipConfirmation)) { - menuSettings.SkipClearCacheConfirmation = skipConfirmation; - } - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("When checked, the shader cache will be cleared immediately without asking for confirmation."); - } - if (shaderCache->GetTotalTasks() > 0) { ImGui::Text("Last shader cache build duration: %s", shaderCache->GetShaderStatsString(true, true).c_str()); @@ -463,6 +453,16 @@ void SettingsTabRenderer::RenderBehaviorTab() ImGui::TextUnformatted("Time in seconds to wait before a tooltip appears when hovering over an item."); } + // Skip confirmation when clearing shader cache (UI behavior, not a shader setting). + auto& menuSettings = globals::menu->GetSettings(); + bool skipConfirmation = menuSettings.SkipClearCacheConfirmation; + if (ImGui::Checkbox("Skip Clear Cache Dialogue", &skipConfirmation)) { + menuSettings.SkipClearCacheConfirmation = skipConfirmation; + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text("When checked, the shader cache will be cleared immediately without asking for confirmation."); + } + SeparatorTextWithFont("Visual Effects", Menu::FontRole::Subheading); if (ImGui::Checkbox("Background Blur", &themeSettings.BackgroundBlurEnabled)) { From e20499ecd208356607bb6980a99b55af46e9f389 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Wed, 20 May 2026 00:29:01 -0700 Subject: [PATCH 2/6] fix(menu): drop format-string interpretation + stray SameLine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses CodeRabbit findings on PR #28: - RenderShaderCompileStatistics passed the result of std::format to ImGui::Text via .c_str(); ImGui::Text treats its first arg as a printf format string, so any '%' in the shader stats would be interpreted. Use the "%s" + .c_str() form directly. - The Log Level combo had an ImGui::SameLine() inside the change branch, before a non-widget call — dead layout op. Co-Authored-By: Claude Opus 4.7 --- src/Menu/AdvancedSettingsRenderer.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Menu/AdvancedSettingsRenderer.cpp b/src/Menu/AdvancedSettingsRenderer.cpp index 7a121475eb..f2099c6c39 100644 --- a/src/Menu/AdvancedSettingsRenderer.cpp +++ b/src/Menu/AdvancedSettingsRenderer.cpp @@ -265,7 +265,7 @@ void AdvancedSettingsRenderer::RenderShaderCompileStatistics() return; } - ImGui::Text(std::format("Shader Compiler : {}", shaderCache->GetShaderStatsString()).c_str()); + ImGui::Text("Shader Compiler : %s", shaderCache->GetShaderStatsString().c_str()); // Derived parallelism metrics are computed lazily on demand and only shown // once compilation has completed to avoid per-frame analysis while compiling. @@ -407,7 +407,6 @@ void AdvancedSettingsRenderer::RenderLoggingControls() }; static int item_current = static_cast(logLevel); if (ImGui::Combo("Log Level", &item_current, items, IM_ARRAYSIZE(items))) { - ImGui::SameLine(); globals::state->SetLogLevel(static_cast(item_current)); } if (auto _tt = Util::HoverTooltipWrapper()) { From 0ae8e80c3480ae078fae19a5c9957b44d99768a1 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Wed, 20 May 2026 01:41:24 -0700 Subject: [PATCH 3/6] fix(menu): drop static log-level cache; align skip-clear-cache label Copilot findings on PR #28: - RenderLoggingControls cached the log-level combo index in a static initialized only on first call. If anything else changed the log level (config reload, console command, another caller of SetLogLevel), the combo kept showing the old value and re-selecting the same item would silently write the wrong level back. Drop the static and read globals::state->GetLogLevel() each frame. - The "Skip Clear Cache Dialogue" checkbox label conflicted with the underlying SkipClearCacheConfirmation field and used "dialogue" (a conversation) where "dialog" / "confirmation" was meant. Rename to "Skip Clear Cache Confirmation" to match the field. Co-Authored-By: Claude Opus 4.7 --- src/Menu/AdvancedSettingsRenderer.cpp | 6 ++++-- src/Menu/SettingsTabRenderer.cpp | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Menu/AdvancedSettingsRenderer.cpp b/src/Menu/AdvancedSettingsRenderer.cpp index f2099c6c39..5d23bd7b37 100644 --- a/src/Menu/AdvancedSettingsRenderer.cpp +++ b/src/Menu/AdvancedSettingsRenderer.cpp @@ -394,7 +394,9 @@ void AdvancedSettingsRenderer::RenderLoggingControls() { Util::DrawSectionHeader("Logging"); - // Log Level selection + // Log Level selection. Resync from state every frame so external changes + // (config reload, console command, another caller of SetLogLevel) don't + // leave the combo displaying a stale selection. spdlog::level::level_enum logLevel = globals::state->GetLogLevel(); const char* items[] = { "trace", @@ -405,7 +407,7 @@ void AdvancedSettingsRenderer::RenderLoggingControls() "critical", "off" }; - static int item_current = static_cast(logLevel); + int item_current = static_cast(logLevel); if (ImGui::Combo("Log Level", &item_current, items, IM_ARRAYSIZE(items))) { globals::state->SetLogLevel(static_cast(item_current)); } diff --git a/src/Menu/SettingsTabRenderer.cpp b/src/Menu/SettingsTabRenderer.cpp index 43bc5fdb14..d639d3ffd7 100644 --- a/src/Menu/SettingsTabRenderer.cpp +++ b/src/Menu/SettingsTabRenderer.cpp @@ -456,7 +456,7 @@ void SettingsTabRenderer::RenderBehaviorTab() // Skip confirmation when clearing shader cache (UI behavior, not a shader setting). auto& menuSettings = globals::menu->GetSettings(); bool skipConfirmation = menuSettings.SkipClearCacheConfirmation; - if (ImGui::Checkbox("Skip Clear Cache Dialogue", &skipConfirmation)) { + if (ImGui::Checkbox("Skip Clear Cache Confirmation", &skipConfirmation)) { menuSettings.SkipClearCacheConfirmation = skipConfirmation; } if (auto _tt = Util::HoverTooltipWrapper()) { From 9bfa371639257a11bbc59e6ede8a82453091b5c4 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Wed, 20 May 2026 22:27:03 -0700 Subject: [PATCH 4/6] fix(menu): capitalize INIs label for acronym consistency Copilot finding on PR #28: "Inis" reads as a word; everywhere else in the file the acronym is rendered "INIs" ("test INI files", "the INI" etc.). Align the button label and its tooltip reference. Co-Authored-By: Claude Opus 4.7 --- src/FeatureIssues.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/FeatureIssues.cpp b/src/FeatureIssues.cpp index 2a600ebf41..64362979d5 100644 --- a/src/FeatureIssues.cpp +++ b/src/FeatureIssues.cpp @@ -1487,7 +1487,7 @@ namespace FeatureIssues themeSettings.StatusPalette.RestartNeeded, themeSettings.StatusPalette.CurrentHotkey); - if (ImGui::Button("Create Test Inis", { -1, 0 })) { + if (ImGui::Button("Create Test INIs", { -1, 0 })) { auto testInis = CreateTestInis(); logger::info("Created {} test INI files for feature issue testing", testInis.size()); } @@ -1523,7 +1523,7 @@ namespace FeatureIssues if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text( "Removes all test INI files and restores any modified INI files to their original state.\n" - "This undoes all changes made by 'Create Test Inis'.\n" + "This undoes all changes made by 'Create Test INIs'.\n" "Restart CS after restoring to see normal operation."); } } From 1756a7920b008aab056b521f8fb64648ee7ff4bb Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Thu, 21 May 2026 22:18:28 -0700 Subject: [PATCH 5/6] fix(menu): use local shaderCache, guard hardware_concurrency==0 Address Copilot review comments on PR #28: - Use the local `shaderCache` alias consistently inside RenderShaderCompileFlags() instead of redundantly dereferencing globals::shaderCache for the two Clear() calls. - Clamp std::thread::hardware_concurrency() to at least 1 before using it as the SliderInt max, since the standard permits it to return 0 if the implementation can't detect core count, which would produce an invalid slider range (min=1, max=0) and trip ImGui assertions. --- src/Menu/AdvancedSettingsRenderer.cpp | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/Menu/AdvancedSettingsRenderer.cpp b/src/Menu/AdvancedSettingsRenderer.cpp index 5d23bd7b37..7265b79a09 100644 --- a/src/Menu/AdvancedSettingsRenderer.cpp +++ b/src/Menu/AdvancedSettingsRenderer.cpp @@ -121,7 +121,7 @@ void AdvancedSettingsRenderer::RenderShaderCompileFlags() if (ImGui::Checkbox("Half Precision (Partial Precision)", &partialPrecision)) { globals::state->enablePartialPrecision.store(partialPrecision, std::memory_order_relaxed); // Force a recompile so the flag actually takes effect on subsequent shader builds. - globals::shaderCache->Clear(); + shaderCache->Clear(); } if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text( @@ -140,7 +140,7 @@ void AdvancedSettingsRenderer::RenderShaderCompileFlags() if (ImGui::Checkbox("Avoid Flow Control", &avoidFlowControl)) { globals::state->enableAvoidFlowControl.store(avoidFlowControl, std::memory_order_relaxed); // Force a recompile so the flag actually takes effect on subsequent shader builds. - globals::shaderCache->Clear(); + shaderCache->Clear(); } if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text( @@ -160,14 +160,19 @@ void AdvancedSettingsRenderer::RenderShaderThreading() Util::DrawSectionHeader("Threading"); - ImGui::SliderInt("Compiler Threads", &shaderCache->compilationThreadCount, 1, static_cast(std::thread::hardware_concurrency())); + // hardware_concurrency() is permitted to return 0 if the implementation can't + // detect it; clamp to at least 1 so the slider range (min=1, max=N) stays valid + // and ImGui doesn't assert. + const int32_t maxThreads = static_cast(std::max(1u, std::thread::hardware_concurrency())); + + ImGui::SliderInt("Compiler Threads", &shaderCache->compilationThreadCount, 1, maxThreads); if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text( "Number of threads used to compile shaders at startup. " "Defaults to all logical cores minus one for OS headroom (E-cores included). " "Higher values finish compilation faster but may make the system less responsive."); } - ImGui::SliderInt("Background Compiler Threads", &shaderCache->backgroundCompilationThreadCount, 1, static_cast(std::thread::hardware_concurrency())); + ImGui::SliderInt("Background Compiler Threads", &shaderCache->backgroundCompilationThreadCount, 1, maxThreads); if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text( "Number of threads used to compile shaders during gameplay. " From d1ca93582f5e8ad84bcfe227856bda984b39731c Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Thu, 21 May 2026 22:33:07 -0700 Subject: [PATCH 6/6] fix(menu): richer thread-count fallback; clamp persisted values; grammar Address follow-up review comments on PR #28: - maxThreads now falls back to the actual compile-pool thread count (which itself has a sensible default) when hardware_concurrency() returns 0, instead of pinning users to 1 thread on platforms where the OS query fails. - compilationThreadCount and backgroundCompilationThreadCount are snapped back into [1, maxThreads] before being passed to the slider, so a stale persisted config can't render the slider in an out-of-range state. - Tooltip wording: "Intended for developing." -> "Intended for development." --- src/Menu/AdvancedSettingsRenderer.cpp | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/Menu/AdvancedSettingsRenderer.cpp b/src/Menu/AdvancedSettingsRenderer.cpp index 7265b79a09..0738816f32 100644 --- a/src/Menu/AdvancedSettingsRenderer.cpp +++ b/src/Menu/AdvancedSettingsRenderer.cpp @@ -161,9 +161,19 @@ void AdvancedSettingsRenderer::RenderShaderThreading() Util::DrawSectionHeader("Threading"); // hardware_concurrency() is permitted to return 0 if the implementation can't - // detect it; clamp to at least 1 so the slider range (min=1, max=N) stays valid - // and ImGui doesn't assert. - const int32_t maxThreads = static_cast(std::max(1u, std::thread::hardware_concurrency())); + // detect it. Fall back to the actual compile-pool thread count we ended up + // using at startup (which itself defaults to a sensible value when the OS + // query fails), then clamp to at least 1 so the slider range (min=1, max=N) + // stays valid and ImGui doesn't assert. + const uint32_t hwThreads = std::thread::hardware_concurrency(); + const int32_t poolThreads = static_cast(shaderCache->compilationPool.get_thread_count()); + const int32_t maxThreads = std::max({ 1, poolThreads, static_cast(hwThreads) }); + + // Snap the persisted values back into the valid range — a stale config can + // otherwise leave compilationThreadCount above maxThreads, which would + // render the slider in an out-of-range state. + shaderCache->compilationThreadCount = std::clamp(shaderCache->compilationThreadCount, 1, maxThreads); + shaderCache->backgroundCompilationThreadCount = std::clamp(shaderCache->backgroundCompilationThreadCount, 1, maxThreads); ImGui::SliderInt("Compiler Threads", &shaderCache->compilationThreadCount, 1, maxThreads); if (auto _tt = Util::HoverTooltipWrapper()) { @@ -195,7 +205,7 @@ void AdvancedSettingsRenderer::RenderShaderCacheControls() if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text( "Automatically recompile shaders on file change. " - "Intended for developing."); + "Intended for development."); } // Dump Shaders option