Skip to content

refactor: unify restart-required infrastructure#39

Merged
alandtse merged 17 commits into
devfrom
codex/unified-restart-required-settings
May 25, 2026
Merged

refactor: unify restart-required infrastructure#39
alandtse merged 17 commits into
devfrom
codex/unified-restart-required-settings

Conversation

@Codex
Copy link
Copy Markdown

@Codex Codex AI commented May 24, 2026

Thanks for asking me to work on this. I will get started on it and keep this PR's description up to date as I form a plan and make progress.


This section details on the original issue you should resolve

<issue_title>feat(settings): unified restart-required infrastructure + MCP introspection</issue_title>
<issue_description># Proposal: unified restart-required settings infrastructure

Motivation

Three feature surfaces in PR-2/PR-3 grew their own ad-hoc "pending restart"
machinery and per-field comparison logic:

  1. DLSSperf (Upscaling.cpp::DrawSettings + DLSSperf::HasBootSnapshot())
    — boot snapshot of upscaleMethod and qualityMode; banners when live
    settings.* differs from the boot value.
  2. DlssEnhancer (DlssEnhancer::enabledAtBoot, qualityModeAtBoot,
    LatchEnabled(), LatchQualityMode()) — same pattern, separate boot
    fields, separate "RESTART REQUIRED for this change to take effect" banner.
  3. Frame Generation (Upscaling.cpp Frame Gen section) — has the
    "Toggling this setting requires a restart to work correctly" hint but no
    actual diff-vs-boot detection.

Each implementation rewrites the same four moving parts:

  • a latch at boot call wired into the feature's PostPostLoad or first
    Setup pass,
  • a bootValue storage slot (or set of slots) on the feature,
  • a per-field comparison + colored banner in DrawSettings,
  • documentation in tooltips / banners that the change is restart-gated.

Net effect: ~50 lines of similar code per feature, easy to forget, hard to
keep consistent (DLSSperf banner phrasing vs DlssEnhancer banner phrasing vs
Frame Generation static hint).

There's also a step-up opportunity: today no feature surfaces "X is restart-
required; you cannot change it now even though the slider moves." The user
only learns this after they save settings and notice nothing changed.

Goals

  1. Single point of truth per field for whether a change is live, requires
    restart, or is boot-only.
  2. Automatic pending-diff banner with the existing
    Util::Text::RestartNeeded styling — features don't need to write the
    comparison and the format string each time.
  3. Optional auto-disable for boot-only fields (the slider is grey while
    the corresponding feature is locked at boot value).
  4. Backwards-compatible — feature Settings structs and JSON shapes stay
    untouched; opt-in per field.
  5. Small surface area — no central registry, no service locator; a
    header-only typed wrapper that drops into existing structs.

Proposed shape — pure-POD Settings + sibling metadata table

Only two lifecycles exist in practice:

  • Runtime — changes apply this frame. The default — implicit.
  • RestartRequired — change persists in JSON immediately, banner shows
    pending diff, takes effect at next launch. Opt-in via metadata table.

There is no BootOnly / disabled-slider lifecycle. Every restart-required
setting we have today (DLSSperf checkbox, qualityMode under DLSSperf,
frame-gen toggles, HDR enable) lets the user stage the change — disabling
the widget would be a regression.

Why metadata lives outside Settings

An earlier draft of this proposal had a RestartRequired<T> wrapper field
inside Settings. That breaks two important invariants:

  1. Constant-buffer layout. Several feature Settings structs are
    uploaded verbatim to GPU constant buffers (or have a 1:1 mirror struct
    that is). Any wrapper that adds inheritance (SettingValueBase) or
    per-field bookkeeping changes the in-memory size and breaks the cbuffer
    binding.
  2. JSON shape. NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT keys
    off field names and the order they're listed. A wrapper that round-trips
    "transparently" still risks accidental schema drift if the macro ever
    changes its expectations or if a feature uses a hand-rolled
    to_json/from_json.

Settings must stay a pure POD struct that any consumer — JSON
serializer, ImGui slider, cbuffer upload — can treat as a plain
collection of named fields. Restart-required metadata lives beside
the struct, not inside it.

The shape

// Utils/RestartSettings.h

namespace Util::Settings
{
    // Erased view — what gets handed to MCP, banners, anyone outside the
    // owning feature. Binary-compatible with the typed RestartField so a
    // `std::span<const RestartFieldInfo>` is the public contract.
    struct RestartFieldInfo
    {
        const char* jsonKey;   // matches the NLOHMANN field name — e.g. "qualityMode"
        const char* label;     // user-facing string — e.g. "Quality Mode"
        size_t      offset;    // byte offset within the owning Settings struct
        size_t      size;      // sizeof(field type)
    };

    template <typename SettingsT>
    struct RestartField : RestartFieldInfo
    {
        template <typename T>
        constexpr RestartField(const char* jsonKey,
                               const char* userLabel,
                               T SettingsT::* member)
          : RestartFieldInfo{
                jsonKey,
                userLabel,
                reinterpret_cast<size_t>(&(reinterpret_cast<SettingsT*>(0)->*member)),
                sizeof(T)
            } {}
    };

    template <typename SettingsT, size_t N>
    using RestartTable = std::array<RestartField<SettingsT>, N>;
}

// Convenience macro — stringifies the member name so the author doesn't
// duplicate it. RESTART_FIELD(qualityMode, "Quality Mode") becomes
// { "qualityMode", "Quality Mode", &Settings::qualityMode }.
#define RESTART_FIELD(member, userLabel) \
    { #member, userLabel, &std::remove_reference_t<decltype(*this)>::member }
// Note: the macro above resolves &Settings::member when used inside a
// member-aware context; a sibling-table form is `{ #member, userLabel, &Settings::member }`
// written directly.

Usage at the feature site

// Settings — unchanged from today. Pure POD, cbuffer-safe, JSON-stable.
struct Settings
{
    float sharpnessDLSS = 0.0f;
    uint  presetDLSS    = 0;
    bool  enableDLSSperf = false;
    uint  qualityMode = 1;
    // ... rest unchanged
};

// Metadata table — sits beside the struct. Compile-time constant.
inline constexpr Util::Settings::RestartTable<Settings, 2> kRestartFields{{
    { "enableDLSSperf", "DLSSperf",     &Settings::enableDLSSperf },
    { "qualityMode",    "Quality Mode", &Settings::qualityMode    },
}};

No JSON change. No cbuffer change. No widget-call-site change. The table
is the single source of truth for "this field needs a restart to apply."

Author burden per field:

  • Runtime field: zero changes — Settings struct stays as-is.
  • Restart-required field: one line in the table — name the member
    pointer + user-facing label. The hand-rolled xAtBoot slot, the
    LatchX() method, and the manual settings.x != xAtBoot comparison
    in DrawSettings all go away.

Uniform discovery — one Feature virtual

Per-feature tables stay local for compile-time safety, but the system as a
whole needs a single way to enumerate restart-gated fields across all
features (for MCP, for a future "show me everything pending restart"
summary panel, for telemetry, etc.). The minimal hook is one virtual on
the Feature base, defaulting to "no restart-gated fields":

struct Feature
{
    // ... existing interface ...

    /// Restart-gated fields this feature owns. Default: none.
    /// Override and return a span over the feature's static constexpr
    /// kRestartFields table.
    virtual std::span<const Util::Settings::RestartFieldInfo>
    GetRestartRequiredFields() const { return {}; }

    /// Boot-snapshot view — returns the value of `field` as it was when
    /// the feature latched, byte-addressable so MCP can deserialize via
    /// the field's known size/offset. nullptr if not yet latched or the
    /// field isn't in the restart table.
    virtual const void* GetBootValue(std::string_view jsonKey) const { return nullptr; }
};

Each feature with restart-required fields overrides:

struct Upscaling : Feature
{
    std::span<const Util::Settings::RestartFieldInfo>
    GetRestartRequiredFields() const override
    {
        return { kRestartFields.data(), kRestartFields.size() };
    }

    const void* GetBootValue(std::string_view jsonKey) const override
    {
        return bootSnapshot.RawBoot(jsonKey);  // helper added to BootSnapshot
    }
};

Features without restart-gated fields don't touch this — they inherit the
empty defaults and contribute nothing to the discovery surface.

MCP / Remote Control integration

The MCP server (RemoteControl feature) exposes feature settings
programmatically. With the discovery virtual in place, it can answer two
new classes of question without per-feature special-casing:

"What's restart-required for feature X?"

// Inside MCP handler for a "list_restart_required" tool call:
json result = json::array();
Feature::ForEachLoadedFeature([&](Feature* f) {
    for (const auto& field : f->GetRestartRequiredFields()) {
        result.push_back({
            { "feature", f->GetShortName() },
            { "key", field.jsonKey },
            { "label", field.label },
        });
    }
});

"Can I set Upscaling.qualityMode at runtime?"

// Inside MCP handler for a "set_setting" tool call:
auto* feature = Feature::FindByShortName(req.feature);
for (const auto& field : feature->GetRestartRequiredFields()) {
    if (field.jsonKey == req.key) {
        return Reject("'" + req.key + "' on " + req.feature +
                      " requires a restart to apply. Use 'stage_setting' "
                      "if you want to save the new value for next launch.");
    }
}
// Field not in restart table → safe to set live.

"What's currently staged?"

// Inside MCP handler for a "list_pending_restart" tool call:
Feature::ForEachLoadedFeature([&](Feature* f) {
    for (const auto& field : f->GetRestartRequiredFields()) {
        const void* boot = f->GetBootValue(field.jsonKey);
        if (!boot) continue;
        const void* live = reinterpret_cast<const char*>(f->GetSettingsBlob()) + field.offset;
        if (std::memcmp(boot, live, field.size) != 0) {
            // pending — emit to result
        }
    }
});

The MCP side doesn't need any per-feature glue — it operates entirely
through the Feature interface + RestartFieldInfo table entries.

Boot snapshot tracker

BootSnapshot<Settings> carries a copy of Settings as it stood at the
moment Latch() was called, plus a pointer to the metadata table. Diffing
is a memcmp of the relevant field slices using the table's offset/size.

namespace Util::Settings
{
    template <typename SettingsT>
    class BootSnapshot
    {
    public:
        // Templated on N so the snapshot binds to a specific table size.
        template <size_t N>
        explicit BootSnapshot(const RestartTable<SettingsT, N>& table);

        // Call once after the feature's settings have stabilized — typically
        // at the end of PostPostLoad.
        void Latch(const SettingsT& live);

        // Per-field accessor: did this specific field change since boot?
        // Identified by member pointer, looked up in the bound table.
        template <typename T>
        bool HasPendingChange(const SettingsT& live, T SettingsT::*field) const;

        // Read-only access to the boot value for downstream code that needs
        // to route through it (e.g., Streamline DLSS options, RT allocation
        // — the existing `dlssPerf.HasBootSnapshot() ? boot : live` ternaries
        // collapse to `snap.Boot(&Settings::field)`).
        template <typename T>
        const T& Boot(T SettingsT::*field) const;

        bool IsLatched() const;

    private:
        SettingsT bootCopy_{};
        const RestartFieldSlice* table_;  // type-erased view into the table
        size_t tableSize_;
        bool latched_ = false;
    };
}

Each feature owns one:

struct Upscaling : Feature
{
    Settings settings;
    Util::Settings::BootSnapshot<Settings> bootSnapshot{ kRestartFields };

    void PostPostLoad() override
    {
        // existing init...
        bootSnapshot.Latch(settings);
    }
};

No reflection needed. Since the table is a constexpr std::array of
member-pointer-derived offsets, Latch is a single memcpy of Settings,
and HasPendingChange is a memcmp of the field slice. Member pointers
take the place of any macro or PFR reflection — the table itself is the
authoritative list.

UI integration — one call, automatic banner sweep

The author makes one call at the top (or bottom) of DrawSettings and
the snapshot auto-renders a Util::Text::RestartNeeded banner for every
pending-diff field in its table:

void Upscaling::DrawSettings()
{
    bootSnapshot.DrawPendingBanners(settings);   // ← only required call

    // ... all existing ImGui widget code, unchanged ...
    ImGui::SliderInt("Quality Mode", reinterpret_cast<int*>(&settings.qualityMode), 1, 4);
    ImGui::Checkbox("DLSSperf", &settings.enableDLSSperf);
    // etc.
}

DrawPendingBanners walks the bound restart table once. For each field
whose live value differs from the boot snapshot, it emits one
RestartNeeded-styled line using the field's label from the table:

Pending restart: Quality Mode changed (active = Quality, selected = Ultra Performance)
Pending restart: DLSSperf changed (active = off, selected = on)

Widgets themselves are unchanged — no per-call helper, no need to remember
to add a Diff call next to every restart-gated slider, no copy-pasting
format strings. The slider is never disabled; users always stage a
change. Same UX shape as today's DLSSperf banners, but consolidated into
one summary block instead of inline scatter.

Variant: inline-near-widget rendering. Some surfaces will want the
banner to render right next to the slider (today's pattern). Optional
helper for that case:

ImGui::SliderInt("Quality Mode", reinterpret_cast<int*>(&settings.qualityMode), 1, 4);
Util::UI::DrawSettingDiff(bootSnapshot, settings, &Settings::qualityMode);

DrawSettingDiff emits the same banner as DrawPendingBanners but only
for the single field passed in. Mix and match: most features want the
summary block; surfaces where the inline placement is load-bearing (e.g.,
a slider deep inside a sub-tree) can use the per-field call. Either way
the field metadata + diff logic is shared.

Future opt-in: wrapped widget API. If a future PR wants to collapse
the inline case to one call site:

Util::UI::Slider(bootSnapshot, settings, &Settings::qualityMode,
                 1, 4, qualityModes[settings.qualityMode]);

A templated thin wrapper around ImGui::SliderInt that does the slider
draw and the inline diff in one call, deriving the user label from the
restart-table entry. Out of scope for v1 — the summary-block path is
sufficient.

Migration path

Drop-in. Runtime fields and Settings structs require zero changes —
no wrapper, no POD-layout impact, no JSON impact, no cbuffer impact.

For a feature that has restart-required fields, the author:

  1. Declare one inline constexpr Util::Settings::RestartTable<Settings, N> kRestartFields{{ ... }}; beside the Settings struct, naming each
    restart-gated field as { "jsonKey", "User Label", &Settings::field }.
  2. Add a BootSnapshot<Settings> bootSnapshot{ kRestartFields }; member
    to the feature class.
  3. Call bootSnapshot.Latch(settings) once at the end of PostPostLoad.
  4. Override Feature::GetRestartRequiredFields() to return a span over
    kRestartFields (and GetBootValue to delegate to the snapshot).
    This is what makes the field discoverable to MCP and other consumers.
  5. Add bootSnapshot.DrawPendingBanners(settings); once at the top of
    DrawSettings. All hand-rolled per-field pending-diff blocks delete.

Settings structs that have no restart-required fields are entirely
untouched, inherit the empty defaults from Feature, and the BootSnapshot
infrastructure is invisible to them.

Refactor sites identified in the current codebase

Audited via grep -rni 'restart required\|pending restart\|AtBoot\|HasBootSnapshot'
across src/. Each entry has the boot-snapshot pattern, the
restart-required UI cue, or both.

Canary sites (already use Util::Text::RestartNeeded):

File Lines Fields converted
Features/Upscaling.cpp 239, 292, 345 DLSSperf: enableDLSSperf, qualityMode, upscaleMethod
Features/Upscaling/DlssEnhancer.cpp 190 DlssEnhancer: enabled

These will likely flip first — they're the cleanest fits.

Boot-snapshot machinery to retire:

File Pattern
Features/Upscaling/DLSSperf.{cpp,h} HasBootSnapshot(), bootUpscaleMethod, bootQualityMode, install-time latch
Features/Upscaling/DlssEnhancer.{cpp,h} enabledAtBoot, qualityModeAtBoot, LatchEnabled(), LatchQualityMode(), IsLoaded() aliasing enabledAtBoot
Features/Upscaling/DlssEnhancer/Bridge.{cpp,h} BootSequence(), GetQualityModeAtBoot() (consumers of the latch above)
Features/DynamicCubemaps.{cpp,h} enabledAtBoot (VR-only gate at line 33; also set at line 183)

The downstream consumers that route through these (e.g.,
Upscaling::GetUpscaleMethod() returning the boot snapshot, Streamline's
SetDLSSOptions ternary at Features/Upscaling/Streamline.cpp:426) become
bootSnapshot.Boot(&Settings::field) calls.

Hand-rolled restart-required UI cues not yet using the helper:

File Lines Surface
Features/RenderDoc.cpp 158, 163 RenderDoc capture enable/disable (uses raw TextColored(StatusPalette.RestartNeeded, ...) — should switch to Util::Text::RestartNeeded regardless)
Features/Upscaling.cpp 309 Method change tooltip ("Changing this setting requires a restart")
Features/Upscaling.cpp 338 DLSSperf tooltip ("Restart required to enable/disable")
Features/Upscaling.cpp 359 Frame Generation hint ("Toggling this setting requires a restart")
Features/Upscaling.cpp 382, 385 Frame Generation Util::Text::Warning("Warning: Requires restart")
Features/Upscaling.cpp 508 DX12 swap-chain section ("Changing this requires a restart")
Features/UnifiedWater.cpp 337 Water effect ("requires a change of location or game restart") — soft restart, may not need full machinery
Features/VR/SettingsUI.cpp 366, 368 VR debug overrides ("auto-enables Reprojection — restart required")
Menu/FeatureListRenderer.cpp 667 Generic feature-disable warning ("Restart required for changes to take effect")

Some of these are static tooltips/hints — not pending-diff banners — and
might stay as plain text. The PR can decide per-site whether to add the
field to a restart table (true pending-diff semantics) or leave as a
static informational hint.

Estimated net delta: +150 LOC infrastructure, -200 LOC across the
seven convertible sites
(DLSSperf, DlssEnhancer, DynamicCubemaps,
Upscaling's three banners + Frame Generation warnings). Static-only sites
(RenderDoc tooltip, VR debug overrides, FeatureListRenderer summary)
contribute -20 LOC if also converted to Util::Text::RestartNeeded for
phrasing consistency.

Open questions

  1. Granularity of "change" — exact memcmp vs configurable predicate?
    Float fields might want an epsilon. Exact equality is fine for v1
    (matches today's behavior); epsilon is a follow-up if a feature asks.
  2. Should staging the change block JSON save? No — settings still
    round-trip; the staged value lives in the JSON until restart latches it.
    This matches the current DLSSperf behavior.
  3. Conditional restart-required — some fields are only restart-required
    under certain other settings (e.g., qualityMode is runtime unless
    enableDLSSperf is on). Out of scope for v1; the worst-case lifecycle
    (always show the banner when changed from boot value) is the safe
    default. A future extension could let the feature pass a predicate to
    DrawSettingDiff that suppresses the banner when the field is currently
    runtime-effective.
  4. Per-type label formattingToDisplayString(T) needs overloads or
    a customization hook for enum-like uints (e.g., qualityMode = 2
    "Balanced" rather than "2"). Easiest path: add an optional
    std::function<std::string(const void*)> to RestartField that the
    feature can supply when the bare numeric isn't useful. Out of scope for
    v1 — the bare value is no worse than today's banners.

Scope for the new PR

  • New header: src/Utils/SettingValue.h (typed wrapper + lifecycle aliases).
  • New header: src/Utils/BootSnapshot.h (snapshot container + diff API).
  • New helper in src/Utils/UI.{h,cpp}: DrawSetting<Field> and
    DrawSettingDiff<Field>.
  • Convert DLSSperf, DlssEnhancer, Frame Generation as the canary sites
    (proves the API and removes the duplicated boilerplate in one PR).
  • Documentation: a short section in CLAUDE.md under "Configuration
    System" describing when to pick which lifecycle.

Not in scope:

  • HDRDisplay, Streamline log level, other latent sites — follow-up PRs as
    each surface is touched.
  • Per-field localization of labels — features still pass their own user
    strings.
    </issue_description>

Comments on the Issue (you are @codex[agent] in this section)

Copy link
Copy Markdown
Owner

@alandtse alandtse left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codex Thanks for the infrastructure work — BootSnapshot + RestartFieldInfo + MCP introspection + Upscaling canary look solid and CodeQL is green. A few items from the issue are still outstanding before this can come out of WIP. Please address:

1. PR title — conventional commit format

[WIP] Add unified restart-required settings infrastructure doesn't parse as a conventional commit. The repo squash-merges and semantic-release reads the PR title, so the title becomes the commit message. Please retitle to match the issue:

feat(settings): unified restart-required infrastructure + MCP introspection

(Drop the [WIP] prefix when ready to merge.)

2. Missed refactor sites from the issue's enumerated list

The issue's site list was authored against a branch that had DlssEnhancer/Bridge — those don't exist on dev, so ignore those entries. But these are still present on dev and untouched:

  • Features/DynamicCubemaps — explicitly enumerated in the issue. src/Features/DynamicCubemaps.cpp:33 and :183, src/Features/DynamicCubemaps.h:122 still use the ad-hoc enabledAtBoot pattern. Please convert to a kRestartFields table + BootSnapshot<Settings> (or document why this site stays manual — VR-only gating may complicate it).
  • Static "Requires restart" hints not converted to pending-diff banners at src/Features/Upscaling.cpp Backend Diagnostics (streamlineLogLevel, the issue's line-508 site) and the Method combo's restart tooltip (issue's line 309). Add these fields to kRestartFields or document why they stay static.
  • Raw TextColored(StatusPalette.RestartNeeded, ...) callsites the issue flagged for migration to Util::Text::RestartNeeded regardless: src/Features/RenderDoc.cpp:158, src/Features/VRStereoOptimizations.cpp:264, plus src/Features/WeatherEditor.cpp:606 and src/WeatherEditor/EditorWindow.cpp:362 (same pattern, not in the issue list).
  • The remaining sites (UnifiedWater.cpp, VR/SettingsUI.cpp, Menu/FeatureListRenderer.cpp) were flagged in the issue as "may stay static." Defensible to defer, but please call them out as deferred follow-ups in the PR body so future readers know they were considered.

3. Latent sibling sites with the same pattern

These weren't in the issue but match the enabledAtBoot shape and are worth converting (or at least acknowledging) in the same PR:

  • src/Features/VolumetricLighting.h:25
  • src/Features/Upscaling/Streamline.h:31

4. Ordering invariant in kRestartFields is silently load-bearing

src/Features/Upscaling.h:130-137GetRestartRequiredFields() returns a prefix kRestartFields.data() ... +baseCount+extra. The first 3 entries must be the always-restart fields; the last 2 must be the DLSSperf-conditional ones. Reordering the array silently breaks MCP's list_restart_required and the pending-diff banners. Please either:

  • split into two tables (kAlwaysRestartFields + kDlssPerfRestartFields) and concat the spans, or
  • filter by predicate at runtime, or
  • at minimum add a // ORDER MATTERS: comment pinning the layout.

5. PR body scope note

list_pending_restart walks every loaded feature, but since only Upscaling overrides GetRestartRequiredFields() so far, MCP only returns Upscaling fields. Please add a line to the PR body noting that DynamicCubemaps / VolumetricLighting / Streamline conversions are follow-ups (or land them here per item 2/3), so users don't expect global coverage from day one.


Once these are addressed and the title is fixed, this should be ready for review. Ping me when you've pushed updates.

@Codex
Copy link
Copy Markdown
Author

Codex AI commented May 24, 2026

@codex Thanks for the infrastructure work — BootSnapshot + RestartFieldInfo + MCP introspection + Upscaling canary look solid and CodeQL is green. A few items from the issue are still outstandin...

Addressed the outstanding refactor sites + ordering concerns in commit e8234e0:

  • Converted DynamicCubemaps VR SSR gate from enabledAtBoot to BootSnapshot + kRestartFields and exposed restart fields/boot values for MCP.
  • Converted VolumetricLighting VR enable gating to BootSnapshot + kRestartFields (VR-only restart-required) and removed unused enabledAtBoot.
  • Removed unused Streamline::enabledAtBoot (it wasn’t referenced anywhere).
  • Fixed Upscaling restart-field ordering by splitting always-vs-DLSSperf tables (no prefix-order invariant).
  • Added pending-diff banners for streamlineLogLevel and presetDLSS.
  • Migrated the flagged TextColored(...RestartNeeded...) sites to Util::Text::RestartNeeded.

PR title: I can’t retitle the PR from this environment; please update it to feat(settings): unified restart-required infrastructure + MCP introspection when ready to merge.

@Codex Codex AI requested a review from alandtse May 24, 2026 23:54
@alandtse alandtse changed the title [WIP] Add unified restart-required settings infrastructure feat(settings): unified restart-required infrastructure May 24, 2026
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 25, 2026

No actionable suggestions for changed features.

@alandtse alandtse changed the title feat(settings): unified restart-required infrastructure refactor: unified restart-required infrastructure + MCP introspection May 25, 2026
@alandtse alandtse marked this pull request as ready for review May 25, 2026 01:47
Copilot AI review requested due to automatic review settings May 25, 2026 01:47
@alandtse alandtse changed the title refactor: unified restart-required infrastructure + MCP introspection refactor: unify restart-required infrastructure May 25, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a unified, reusable “restart-required settings” infrastructure (metadata table + boot snapshot diffing) and wires it into UI helpers and MCP/RemoteControl introspection, then migrates several features (notably Upscaling + VR-only toggles) onto the new mechanism.

Changes:

  • Add Util::Settings::RestartFieldInfo/RestartTable and Util::Settings::BootSnapshot to latch “boot” settings and diff against current selections.
  • Add UI helpers for consistent “Pending restart” banners and update multiple UIs to use Util::Text::RestartNeeded.
  • Extend RemoteControl MCP “feature” tool with list_restart_required and list_pending_restart, and add unit tests for BootSnapshot.

Reviewed changes

Copilot reviewed 26 out of 26 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/cpp/test_bootsnapshot.cpp Adds Catch2 unit tests covering unlatch/latch behavior, diffs, and field lookup for BootSnapshot.
tests/cpp/CMakeLists.txt Adds the new BootSnapshot test to the cpp_tests target.
src/WeatherEditor/EditorWindow.cpp Formatting cleanup and switches an “Active:” label to Util::Text::RestartNeeded.
src/Utils/UI.h Adds restart-required UI helpers (DrawSettingDiff, DrawPendingBanners) and includes BootSnapshot.
src/Utils/RestartSettings.h Introduces restart-field metadata (RestartFieldInfo, RestartTable) + helper macro and lookup.
src/Utils/BootSnapshot.h Adds boot-latching + memcmp-based diffing + member metadata lookup for restart-gated settings.
src/Hooks.cpp Latches Upscaling boot snapshot early during render-target creation initialization.
src/Features/WeatherEditor.cpp Replaces custom colored text with Util::Text::RestartNeeded for wind relation display.
src/Features/VRStereoOptimizations.cpp Replaces manual RestartNeeded coloring with Util::Text::RestartNeeded.
src/Features/VolumetricLighting.h Adds restart-field table + boot snapshot + Feature introspection overrides (VR-only gating).
src/Features/VolumetricLighting.cpp Draws per-setting restart diffs in VR and latches snapshot in PostPostLoad.
src/Features/Upscaling/Streamline.h Removes an unused boot-latch flag from Streamline state.
src/Features/Upscaling/Streamline.cpp Switches DLSS qualityMode boot selection to use Upscaling’s bootSnapshot.
src/Features/Upscaling/DLSSperf.h Removes DLSSperf’s bespoke boot snapshot storage/accessors.
src/Features/Upscaling/DLSSperf.cpp Ensures Upscaling boot snapshot is latched robustly even if call order changes.
src/Features/Upscaling.h Adds restart-field table + boot snapshot + Feature introspection overrides for Upscaling settings.
src/Features/Upscaling.cpp Replaces bespoke restart-required UI logic with BootSnapshot/UI helpers; adds qualityMode range clamp on load.
src/Features/RenderDoc.cpp Uses Util::Text::RestartNeeded for the “requires restart” capture hint.
src/Features/RemoteControl.cpp Adds restart-required/pending-restart MCP actions and byte/hex reporting.
src/Features/DynamicCubemaps.h Adds restart-field table + boot snapshot + Feature introspection overrides (VR-only gating).
src/Features/DynamicCubemaps.cpp Uses boot snapshot diffing for VR SSR “pending restart” messaging; latches in PostPostLoad.
src/Feature.h Adds virtual introspection surface for restart-required fields + boot/live blob accessors.
features/Weather Editor/Shaders/Features/WeatherEditor.ini Bumps WeatherEditor feature version.
features/Volumetric Lighting/Shaders/Features/VolumetricLighting.ini Bumps VolumetricLighting feature version.
features/Dynamic Cubemaps/Shaders/Features/DynamicCubemaps.ini Bumps DynamicCubemaps feature version.
.claude/CLAUDE.md Documents the new restart-gated settings mechanism and points to Upscaling as a canary.
Comments suppressed due to low confidence (2)

src/Features/Upscaling.cpp:306

  • DLSS Model Preset is shown with DrawSettingDiff + a tooltip saying it "requires a restart", but the backend applies presetDLSS at runtime in Streamline::SetDLSSOptions (called every frame). This banner/text will incorrectly tell users (and MCP) that the change won't apply until restart. Update the UI copy and/or remove the restart-diff rendering for this field unless you intentionally make the preset boot-latched.
			const char* presets[] = { "Default", "Preset J", "Preset K", "Preset L", "Preset M" };
			ImGui::Combo("DLSS Model Preset", (int*)&settings.presetDLSS, presets, 5);
			Util::UI::DrawSettingDiff(bootSnapshot, settings, &Settings::presetDLSS);
			if (auto _tt = Util::HoverTooltipWrapper()) {
				ImGui::Text("Choose which DLSS AI model preset to use.");
				ImGui::Text("Each model offers different visual quality, performance, and motion stability.");
				ImGui::Text("Set to 'Default' for automatic selection based on your Upscale Preset and hardware.");
				ImGui::Text("Changing this setting requires a restart to take effect.");
			}

src/Features/RemoteControl.cpp:587

  • The MCP tool schema describes shortName as "Required for all actions except 'list'", but this PR makes it optional for list_restart_required and list_pending_restart. Update the parameter description to match the actual behavior so clients don't treat shortName as mandatory for these actions.
	                      .with_string_param("action",
							  "One of: 'list', 'list_restart_required', "
							  "'list_pending_restart', 'get', 'set', 'reset', 'toggle'.")
	                      .with_string_param("shortName",
							  "Required for all actions except 'list'. From the "
							  "list response.",
							  /*required=*/false)

Comment thread tests/cpp/test_bootsnapshot.cpp Outdated
Comment thread src/Utils/BootSnapshot.h Outdated
Comment thread src/Features/Upscaling.h Outdated
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 25, 2026

✅ A pre-release build is available for this PR:
Download

Codex AI and others added 9 commits May 24, 2026 21:03
…tion

Co-authored-by: alandtse <7086117+alandtse@users.noreply.github.com>
Co-authored-by: alandtse <7086117+alandtse@users.noreply.github.com>
Co-authored-by: alandtse <7086117+alandtse@users.noreply.github.com>
The three-table split (kBootSnapshotFields + kAlwaysRestartFields +
kDlssPerfRestartFields) violated the issue's "single point of truth
per field" goal and reintroduced the v2-deferred conditional-discovery
mechanism. The per-widget banner gating in DrawSettings already
conditions on dlssPerf.IsHookActive() at the call site, so the
discovery table doesn't need its own gating — MCP can report all
restart-gated fields and clients can check feature state.

Drops the mutable runtime concat buffer along with it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
No behavior change. Pre-commit hook surfaced legacy formatting drift
when touching this file in the next commit; landing the format pass
separately so the logic diff stays minimal.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The earlier TextColored→Util::Text::RestartNeeded conversion left
WeatherEditor::DisplayWindInfo with an unused `theme` local (treated
as error under /W4) and removed EditorWindow::ShowObjectsWindow's
`theme` declaration while leaving a later `theme.Palette.Text`
reference dangling.

Drop the unused WeatherEditor local; restore the EditorWindow local
so the surviving caller compiles.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Feature Version Audit flagged these as needing bumps because the
restart-required infrastructure work touched their feature classes.

- Dynamic Cubemaps: 2-3-1 → 2-4-0 (BootSnapshot integration)
- Volumetric Lighting: 1-1-0 → 1-2-0 (BootSnapshot integration)
- Weather Editor: 2-0-1 → 2-1-0 (RestartNeeded helper migration)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Latch via memcpy so padding bytes copy verbatim. Assignment copies the
  object representation for trivially-copyable types per the standard,
  but memcpy removes any compiler latitude and matches the field-slice
  memcmp comparisons in HasPendingChange.
- Test name typo: "starts unlatching" -> "starts unlatched".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
alandtse and others added 5 commits May 24, 2026 21:04
Move loose enableRenderDocCapture/captureFrameCount fields into a
proper Settings struct so the boot-snapshot pattern can hook in.
Replace the static "Requires restart to enable..." line with the
DrawSettingDiff banner. The disable path keeps its performance-impact
warning since that's load-bearing context the banner doesn't carry.

JSON keys are unchanged ("Enable RenderDoc Capture", "Capture Frame
Count"), so existing user configs round-trip cleanly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
stereoMode is restart-gated (stencil/CS resources are only set up
during VR's SetupResources hook). Latch from VR::PostPostLoad and
replace the static "Restart is required..." hint with DrawSettingDiff
so the user sees the active vs selected values.

VRStereoOptimizations isn't itself a Feature subclass (it's a child
of the VR feature), so MCP discovery via GetRestartRequiredFields
doesn't pick this up. UI behavior still works through the bootSnapshot
helper; a follow-up could expose VR's restart-gated sub-fields via
the VR Feature's discovery override.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ant hints

- presetDLSS is runtime-effective: Streamline::SetDLSSOptions reads
  settings.presetDLSS per-frame and applies it via slDLSSSetOptions.
  Treating it as restart-gated was misleading the UI and MCP discovery.
  (Catch from Copilot review.)
- Remove two static "Changing this setting requires a restart" lines
  next to the DLSS Model Preset and Streamline Logging combos; their
  pending-diff DrawSettingDiff banners (where applicable) cover the
  same signal without the duplicate hint.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This PR is now a refactor with no behavior change for end users —
settings shapes are unchanged, JSON round-trips unchanged. Bumping
feature versions implies the per-feature settings schema changed,
which it didn't. Revert the bumps back to their dev-branch values.

- Dynamic Cubemaps: 2-4-0 -> 2-3-1
- Volumetric Lighting: 1-2-0 -> 1-1-0
- Weather Editor: 2-1-0 -> 2-0-1

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Catch2 doesn't link the std::string_view StringMaker by default, so
`REQUIRE(std::string_view(...) == "literal")` fails to link with
LNK2001. Cast to std::string instead.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@alandtse alandtse force-pushed the codex/unified-restart-required-settings branch from 913ec49 to 2ae2836 Compare May 25, 2026 04:05
Comment thread src/Features/DynamicCubemaps.cpp Outdated
Comment thread src/Features/DynamicCubemaps.cpp
Comment thread src/Features/RemoteControl.cpp Outdated
Comment thread src/Features/RemoteControl.cpp Outdated
Comment thread src/Features/VolumetricLighting.cpp Outdated
Copy link
Copy Markdown
Owner

@alandtse alandtse left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comments. The REL::Module::IsVR should be checked throughout.

Co-authored-by: alandtse <7086117+alandtse@users.noreply.github.com>
@Codex
Copy link
Copy Markdown
Author

Codex AI commented May 25, 2026

@codex Thanks for the infrastructure work — BootSnapshot + RestartFieldInfo + MCP introspection + Upscaling canary look solid and CodeQL is green. A few items from the issue are still outstandin...

Follow-up in dd972af: standardized VR-only restart gating to use globals::game::isVR (DynamicCubemaps + VolumetricLighting) and switched DynamicCubemaps’ SSR pending-restart UI to the shared Util::UI::DrawSettingDiff banner.

PR title: I still can’t retitle from this environment; please rename to feat(settings): unified restart-required infrastructure + MCP introspection before merge.

@Codex Codex AI requested a review from alandtse May 25, 2026 05:15
Per the minimal-but-semantically-rich design philosophy
(EdenLabs/agentic-renderdoc), drop the two new `list_restart_required`
and `list_pending_restart` actions and fold their information into
the existing `list` response.

Each feature entry now carries an optional `restartFields` array of
`{ key, label, pending }` — `pending=true` means the user has staged
a change that won't apply until next launch. One tool call answers
"what features exist", "which fields are restart-gated", and "what's
currently pending"; clients filter as needed.

Drops the BytesToHex helper that the old list_pending_restart used to
emit raw boot/live bytes. Clients that need typed values for diffing
can use `get` to read live settings; the staged value is right there.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
When a loaded feature has a restart-gated setting whose live value
differs from the boot-latched value, paint its name in the feature
list with the same StatusPalette.RestartNeeded green that already
signals "INI exists but feature not loaded — toggled off at boot."
Same color, same meaning from the user's POV: "this feature has
unmade changes that take effect on restart."

Adds `Feature::HasAnyPendingRestart()` for the per-frame check —
short-circuits to false for features with no restart-gated fields,
so the per-frame cost is one virtual call + an empty-span check for
the vast majority of features. Features with restart-gated fields
walk their table doing field-slice memcmp, same data path the MCP
`list` response uses for its per-field `pending` flag.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@alandtse alandtse merged commit 045e72e into dev May 25, 2026
21 of 25 checks passed
@alandtse alandtse deleted the codex/unified-restart-required-settings branch May 25, 2026 08:24
alandtse added a commit that referenced this pull request May 25, 2026
Brings in:
- 045e72e refactor: unify restart-required infrastructure (#39)
- 5c00a38 build: drop /XO from robocopy auto-deploy (#37)

The /XO drop is identical to our local cc65edc -- merge auto-resolved
cleanly. The restart-required infrastructure (BootSnapshot, RestartSettings)
will be adopted into ShadowCasterManager in a follow-up commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(settings): unified restart-required infrastructure + MCP introspection

3 participants