Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,7 @@ Feature versions are automatically extracted from `.ini` files and compiled into
- JSON-based settings with nlohmann_json
- Hot-reload capability through ImGui interface
- Versioned feature configurations for compatibility
- Restart-gated fields use `Util::Settings::BootSnapshot` + `kRestartFields` metadata to diff boot-latched vs selected values (drives `Util::Text::RestartNeeded` banners and MCP introspection; see Upscaling for a canary)

### Error Handling

Expand Down
37 changes: 37 additions & 0 deletions src/Feature.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
#include "FeatureCategories.h"
#include "FeatureConstraints.h"
#include "FeatureVersions.h"
#include "Utils/RestartSettings.h"

#include <cstring>
#include <span>
#include <string_view>
#ifdef TRACY_ENABLE
# include <Tracy/Tracy.hpp>
# include <Tracy/TracyD3D11.hpp>
Expand All @@ -21,6 +26,38 @@ struct Feature
// Override in features to expose settings for search
virtual std::vector<SettingSearchEntry> GetSettingsSearchEntries() { return {}; }

// Restart-required settings introspection. Default: none.
// Features with restart-gated fields override these to expose them to UI
// helpers and MCP/RemoteControl without per-feature glue.
virtual std::span<const Util::Settings::RestartFieldInfo> GetRestartRequiredFields() const { return {}; }
virtual const void* GetBootValue(std::string_view /*jsonKey*/) const { return nullptr; }
virtual const void* GetSettingsBlob() const { return nullptr; }
virtual size_t GetSettingsBlobSize() const { return 0; }

// True if any restart-gated setting's live value differs from the
// boot-latched value. Drives the green "RestartNeeded" tint in the
// feature list and the `pending` flag in MCP's `list` response.
bool HasAnyPendingRestart() const
{
const auto fields = GetRestartRequiredFields();
if (fields.empty())
return false;
const auto* live = reinterpret_cast<const unsigned char*>(GetSettingsBlob());
const size_t liveSize = GetSettingsBlobSize();
if (!live || liveSize == 0)
return false;
for (const auto& field : fields) {
if (!field.jsonKey || field.size == 0)
continue;
if (field.offset + field.size > liveSize)
continue;
const void* boot = GetBootValue(field.jsonKey);
if (boot && std::memcmp(boot, live + field.offset, field.size) != 0)
return true;
}
return false;
}

// Nexus Mods base URL for Skyrim Special Edition
static constexpr std::string_view NEXUS_BASE_URL = "https://www.nexusmods.com/skyrimspecialedition/mods/";
bool loaded = false;
Expand Down
24 changes: 10 additions & 14 deletions src/Features/DynamicCubemaps.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "ShaderCache.h"
#include "State.h"
#include "Utils/D3D.h"
#include "Utils/UI.h"

constexpr auto MIPLEVELS = 8;

Expand All @@ -30,14 +31,9 @@ void DynamicCubemaps::DrawSettings()
recompileFlag |= ImGui::Checkbox("Enable Screen Space Reflections", reinterpret_cast<bool*>(&settings.EnabledSSR));
if (auto _tt = Util::HoverTooltipWrapper()) {
ImGui::Text("Enable Screen Space Reflections on Water");
if (REL::Module::IsVR() && !enabledAtBoot) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 0.0f, 0.0f, 1.0f));
ImGui::Text(
"A restart is required to enable in VR. "
"Save Settings after enabling and restart the game.");
ImGui::PopStyleColor();
}
}
if (globals::game::isVR)
Util::UI::DrawSettingDiff(bootSnapshot, settings, &Settings::EnabledSSR);
ImGui::TreePop();
}

Expand Down Expand Up @@ -119,7 +115,7 @@ void DynamicCubemaps::DrawSettings()
}
ImGui::TreePop();
}
if (REL::Module::IsVR()) {
if (globals::game::isVR) {
if (ImGui::TreeNodeEx("Advanced VR Settings", ImGuiTreeNodeFlags_DefaultOpen)) {
Util::RenderImGuiSettingsTree(iniVRCubeMapSettings, "VR");
Util::RenderImGuiSettingsTree(hiddenVRCubeMapSettings, "hiddenVR");
Expand All @@ -131,7 +127,7 @@ void DynamicCubemaps::DrawSettings()
void DynamicCubemaps::LoadSettings(json& o_json)
{
settings = o_json;
if (REL::Module::IsVR()) {
if (globals::game::isVR) {
Util::LoadGameSettings(iniVRCubeMapSettings);
}
recompileFlag = true;
Expand All @@ -140,15 +136,15 @@ void DynamicCubemaps::LoadSettings(json& o_json)
void DynamicCubemaps::SaveSettings(json& o_json)
{
o_json = settings;
if (REL::Module::IsVR()) {
if (globals::game::isVR) {
Util::SaveGameSettings(iniVRCubeMapSettings);
}
}

void DynamicCubemaps::RestoreDefaultSettings()
{
settings = {};
if (REL::Module::IsVR()) {
if (globals::game::isVR) {
Util::ResetGameSettingsToDefaults(iniVRCubeMapSettings);
Util::ResetGameSettingsToDefaults(hiddenVRCubeMapSettings);
}
Expand All @@ -157,7 +153,7 @@ void DynamicCubemaps::RestoreDefaultSettings()

void DynamicCubemaps::DataLoaded()
{
if (REL::Module::IsVR()) {
if (globals::game::isVR) {
// enable cubemap settings in VR
Util::EnableBooleanSettings(iniVRCubeMapSettings, GetName());
Util::EnableBooleanSettings(hiddenVRCubeMapSettings, GetName());
Expand All @@ -167,7 +163,8 @@ void DynamicCubemaps::DataLoaded()

void DynamicCubemaps::PostPostLoad()
{
if (REL::Module::IsVR() && settings.EnabledSSR) {
bootSnapshot.LatchIfNeeded(settings);
if (globals::game::isVR && settings.EnabledSSR) {
std::map<std::string, uintptr_t> earlyhiddenVRCubeMapSettings{
{ "bScreenSpaceReflectionEnabled:Display", 0x1ED5BC0 },
};
Expand All @@ -180,7 +177,6 @@ void DynamicCubemaps::PostPostLoad()
*setting = true;
}
}
enabledAtBoot = true;
Comment thread
alandtse marked this conversation as resolved.
}
}

Expand Down
17 changes: 16 additions & 1 deletion src/Features/DynamicCubemaps.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once

#include "Buffer.h"
#include "Utils/BootSnapshot.h"

class MenuOpenCloseEventHandler : public RE::BSTEventSink<RE::MenuOpenCloseEvent>
{
Expand Down Expand Up @@ -119,7 +120,21 @@ struct DynamicCubemaps : Feature
};

Settings settings;
bool enabledAtBoot = false;

inline static constexpr Util::Settings::RestartTable<Settings, 1> kRestartFields{ {
UTIL_RESTART_FIELD(Settings, EnabledSSR, "Screen Space Reflections"),
} };
Util::Settings::BootSnapshot<Settings> bootSnapshot{ kRestartFields };

std::span<const Util::Settings::RestartFieldInfo> GetRestartRequiredFields() const override
{
// VR-only: enabling SSR needs game-setting initialization at startup.
return globals::game::isVR ? std::span<const Util::Settings::RestartFieldInfo>{ kRestartFields.data(), kRestartFields.size() } : std::span<const Util::Settings::RestartFieldInfo>{};
}
const void* GetBootValue(std::string_view jsonKey) const override { return bootSnapshot.RawBoot(jsonKey); }
const void* GetSettingsBlob() const override { return &settings; }
size_t GetSettingsBlobSize() const override { return sizeof(settings); }

void UpdateCubemap();

void PostDeferred();
Expand Down
40 changes: 38 additions & 2 deletions src/Features/RemoteControl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
#include <imgui_stdlib.h>

#include <algorithm>
#include <cstring>
#include <format>
#include <optional>
#include <stdexcept>
Expand Down Expand Up @@ -451,7 +452,7 @@ static mcp::json EngineStateBlob()
// Helper used by feature(action="list") to build one entry per feature.
static mcp::json FeatureEntry(Feature* f)
{
return mcp::json({
mcp::json entry({
{ "name", f->GetName() },
{ "shortName", f->GetShortName() },
{ "loaded", f->loaded },
Expand All @@ -461,6 +462,36 @@ static mcp::json FeatureEntry(Feature* f)
{ "supportsVR", f->SupportsVR() },
{ "inMenu", f->IsInMenu() },
});

// Inline restart-gated metadata so `list` is the single tool that answers
// "what features exist", "which fields need a restart to apply", and
// "is anything currently pending". Each entry's `pending` is true when
// the live setting differs from the boot-latched value.
const auto fields = f->GetRestartRequiredFields();
if (!fields.empty()) {
mcp::json restartFields = mcp::json::array();
const auto* liveBase = reinterpret_cast<const unsigned char*>(f->GetSettingsBlob());
const size_t liveSize = f->GetSettingsBlobSize();
for (const auto& field : fields) {
bool pending = false;
if (liveBase && field.jsonKey && field.size != 0 &&
field.offset + field.size <= liveSize) {
const void* boot = f->GetBootValue(field.jsonKey);
if (boot &&
std::memcmp(boot, liveBase + field.offset, field.size) != 0) {
pending = true;
}
}
restartFields.push_back(mcp::json({
{ "key", field.jsonKey ? field.jsonKey : "" },
{ "label", field.label ? field.label : "" },
{ "pending", pending },
}));
}
entry["restartFields"] = restartFields;
}

return entry;
}

void RemoteControl::RegisterInspectTool()
Expand Down Expand Up @@ -517,7 +548,11 @@ void RemoteControl::RegisterFeatureTool()
"Actions:\n"
" list — no other params. Returns a JSON array; "
"each entry has { name, shortName, loaded, version, "
"category, isCore, supportsVR, inMenu }.\n"
"category, isCore, supportsVR, inMenu }. Features "
"with restart-gated settings also include "
"`restartFields: [{ key, label, pending }]` — "
"`pending=true` means the user has staged a change "
"that won't take effect until the next launch.\n"
" get — params: shortName. Returns the "
"Feature::SaveSettings(json) blob. May return null "
"if the feature has no SaveSettings/LoadSettings "
Expand Down Expand Up @@ -588,6 +623,7 @@ void RemoteControl::RegisterFeatureTool()
}

const std::string shortName = params.value("shortName", std::string{});

if (shortName.empty()) {
return ErrorResult("missing required parameter 'shortName'",
{ { "action", action } });
Expand Down
63 changes: 33 additions & 30 deletions src/Features/RenderDoc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,13 @@ RenderDoc* RenderDoc::GetSingleton()

void RenderDoc::Load()
{
// Latch the boot-time value of restart-gated fields so the menu can
// surface pending diffs even though the renderdoc.dll injection itself
// only runs once per launch.
bootSnapshot.LatchIfNeeded(settings);

// Only load RenderDoc if the user has enabled capture
if (!enableRenderDocCapture) {
if (!settings.enableCapture) {
logger::debug("[RenderDoc] RenderDoc capture disabled, skipping initialization");
return;
}
Expand Down Expand Up @@ -132,35 +137,35 @@ void RenderDoc::DrawSettings()
bool isSectionVisible = false;

// Include enable toggle and annotation forcing logic here
bool prevRenderDocCapture = enableRenderDocCapture;
if (ImGui::Checkbox("Enable RenderDoc Capture", &enableRenderDocCapture)) {
if (enableRenderDocCapture && !prevRenderDocCapture) {
bool prevRenderDocCapture = settings.enableCapture;
if (ImGui::Checkbox("Enable RenderDoc Capture", &settings.enableCapture)) {
if (settings.enableCapture && !prevRenderDocCapture) {
globals::state->useFrameAnnotations = globals::state->frameAnnotations;
globals::state->frameAnnotations = true;
}
if (!enableRenderDocCapture && prevRenderDocCapture) {
if (!settings.enableCapture && prevRenderDocCapture) {
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.");
}

// The rest of the UI renders only when capture is active
bool renderDocCaptureEnabled = enableRenderDocCapture;
bool renderDocCaptureEnabled = settings.enableCapture;
bool renderDocActive = IsAvailable();

const auto& themeSettings = Menu::GetSingleton()->GetTheme();

if (renderDocCaptureEnabled && !renderDocActive) {
ImGui::TextColored(themeSettings.StatusPalette.RestartNeeded, "Requires restart to enable RenderDoc capture.");
return;
}

if (!renderDocCaptureEnabled && renderDocActive) {
ImGui::TextColored(themeSettings.StatusPalette.Warning, "Requires restart to disable RenderDoc capture, performance will be severely impacted.");
ImGui::TextColored(themeSettings.StatusPalette.Warning, "Performance will be severely impacted until the game is restarted.");
return;
}

Expand Down Expand Up @@ -539,36 +544,34 @@ void RenderDoc::SetupResources()

void RenderDoc::SaveSettings(json& o_json)
{
o_json["Enable RenderDoc Capture"] = enableRenderDocCapture;
o_json["Enable RenderDoc Capture"] = settings.enableCapture;
o_json["Capture Frame Count"] = GetCaptureFrameCount();
}

void RenderDoc::LoadSettings(json& o_json)
{
if (o_json.contains("Enable RenderDoc Capture") && o_json["Enable RenderDoc Capture"].is_boolean()) {
enableRenderDocCapture = o_json["Enable RenderDoc Capture"];
}
if (!o_json.contains("Capture Frame Count")) {
return;
}

const auto& frameCountJson = o_json["Capture Frame Count"];
if (frameCountJson.is_number_unsigned()) {
const auto frameCount = std::min(frameCountJson.get<uint64_t>(), static_cast<uint64_t>(kMaxCaptureFrameCount));
SetCaptureFrameCount(static_cast<uint32_t>(frameCount));
} else if (frameCountJson.is_number_integer()) {
const auto frameCount = std::clamp(
frameCountJson.get<int64_t>(),
static_cast<int64_t>(kMinCaptureFrameCount),
static_cast<int64_t>(kMaxCaptureFrameCount));
SetCaptureFrameCount(static_cast<uint32_t>(frameCount));
settings.enableCapture = o_json["Enable RenderDoc Capture"];
}
if (o_json.contains("Capture Frame Count")) {
const auto& frameCountJson = o_json["Capture Frame Count"];
if (frameCountJson.is_number_unsigned()) {
const auto frameCount = std::min(frameCountJson.get<uint64_t>(), static_cast<uint64_t>(kMaxCaptureFrameCount));
SetCaptureFrameCount(static_cast<uint32_t>(frameCount));
} else if (frameCountJson.is_number_integer()) {
const auto frameCount = std::clamp(
frameCountJson.get<int64_t>(),
static_cast<int64_t>(kMinCaptureFrameCount),
static_cast<int64_t>(kMaxCaptureFrameCount));
SetCaptureFrameCount(static_cast<uint32_t>(frameCount));
}
}
bootSnapshot.LatchIfNeeded(settings);
}

void RenderDoc::RestoreDefaultSettings()
{
enableRenderDocCapture = false;
SetCaptureFrameCount(1);
settings = {};
}

void RenderDoc::ClearShaderCache()
Expand Down Expand Up @@ -726,12 +729,12 @@ bool RenderDoc::HandleCaptureHotkey(uint32_t a_vkKey)

uint32_t RenderDoc::GetCaptureFrameCount() const
{
return std::clamp(captureFrameCount, kMinCaptureFrameCount, kMaxCaptureFrameCount);
return std::clamp(settings.captureFrameCount, kMinCaptureFrameCount, kMaxCaptureFrameCount);
}

void RenderDoc::SetCaptureFrameCount(uint32_t a_frameCount)
{
captureFrameCount = std::clamp(a_frameCount, kMinCaptureFrameCount, kMaxCaptureFrameCount);
settings.captureFrameCount = std::clamp(a_frameCount, kMinCaptureFrameCount, kMaxCaptureFrameCount);
}

uint64_t RenderDoc::GetRequiredCaptureSpaceBytes() const
Expand Down Expand Up @@ -780,7 +783,7 @@ bool RenderDoc::IsCapturing() const
return false;

// RenderDoc API doesn't have a direct IsCapturing method, but we can check if captures are enabled
return enableRenderDocCapture && renderDocApi != nullptr;
return settings.enableCapture && renderDocApi != nullptr;
}

std::string RenderDoc::GetCapturePath(uint32_t a_index)
Expand Down
Loading
Loading