diff --git a/src/Features/RenderDoc.cpp b/src/Features/RenderDoc.cpp index 9baa3c1568..d02dfb21ae 100644 --- a/src/Features/RenderDoc.cpp +++ b/src/Features/RenderDoc.cpp @@ -147,12 +147,14 @@ void RenderDoc::DrawSettings() globals::state->frameAnnotations = globals::state->useFrameAnnotations; } } - Util::UI::DrawSettingDiff(bootSnapshot, settings, &Settings::enableCapture); - - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Enable RenderDoc frame capture for providing debug captures to the Open Shaders team (or upstream Community Shaders for upstream-relevant issues)."); - ImGui::Text("Enabling capture will force-enable frame annotations for easier debugging and will restore the previous setting when disabled."); - } + // enableCapture is restart-gated (renderdoc.dll only injects at boot). The + // helper renders the tooltip first so it attaches to the checkbox, then the + // pending banner below it -- the previous ordering drew the banner between + // the checkbox and the tooltip, so a pending banner would steal the hover. + Util::UI::RestartGatedAnnotate(bootSnapshot, settings, &Settings::enableCapture, [] { + ImGui::TextUnformatted("Enable RenderDoc frame capture for providing debug captures to the Open Shaders team (or upstream Community Shaders for upstream-relevant issues)."); + ImGui::TextUnformatted("Enabling capture will force-enable frame annotations for easier debugging and will restore the previous setting when disabled."); + }); // The rest of the UI renders only when capture is active bool renderDocCaptureEnabled = settings.enableCapture; diff --git a/src/Features/Upscaling.cpp b/src/Features/Upscaling.cpp index 660bd9da35..393a4d019f 100644 --- a/src/Features/Upscaling.cpp +++ b/src/Features/Upscaling.cpp @@ -329,6 +329,10 @@ void Upscaling::DrawSettings() ImGui::Checkbox("Render engine at upscaled resolution", &settings.renderAtUpscaleRes); if (!methodSupportsPerf) ImGui::EndDisabled(); + // Hover tooltip always renders (so users learn what the option does + // even when greyed out). The pending-restart banner only fires when + // DLSS is the active upscaler -- the feature can't take effect + // otherwise, so a "pending restart" hint there would mislead. if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text( "When enabled, the engine pipeline allocates render targets at the upscaled-render\n" @@ -336,9 +340,9 @@ void Upscaling::DrawSettings() "its output to a private DisplayRes texture. Substantial VRAM and bandwidth savings,\n" "especially at high HMD resolutions.\n" "\n" - "Requires DLSS or FSR. Restart required to enable/disable. Method and Upscale\n" - "Preset changes also require a restart while this is active; sharpness / model preset\n" - "/ Reflex remain live."); + "Requires DLSS or FSR. Toggling this option requires a game restart to take effect.\n" + "While active, Method and Upscale Preset changes also require a restart;\n" + "sharpness / model preset / Reflex remain live."); } if (!methodSupportsPerf && settings.renderAtUpscaleRes) Util::Text::Disabled("Render-at-upscaled-resolution requires DLSS or FSR — switch upscaler Method to activate."); @@ -372,7 +376,10 @@ void Upscaling::DrawSettings() bool fgEnabled = settings.frameGenerationMode != 0; if (ImGui::Checkbox("Frame Generation", &fgEnabled)) settings.frameGenerationMode = fgEnabled ? 1 : 0; - Util::UI::DrawSettingDiff(bootSnapshot, settings, &Settings::frameGenerationMode); + Util::UI::RestartGatedAnnotate(bootSnapshot, settings, &Settings::frameGenerationMode, + "Interpolate real frames with generated ones for a smoother experience. Uses AMD FSR Frame\n" + "Generation. Requires a D3D11-to-D3D12 proxy swapchain which can introduce compatibility\n" + "issues; in particular, frame generation works only in windowed mode."); if (!frameGenerationDx12PathActive) ImGui::BeginDisabled(); @@ -388,7 +395,10 @@ void Upscaling::DrawSettings() bool fgForce = settings.frameGenerationForceEnable != 0; if (ImGui::Checkbox("Force Enable Frame Generation", &fgForce)) settings.frameGenerationForceEnable = fgForce ? 1 : 0; - Util::UI::DrawSettingDiff(bootSnapshot, settings, &Settings::frameGenerationForceEnable); + Util::UI::RestartGatedAnnotate(bootSnapshot, settings, &Settings::frameGenerationForceEnable, + "Bypass the high-refresh-rate monitor check so Frame Generation can run on lower-Hz\n" + "displays. Useful for laptops and older monitors at the cost of less headroom for the\n" + "generated frames."); ImGui::Checkbox("Frame Generation in Menus", &settings.frameGenerationAllowInMenus); if (auto _tt = Util::HoverTooltipWrapper()) { @@ -499,14 +509,17 @@ void Upscaling::DrawSettings() if (ImGui::TreeNodeEx("Backend Diagnostics")) { // Streamline log level selection const char* logLevels[] = { "Off", "Default", "Verbose" }; - int logLevelIdx = static_cast(settings.streamlineLogLevel); + // Clamp before use: streamlineLogLevel is JSON-persisted and could be out + // of range (or a value that overflows the int cast) on a stale or + // hand-edited config; an unclamped value would index logLevels OOB. + int logLevelIdx = std::clamp(static_cast(settings.streamlineLogLevel), + 0, IM_ARRAYSIZE(logLevels) - 1); if (ImGui::Combo("Streamline Logging", &logLevelIdx, logLevels, IM_ARRAYSIZE(logLevels))) { settings.streamlineLogLevel = static_cast(logLevelIdx); } - Util::UI::DrawSettingDiff(bootSnapshot, settings, &Settings::streamlineLogLevel); - if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Streamline logging controls the verbosity of NVIDIA Streamline backend logs. Useful for debugging issues with DLSS/DLSS-G."); - } + Util::UI::RestartGatedAnnotate(bootSnapshot, settings, &Settings::streamlineLogLevel, + "Verbosity of the NVIDIA Streamline backend logs. Useful for debugging issues with DLSS / " + "DLSS-G."); // VR Debug visualization -- per-eye buffers and native inputs if (globals::game::isVR) { diff --git a/src/Features/VolumetricLighting.cpp b/src/Features/VolumetricLighting.cpp index c7f9739194..b9dc846205 100644 --- a/src/Features/VolumetricLighting.cpp +++ b/src/Features/VolumetricLighting.cpp @@ -22,10 +22,13 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( void VolumetricLighting::DrawSettings() { + // VR pre-allocates VL render targets at boot, so a runtime toggle can't + // resize them -- gate only in VR. Non-VR resizes live. if (ImGui::Checkbox("Enable Volumetric Lighting in Exteriors", &settings.ExteriorEnabled)) SetupVL(); if (globals::game::isVR) - Util::UI::DrawSettingDiff(bootSnapshot, settings, &Settings::ExteriorEnabled); + Util::UI::RestartGatedAnnotate(bootSnapshot, settings, &Settings::ExteriorEnabled, + "Volumetric god-rays / fog scattering in exterior cells."); if (settings.ExteriorEnabled) DrawVolumetricLightingSettings(settings.ExteriorQuality, settings.ExteriorCustomSize, false, !inInterior); @@ -33,7 +36,8 @@ void VolumetricLighting::DrawSettings() if (ImGui::Checkbox("Enable Volumetric Lighting in Interiors", &settings.InteriorEnabled)) SetupVL(); if (globals::game::isVR) - Util::UI::DrawSettingDiff(bootSnapshot, settings, &Settings::InteriorEnabled); + Util::UI::RestartGatedAnnotate(bootSnapshot, settings, &Settings::InteriorEnabled, + "Volumetric god-rays / fog scattering in interior cells."); if (settings.InteriorEnabled) DrawVolumetricLightingSettings(settings.InteriorQuality, settings.InteriorCustomSize, true, inInterior); diff --git a/src/Utils/BootSnapshot.h b/src/Utils/BootSnapshot.h index 79a3bed2e8..743a771e70 100644 --- a/src/Utils/BootSnapshot.h +++ b/src/Utils/BootSnapshot.h @@ -98,9 +98,17 @@ namespace Util::Settings return *reinterpret_cast(reinterpret_cast(&bootCopy_) + offset); } + // T's bytes are memcmp'd, so a padded struct could false-positive on + // compare. All registered restart fields are bool/uint/float/enum (no + // padding); constrain with has_unique_object_representations_v if that changes. template bool HasPendingChange(const SettingsT& live, T SettingsT::* member) const noexcept { + // SettingsT may hold std::string, but a registered restart field must be + // trivially copyable -- memcmp on a std::string compares control blocks, not text. + static_assert(std::is_trivially_copyable_v, + "BootSnapshot::HasPendingChange requires a trivially-copyable field type " + "(memcmp on non-trivial types is not a meaningful equality check)."); if (!latched_) { return false; } @@ -115,6 +123,16 @@ namespace Util::Settings if (!latched_ || !field.jsonKey) { return false; } + // Defensive bounds check on the runtime-supplied descriptor: a + // malformed RestartFieldInfo (size zero, offset past the end, or + // offset+size extending beyond sizeof(SettingsT)) would otherwise + // drive memcmp out of bounds. Subtract instead of add to avoid + // overflow when offset+size would wrap size_t. + if (field.size == 0 || + field.offset > sizeof(SettingsT) || + field.size > sizeof(SettingsT) - field.offset) { + return false; + } return std::memcmp(reinterpret_cast(&bootCopy_) + field.offset, reinterpret_cast(&live) + field.offset, field.size) != 0; diff --git a/src/Utils/UI.h b/src/Utils/UI.h index 77289ada38..a59605200f 100644 --- a/src/Utils/UI.h +++ b/src/Utils/UI.h @@ -1070,7 +1070,12 @@ namespace Util } template - requires std::invocable + // body is invoked as an lvalue (`body()` below). Constrain on + // `Body&` so a non-const-callable, move-only, or otherwise + // lvalue-only invocable is not falsely rejected -- the prior + // `std::invocable` form tested invocation on a forwarded-as + // expression that may decay to rvalue and reject lvalue-only types. + requires std::invocable inline void RestartGatedAnnotate(const Util::Settings::BootSnapshot& snapshot, const SettingsT& live, T SettingsT::* field,