diff --git a/src/Features/DynamicCubemaps.cpp b/src/Features/DynamicCubemaps.cpp index 4dac373ce6..c2163804da 100644 --- a/src/Features/DynamicCubemaps.cpp +++ b/src/Features/DynamicCubemaps.cpp @@ -25,114 +25,111 @@ std::vector> DynamicCubemaps::GetS void DynamicCubemaps::DrawSettings() { - if (ImGui::TreeNodeEx("Settings", ImGuiTreeNodeFlags_DefaultOpen)) { - if (ImGui::TreeNodeEx("Screen Space Reflections", ImGuiTreeNodeFlags_DefaultOpen)) { - recompileFlag |= ImGui::Checkbox("Enable Screen Space Reflections", reinterpret_cast(&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 (ImGui::TreeNodeEx("Screen Space Reflections", ImGuiTreeNodeFlags_DefaultOpen)) { + recompileFlag |= ImGui::Checkbox("Enable Screen Space Reflections", reinterpret_cast(&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(); } - ImGui::TreePop(); } + } - if (ImGui::TreeNodeEx("Dynamic Cubemap Creator", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Text("You must enable creator mode by adding the shader define CREATOR"); - ImGui::Checkbox("Enable Creator", reinterpret_cast(&settings.EnabledCreator)); - if (settings.EnabledCreator) { - ImGui::ColorEdit3("Color", reinterpret_cast(&settings.CubemapColor)); - ImGui::SliderFloat("Roughness", &settings.CubemapColor.w, 0.0f, 1.0f, "%.2f"); - if (ImGui::Button("Export")) { - auto device = globals::d3d::device; - auto context = globals::d3d::context; - - D3D11_TEXTURE2D_DESC texDesc{}; - texDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; - texDesc.Height = 1; - texDesc.Width = 1; - texDesc.ArraySize = 6; - texDesc.MipLevels = 1; - texDesc.SampleDesc.Count = 1; - texDesc.Usage = D3D11_USAGE_DEFAULT; - texDesc.BindFlags = 0; - texDesc.MiscFlags = D3D11_RESOURCE_MISC_TEXTURECUBE; - - D3D11_SUBRESOURCE_DATA subresourceData[6]; - - struct PixelData - { - uint8_t r, g, b, a; - }; - - static PixelData colorPixel{}; - - colorPixel = { (uint8_t)((settings.CubemapColor.x * 255.0f) + 0.5f), - (uint8_t)((settings.CubemapColor.y * 255.0f) + 0.5f), - (uint8_t)((settings.CubemapColor.z * 255.0f) + 0.5f), - std::min((uint8_t)254u, (uint8_t)((settings.CubemapColor.w * 255.0f) + 0.5f)) }; - - static PixelData emptyPixel{}; - - subresourceData[0].pSysMem = &colorPixel; - subresourceData[0].SysMemPitch = sizeof(PixelData); - subresourceData[0].SysMemSlicePitch = sizeof(PixelData); - - for (uint i = 1; i < 6; i++) { - subresourceData[i].pSysMem = &emptyPixel; - subresourceData[i].SysMemPitch = sizeof(PixelData); - subresourceData[i].SysMemSlicePitch = sizeof(PixelData); - } - - ID3D11Texture2D* tempTexture; - DirectX::ScratchImage image; + if (ImGui::TreeNodeEx("Dynamic Cubemap Creator", ImGuiTreeNodeFlags_DefaultOpen)) { + ImGui::Text("You must enable creator mode by adding the shader define CREATOR"); + ImGui::Checkbox("Enable Creator", reinterpret_cast(&settings.EnabledCreator)); + if (settings.EnabledCreator) { + ImGui::ColorEdit3("Color", reinterpret_cast(&settings.CubemapColor)); + ImGui::SliderFloat("Roughness", &settings.CubemapColor.w, 0.0f, 1.0f, "%.2f"); + if (ImGui::Button("Export")) { + auto device = globals::d3d::device; + auto context = globals::d3d::context; + + D3D11_TEXTURE2D_DESC texDesc{}; + texDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; + texDesc.Height = 1; + texDesc.Width = 1; + texDesc.ArraySize = 6; + texDesc.MipLevels = 1; + texDesc.SampleDesc.Count = 1; + texDesc.Usage = D3D11_USAGE_DEFAULT; + texDesc.BindFlags = 0; + texDesc.MiscFlags = D3D11_RESOURCE_MISC_TEXTURECUBE; + + D3D11_SUBRESOURCE_DATA subresourceData[6]; + + struct PixelData + { + uint8_t r, g, b, a; + }; + + static PixelData colorPixel{}; + + colorPixel = { (uint8_t)((settings.CubemapColor.x * 255.0f) + 0.5f), + (uint8_t)((settings.CubemapColor.y * 255.0f) + 0.5f), + (uint8_t)((settings.CubemapColor.z * 255.0f) + 0.5f), + std::min((uint8_t)254u, (uint8_t)((settings.CubemapColor.w * 255.0f) + 0.5f)) }; + + static PixelData emptyPixel{}; + + subresourceData[0].pSysMem = &colorPixel; + subresourceData[0].SysMemPitch = sizeof(PixelData); + subresourceData[0].SysMemSlicePitch = sizeof(PixelData); + + for (uint i = 1; i < 6; i++) { + subresourceData[i].pSysMem = &emptyPixel; + subresourceData[i].SysMemPitch = sizeof(PixelData); + subresourceData[i].SysMemSlicePitch = sizeof(PixelData); + } - try { - DX::ThrowIfFailed(device->CreateTexture2D(&texDesc, subresourceData, &tempTexture)); - DX::ThrowIfFailed(CaptureTexture(device, context, tempTexture, image)); + ID3D11Texture2D* tempTexture; + DirectX::ScratchImage image; - if (std::filesystem::create_directories(defaultDynamicCubeMapSavePath)) { - logger::info("Missing DynamicCubeMap Creator directory created: {}", defaultDynamicCubeMapSavePath); - } + try { + DX::ThrowIfFailed(device->CreateTexture2D(&texDesc, subresourceData, &tempTexture)); + DX::ThrowIfFailed(CaptureTexture(device, context, tempTexture, image)); - std::filesystem::path DynamicCubeMapSavePath = defaultDynamicCubeMapSavePath; - std::filesystem::path filename(std::format("R{:03d}G{:03d}B{:03d}A{:03d}.dds", colorPixel.r, colorPixel.g, colorPixel.b, colorPixel.a)); - DynamicCubeMapSavePath /= filename; + if (std::filesystem::create_directories(defaultDynamicCubeMapSavePath)) { + logger::info("Missing DynamicCubeMap Creator directory created: {}", defaultDynamicCubeMapSavePath); + } - if (std::filesystem::exists(DynamicCubeMapSavePath)) { - logger::info("DynamicCubeMap Creator file for {} already exists, skipping.", filename.string()); - } else { - DX::ThrowIfFailed(SaveToDDSFile(image.GetImages(), image.GetImageCount(), image.GetMetadata(), DirectX::DDS_FLAGS::DDS_FLAGS_NONE, DynamicCubeMapSavePath.c_str())); - logger::info("DynamicCubeMap Creator file for {} written", filename.string()); - } + std::filesystem::path DynamicCubeMapSavePath = defaultDynamicCubeMapSavePath; + std::filesystem::path filename(std::format("R{:03d}G{:03d}B{:03d}A{:03d}.dds", colorPixel.r, colorPixel.g, colorPixel.b, colorPixel.a)); + DynamicCubeMapSavePath /= filename; - } catch (const std::exception& e) { - logger::error("Failed in DynamicCubeMap Creator file: {} {}", defaultDynamicCubeMapSavePath, e.what()); + if (std::filesystem::exists(DynamicCubeMapSavePath)) { + logger::info("DynamicCubeMap Creator file for {} already exists, skipping.", filename.string()); + } else { + DX::ThrowIfFailed(SaveToDDSFile(image.GetImages(), image.GetImageCount(), image.GetMetadata(), DirectX::DDS_FLAGS::DDS_FLAGS_NONE, DynamicCubeMapSavePath.c_str())); + logger::info("DynamicCubeMap Creator file for {} written", filename.string()); } - image.Release(); - tempTexture->Release(); + } catch (const std::exception& e) { + logger::error("Failed in DynamicCubeMap Creator file: {} {}", defaultDynamicCubeMapSavePath, e.what()); } + + image.Release(); + tempTexture->Release(); } - ImGui::TreePop(); } - if (REL::Module::IsVR()) { - if (ImGui::TreeNodeEx("Advanced VR Settings", ImGuiTreeNodeFlags_DefaultOpen)) { - Util::RenderImGuiSettingsTree(iniVRCubeMapSettings, "VR"); - Util::RenderImGuiSettingsTree(hiddenVRCubeMapSettings, "hiddenVR"); - ImGui::TreePop(); - } + ImGui::TreePop(); + } + if (REL::Module::IsVR()) { + if (ImGui::TreeNodeEx("Advanced VR Settings", ImGuiTreeNodeFlags_DefaultOpen)) { + Util::RenderImGuiSettingsTree(iniVRCubeMapSettings, "VR"); + Util::RenderImGuiSettingsTree(hiddenVRCubeMapSettings, "hiddenVR"); + ImGui::TreePop(); } + } - ImGui::Spacing(); - ImGui::Spacing(); + ImGui::Spacing(); + ImGui::Spacing(); - ImGui::TreePop(); - } + ImGui::TreePop(); } void DynamicCubemaps::LoadSettings(json& o_json) diff --git a/src/Features/PerformanceOverlay.cpp b/src/Features/PerformanceOverlay.cpp index e134b9ac2f..14122a12bf 100644 --- a/src/Features/PerformanceOverlay.cpp +++ b/src/Features/PerformanceOverlay.cpp @@ -392,7 +392,12 @@ void PerformanceOverlay::DrawFPS() ImGui::TableNextColumn(); ImGui::Text(this->state.isFrameGenerationActive ? "Raw FPS:" : "FPS:"); ImGui::TableNextColumn(); - ImGui::Text("%.1f (%.2f ms)", this->state.smoothFps, this->state.smoothFrameTimeMs); + float avgFrameTime = std::accumulate(this->state.frameTimeHistory.GetData().begin(), + this->state.frameTimeHistory.GetData().end(), 0.0f) / + this->state.frameTimeHistory.GetData().size(); + float avgFps = avgFrameTime > 0.0f ? 1000.0f / avgFrameTime : 0.0f; + + ImGui::Text("%.1f (%.2f ms) | Avg: %.1f", this->state.smoothFps, this->state.smoothFrameTimeMs, avgFps); if (this->state.isFrameGenerationActive) { ImGui::TableNextColumn(); diff --git a/src/Features/VolumetricLighting.cpp b/src/Features/VolumetricLighting.cpp index 193fb19099..d3f408edcc 100644 --- a/src/Features/VolumetricLighting.cpp +++ b/src/Features/VolumetricLighting.cpp @@ -21,22 +21,19 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( void VolumetricLighting::DrawSettings() { - if (ImGui::TreeNodeEx("Settings", ImGuiTreeNodeFlags_DefaultOpen)) { - if (ImGui::Checkbox("Enable Volumetric Lighting in Exteriors", &settings.ExteriorEnabled)) - SetupVL(); + if (ImGui::Checkbox("Enable Volumetric Lighting in Exteriors", &settings.ExteriorEnabled)) + SetupVL(); - if (settings.ExteriorEnabled) - DrawVolumetricLightingSettings(settings.ExteriorQuality, settings.ExteriorCustomSize, false, !inInterior); + if (settings.ExteriorEnabled) + DrawVolumetricLightingSettings(settings.ExteriorQuality, settings.ExteriorCustomSize, false, !inInterior); - if (ImGui::Checkbox("Enable Volumetric Lighting in Interiors", &settings.InteriorEnabled)) - SetupVL(); + if (ImGui::Checkbox("Enable Volumetric Lighting in Interiors", &settings.InteriorEnabled)) + SetupVL(); - if (settings.InteriorEnabled) - DrawVolumetricLightingSettings(settings.InteriorQuality, settings.InteriorCustomSize, true, inInterior); + if (settings.InteriorEnabled) + DrawVolumetricLightingSettings(settings.InteriorQuality, settings.InteriorCustomSize, true, inInterior); - ImGui::Spacing(); - ImGui::TreePop(); - } + ImGui::Spacing(); } void VolumetricLighting::DrawVolumetricLightingSettings(int32_t& quality, TextureSize& customSize, const bool isInterior, const bool inLocationType) diff --git a/src/Menu/SettingsTabRenderer.cpp b/src/Menu/SettingsTabRenderer.cpp index f599e1482b..9dc95ff51e 100644 --- a/src/Menu/SettingsTabRenderer.cpp +++ b/src/Menu/SettingsTabRenderer.cpp @@ -38,7 +38,7 @@ void SettingsTabRenderer::RenderShadersTab() shaderCache->SetDiskCache(useDiskCache); } if (auto _tt = Util::HoverTooltipWrapper()) { - ImGui::Text("Disabling this stops shaders from being loaded from disk, as well as stops shaders from being saved to it."); + ImGui::Text("Disables loading shaders from disk and prevents saving compiled shaders to disk cache."); } bool useAsync = shaderCache->IsAsync(); @@ -49,6 +49,11 @@ void SettingsTabRenderer::RenderShadersTab() ImGui::Text("Skips a shader being replaced if it hasn't been compiled yet. Also makes compilation blazingly fast!"); } + if (shaderCache->GetTotalTasks() > 0) { + ImGui::Text("Last shader cache build duration: %s", + shaderCache->GetShaderStatsString(true, true).c_str()); + } + ImGui::EndTabItem(); } } diff --git a/src/ShaderCache.cpp b/src/ShaderCache.cpp index 72f4bcebb5..b3e67ea3d2 100644 --- a/src/ShaderCache.cpp +++ b/src/ShaderCache.cpp @@ -2057,9 +2057,9 @@ namespace SIE return ShaderCompilationTask::Status::Pending; } - std::string ShaderCache::GetShaderStatsString(bool a_timeOnly) + std::string ShaderCache::GetShaderStatsString(bool a_timeOnly, bool a_elapsedOnly) { - return compilationSet.GetStatsString(a_timeOnly); + return compilationSet.GetStatsString(a_timeOnly, a_elapsedOnly); } inline bool ShaderCache::IsShaderSourceAvailable(const RE::BSShader& shader) @@ -2614,14 +2614,22 @@ namespace SIE return std::max(remaining / rate, 0.0); } - std::string CompilationSet::GetStatsString(bool a_timeOnly) + std::string CompilationSet::GetStatsString(bool a_timeOnly, bool a_elapsedOnly) { double totalMs = static_cast(totalTime.QuadPart) * 1000.0 / frequency.QuadPart; - if (a_timeOnly) - return fmt::format("{}/{}", - GetHumanTime(totalMs), - GetHumanTime(GetEta() + totalMs)); + if (a_timeOnly) { + if (a_elapsedOnly) { + // Only elapsed + return GetHumanTime(totalMs); + } else { + // Elapsed + estimated + return fmt::format("{}/{}", + GetHumanTime(totalMs), + GetHumanTime(GetEta() + totalMs)); + } + } + return fmt::format("{}/{} (successful/total)\tfailed: {}\tcachehits: {}\nElapsed/Estimated Time: {}/{}", (std::uint64_t)completedTasks, (std::uint64_t)totalTasks, diff --git a/src/ShaderCache.h b/src/ShaderCache.h index 5f7d77fd6e..d0c8547967 100644 --- a/src/ShaderCache.h +++ b/src/ShaderCache.h @@ -260,7 +260,7 @@ namespace SIE void Clear(); std::string GetHumanTime(double a_totalMs); double GetEta(); - std::string GetStatsString(bool a_timeOnly = false); + std::string GetStatsString(bool a_timeOnly = false, bool a_elapsedOnly = false); std::atomic completedTasks = 0; std::atomic totalTasks = 0; std::atomic failedTasks = 0; @@ -396,7 +396,7 @@ namespace SIE ID3DBlob* GetCompletedShader(const SIE::ShaderCompilationTask& a_task); ID3DBlob* GetCompletedShader(ShaderClass shaderClass, const RE::BSShader& shader, uint32_t descriptor); ShaderCompilationTask::Status GetShaderStatus(const std::string& a_key); - std::string GetShaderStatsString(bool a_timeOnly = false); + std::string GetShaderStatsString(bool a_timeOnly = false, bool a_elapsedOnly = false); RE::BSGraphics::VertexShader* GetVertexShader(const RE::BSShader& shader, uint32_t descriptor); RE::BSGraphics::PixelShader* GetPixelShader(const RE::BSShader& shader, diff --git a/src/State.cpp b/src/State.cpp index 40ef3a90eb..9541882f14 100644 --- a/src/State.cpp +++ b/src/State.cpp @@ -159,25 +159,41 @@ static const std::string& GetConfigPath(State::ConfigMode a_configMode) } } +static bool LoadJsonFromFile(const std::string& path, json& outJson) +{ + std::ifstream file(path); + if (!file.is_open()) { + logger::warn("Unable to open config file: {}", path); + return false; + } + + try { + file >> outJson; + return true; + } catch (const nlohmann::json::parse_error& e) { + logger::warn("Error parsing json config file ({}): {}\n", path, e.what()); + return false; + } +} + void State::Load(ConfigMode a_configMode, bool a_allowReload) { + ConfigMode configMode = a_configMode; auto shaderCache = globals::shaderCache; - json settings; + json defaultSettings; + json userSettings; + json finalSettings; bool errorDetected = false; - auto configFolderPath = std::filesystem::path(GetConfigPath(a_configMode)).parent_path().string(); - auto defaultConfigFilePath = GetConfigPath(ConfigMode::DEFAULT); - auto userConfigFilePath = GetConfigPath(ConfigMode::USER); - try { - std::filesystem::create_directories(configFolderPath); + std::filesystem::create_directories(folderPath); } catch (const std::filesystem::filesystem_error& e) { - logger::warn("Error creating directory during Load ({}) : {}\n", configFolderPath, e.what()); + logger::warn("Error creating directory during Load ({}) : {}\n", folderPath, e.what()); errorDetected = true; } - // Attempt to load the config file - auto tryLoadConfig = [&](const std::string& path) -> bool { + // Lambda to load a config file + auto tryLoadConfig = [&](const std::string& path, json& settings) { std::ifstream i(path); logger::info("Attempting to open config file: {}", path); if (!i.is_open()) { @@ -195,70 +211,55 @@ void State::Load(ConfigMode a_configMode, bool a_allowReload) } }; - // NEW LOADING ORDER: Default → Overrides → User - - // Step 1: Always start with default settings - logger::info("Loading default settings from: {}", defaultConfigFilePath); - if (!tryLoadConfig(defaultConfigFilePath)) { - logger::info("No default config ({}), generating new one", defaultConfigFilePath); + // 1. Load default config + if (!tryLoadConfig(defaultConfigPath, defaultSettings)) { + logger::info("No default config ({}), generating new one", defaultConfigPath); + // Generate default settings std::fill(enabledClasses, enabledClasses + magic_enum::enum_integer(RE::BSShader::Type::Total) - 1, true); - Save(ConfigMode::DEFAULT); - // Attempt to load the newly created config - if (!tryLoadConfig(defaultConfigFilePath)) { - logger::error("Error opening newly created default config file ({})\n", defaultConfigFilePath); + SaveDefaults(); + if (!tryLoadConfig(defaultConfigPath, defaultSettings)) { + logger::error("Error opening newly created default config file ({})\n", defaultConfigPath); return; } } + finalSettings = defaultSettings; - // Step 2: Apply overrides (only new/changed ones) to default settings + // 2. Apply overrides auto overrideManager = SettingsOverrideManager::GetSingleton(); size_t overridesDiscovered = overrideManager->DiscoverOverrides(); json appliedOverrides = overrideManager->LoadAppliedOverridesTracking(); if (overridesDiscovered > 0) { logger::info("Discovered {} override files", overridesDiscovered); - - // Apply global overrides to main settings (only new/changed ones) - size_t newGlobalOverrides = overrideManager->ApplyNewOverrides(settings, appliedOverrides); + size_t newGlobalOverrides = overrideManager->ApplyNewOverrides(finalSettings, appliedOverrides); if (newGlobalOverrides > 0) { logger::info("Applied {} new/changed global override(s)", newGlobalOverrides); } } - // Step 3: Apply user settings on top of default + overrides - if (a_configMode == ConfigMode::USER) { - json userSettings; - std::ifstream userFile(userConfigFilePath); - if (userFile.is_open()) { - try { - userFile >> userSettings; - userFile.close(); - - // Merge user settings on top of (default + overrides) - for (auto& [key, value] : userSettings.items()) { - settings[key] = value; - } - logger::info("Applied user settings from: {}", userConfigFilePath); - } catch (const nlohmann::json::parse_error& e) { - logger::warn("Error parsing user config file: {}", e.what()); - userFile.close(); - } + // 3. Load user config and merge + if (configMode != ConfigMode::DEFAULT) { + std::string configPath = GetConfigPath(configMode); + if (tryLoadConfig(configPath, userSettings)) { + logger::info("Merging user config overrides from: {}", configPath); + MergeJsonSettings(finalSettings, userSettings); } else { - logger::info("No user config file found at: {}", userConfigFilePath); + logger::info("No user config found ({}), using defaults", configPath); } } + // 4. Apply final settings try { - // Load Menu settings - - if (settings["Menu"].is_object()) { + // Menu + if (finalSettings["Menu"].is_object()) { logger::info("Loading 'Menu' settings"); - globals::menu->Load(settings["Menu"]); + globals::menu->Load(finalSettings["Menu"]); } - if (settings["Advanced"].is_object()) { + // Advanced + if (finalSettings["Advanced"].is_object()) { logger::info("Loading 'Advanced' settings"); - json& advanced = settings["Advanced"]; + json& advanced = finalSettings["Advanced"]; if (advanced["Dump Shaders"].is_boolean()) shaderCache->SetDump(advanced["Dump Shaders"]); if (advanced["Log Level"].is_number_integer()) @@ -275,9 +276,10 @@ void State::Load(ConfigMode a_configMode, bool a_allowReload) frameAnnotations = advanced["Frame Annotations"]; } - if (settings["General"].is_object()) { + // General + if (finalSettings["General"].is_object()) { logger::info("Loading 'General' settings"); - json& general = settings["General"]; + json& general = finalSettings["General"]; if (general["Enable Shaders"].is_boolean()) shaderCache->SetEnabled(general["Enable Shaders"]); @@ -289,9 +291,10 @@ void State::Load(ConfigMode a_configMode, bool a_allowReload) shaderCache->SetAsync(general["Enable Async"]); } - if (settings["Replace Original Shaders"].is_object()) { + // Shaders + if (finalSettings["Replace Original Shaders"].is_object()) { logger::info("Loading 'Replace Original Shaders' settings"); - json& originalShaders = settings["Replace Original Shaders"]; + json& originalShaders = finalSettings["Replace Original Shaders"]; ForEachShaderTypeWithIndex([&](auto type, int classIndex) { auto name = magic_enum::enum_name(type); if (originalShaders[name].is_boolean()) { @@ -301,13 +304,14 @@ void State::Load(ConfigMode a_configMode, bool a_allowReload) } }); } + // Ensure 'Disable at Boot' section exists in the JSON - if (!settings.contains("Disable at Boot") || !settings["Disable at Boot"].is_object()) { + if (!finalSettings.contains("Disable at Boot") || !finalSettings["Disable at Boot"].is_object()) { // Initialize to an empty object if it doesn't exist - settings["Disable at Boot"] = json::object(); + finalSettings["Disable at Boot"] = json::object(); } - json& disabledFeaturesJson = settings["Disable at Boot"]; + json& disabledFeaturesJson = finalSettings["Disable at Boot"]; logger::info("Loading 'Disable at Boot' settings"); for (auto& [featureName, featureStatus] : disabledFeaturesJson.items()) { @@ -317,6 +321,7 @@ void State::Load(ConfigMode a_configMode, bool a_allowReload) logger::warn("Invalid entry for feature '{}' in 'Disable at Boot', expected boolean.", featureName); } } + for (const auto& [featureName, _] : specialFeatures) { if (IsFeatureDisabled(featureName)) { logger::info("Special Feature '{}' disabled at boot", featureName); @@ -324,7 +329,7 @@ void State::Load(ConfigMode a_configMode, bool a_allowReload) } auto upscaling = globals::upscaling; - auto& upscalingJson = settings[upscaling->GetShortName()]; + auto& upscalingJson = finalSettings[upscaling->GetShortName()]; if (upscalingJson.is_object()) { logger::info("Loading Upscaling settings"); try { @@ -337,7 +342,7 @@ void State::Load(ConfigMode a_configMode, bool a_allowReload) logger::warn("Missing settings for Upscaling, using default."); } - // Feature loading with new override tracking system + // Feature loading with overrides for (auto* feature : Feature::GetFeatureList()) { try { const std::string featureName = feature->GetShortName(); @@ -345,19 +350,19 @@ void State::Load(ConfigMode a_configMode, bool a_allowReload) if (!isDisabled) { logger::info("Loading Feature: '{}'", featureName); - // Load base feature settings from merged config (default + user) - feature->Load(settings); + // Load feature settings from merged config (default + overrides + user) + feature->Load(finalSettings); - // Apply new/changed feature-specific overrides if any + // Apply feature-specific overrides if any if (overridesDiscovered > 0) { json featureJson; - feature->SaveSettings(featureJson); // Get current settings as JSON + feature->SaveSettings(featureJson); // Save current settings to JSON size_t newFeatureOverrides = overrideManager->ApplyNewFeatureOverrides(featureName, featureJson, appliedOverrides); if (newFeatureOverrides > 0) { logger::info("Applied {} new/changed override(s) to {}", newFeatureOverrides, feature->GetName()); try { - feature->LoadSettings(featureJson); // Reload with new overrides applied + feature->LoadSettings(featureJson); // Reload settings after applying overrides } catch (...) { logger::warn("Invalid override settings for {}, keeping original settings.", feature->GetName()); } @@ -379,9 +384,9 @@ void State::Load(ConfigMode a_configMode, bool a_allowReload) overrideManager->SaveAppliedOverridesTracking(appliedOverrides); } - if (settings["Version"].is_string() && settings["Version"].get() != Plugin::VERSION.string()) { - logger::info("Found older config for version {}; upgrading to {}", (std::string)settings["Version"], Plugin::VERSION.string()); - Save(a_configMode); // Use original config mode + if (finalSettings["Version"].is_string() && finalSettings["Version"].get() != Plugin::VERSION.string()) { + logger::info("Found older config for version {}; upgrading to {}", (std::string)finalSettings["Version"], Plugin::VERSION.string()); + Save(configMode); } FeatureIssues::ScanForOrphanedFeatureINIs(); @@ -402,9 +407,7 @@ void State::Load(ConfigMode a_configMode, bool a_allowReload) void State::Save(ConfigMode a_configMode) { - const auto shaderCache = globals::shaderCache; std::string configPath = GetConfigPath(a_configMode); - std::ofstream o{ configPath }; try { std::filesystem::create_directories(folderPath); @@ -413,57 +416,63 @@ void State::Save(ConfigMode a_configMode) return; } - // Check if the file opened successfully - if (!o.is_open()) { - logger::warn("Failed to open config file for saving: {}", configPath); - return; // Exit early if file cannot be opened + if (a_configMode == ConfigMode::DEFAULT) { + // Save complete default settings + SaveDefaults(); + return; } - json settings; - - globals::menu->Save(settings["Menu"]); - - json advanced; - advanced["Dump Shaders"] = shaderCache->IsDump(); - advanced["Log Level"] = logLevel; - advanced["Shader Defines"] = shaderDefinesString; - advanced["Compiler Threads"] = shaderCache->compilationThreadCount; - advanced["Background Compiler Threads"] = shaderCache->backgroundCompilationThreadCount; - advanced["Use FileWatcher"] = shaderCache->UseFileWatcher(); - advanced["Frame Annotations"] = frameAnnotations; - settings["Advanced"] = advanced; - - json general; - general["Enable Shaders"] = shaderCache->IsEnabled(); - general["Enable Disk Cache"] = shaderCache->IsDiskCache(); - general["Enable Async"] = shaderCache->IsAsync(); - - settings["General"] = general; + // For user/test configs, save only differences from default + json currentSettings = GetCurrentSettings(); + json defaultSettings; - auto upscaling = globals::upscaling; - auto& upscalingJson = settings[upscaling->GetShortName()]; - upscaling->SaveSettings(upscalingJson); + // Load default settings for comparison + std::ifstream defaultFile(defaultConfigPath); + if (defaultFile.is_open()) { + try { + defaultFile >> defaultSettings; + } catch (const nlohmann::json::parse_error& e) { + logger::warn("Error parsing default config for comparison: {}", e.what()); + // If can't read defaults, save full config as fallback + SaveFullConfig(a_configMode, currentSettings); + return; + } + defaultFile.close(); + } else { + logger::warn("Cannot open default config for comparison, saving full config"); + SaveFullConfig(a_configMode, currentSettings); + return; + } - json originalShaders; - ForEachShaderTypeWithIndex([&](auto type, int classIndex) { - originalShaders[magic_enum::enum_name(type)] = enabledClasses[classIndex]; - }); - settings["Replace Original Shaders"] = originalShaders; + json overrideSettings; + CreateSettingsDiff(defaultSettings, currentSettings, overrideSettings); - json disabledFeaturesJson; - for (const auto& [featureName, isDisabled] : disabledFeatures) { - disabledFeaturesJson[featureName] = isDisabled; + // Check for differences + if (overrideSettings.empty()) { + // Remove user config if identical to defaults + if (std::filesystem::exists(configPath)) { + try { + std::filesystem::remove(configPath); + logger::info("Removed user config file as all settings match defaults: {}", configPath); + } catch (const std::filesystem::filesystem_error& e) { + logger::warn("Failed to remove empty user config file: {}. Error: {}", configPath, e.what()); + } + } else { + logger::info("No user config differences to save - keeping file empty"); + } + return; } - settings["Disable at Boot"] = disabledFeaturesJson; - - settings["Version"] = Plugin::VERSION.string(); - for (auto* feature : Feature::GetFeatureList()) - feature->Save(settings); + // Save the override settings + std::ofstream o{ configPath }; + if (!o.is_open()) { + logger::warn("Failed to open config file for saving: {}", configPath); + return; + } try { - o << settings.dump(1); - logger::info("Saving settings to {}", configPath); + o << overrideSettings.dump(1); + logger::info("Saving override settings to {}", configPath); } catch (const std::exception& e) { logger::warn("Failed to write settings to file: {}. Error: {}", configPath, e.what()); } @@ -894,3 +903,114 @@ float State::GetTotalSmoothedDrawCalls() const { return static_cast(smoothDrawCalls[magic_enum::enum_integer(RE::BSShader::Type::Total)]); } + +void State::SaveDefaults() +{ + json settings = GetCurrentSettings(); + + std::ofstream o{ defaultConfigPath }; + if (!o.is_open()) { + logger::warn("Failed to open default config file for saving: {}", defaultConfigPath); + return; + } + + try { + o << settings.dump(1); + logger::info("Saving default settings to {}", defaultConfigPath); + } catch (const std::exception& e) { + logger::warn("Failed to write default settings to file: {}. Error: {}", defaultConfigPath, e.what()); + } +} + +void State::SaveFullConfig(ConfigMode a_configMode, const json& settings) +{ + std::string configPath = GetConfigPath(a_configMode); + std::ofstream o{ configPath }; + + if (!o.is_open()) { + logger::warn("Failed to open config file for saving: {}", configPath); + return; + } + + try { + o << settings.dump(1); + logger::info("Saving full settings to {}", configPath); + } catch (const std::exception& e) { + logger::warn("Failed to write settings to file: {}. Error: {}", configPath, e.what()); + } +} + +json State::GetCurrentSettings() +{ + const auto shaderCache = globals::shaderCache; + json settings; + + globals::menu->Save(settings["Menu"]); + + json advanced; + advanced["Dump Shaders"] = shaderCache->IsDump(); + advanced["Log Level"] = logLevel; + advanced["Shader Defines"] = shaderDefinesString; + advanced["Compiler Threads"] = shaderCache->compilationThreadCount; + advanced["Background Compiler Threads"] = shaderCache->backgroundCompilationThreadCount; + advanced["Use FileWatcher"] = shaderCache->UseFileWatcher(); + advanced["Frame Annotations"] = frameAnnotations; + settings["Advanced"] = advanced; + + json general; + general["Enable Shaders"] = shaderCache->IsEnabled(); + general["Enable Disk Cache"] = shaderCache->IsDiskCache(); + general["Enable Async"] = shaderCache->IsAsync(); + settings["General"] = general; + + auto upscaling = globals::upscaling; + auto& upscalingJson = settings[upscaling->GetShortName()]; + upscaling->SaveSettings(upscalingJson); + + json originalShaders; + ForEachShaderTypeWithIndex([&](auto type, int classIndex) { + originalShaders[magic_enum::enum_name(type)] = enabledClasses[classIndex]; + }); + settings["Replace Original Shaders"] = originalShaders; + + json disabledFeaturesJson; + for (const auto& [featureName, isDisabled] : disabledFeatures) { + disabledFeaturesJson[featureName] = isDisabled; + } + settings["Disable at Boot"] = disabledFeaturesJson; + + settings["Version"] = Plugin::VERSION.string(); + + for (auto* feature : Feature::GetFeatureList()) + feature->Save(settings); + + return settings; +} + +void State::MergeJsonSettings(json& target, const json& source) +{ + for (auto& [key, value] : source.items()) { + if (value.is_object() && target[key].is_object()) { + MergeJsonSettings(target[key], value); + } else { + target[key] = value; + } + } +} + +void State::CreateSettingsDiff(const json& defaultSettings, const json& currentSettings, json& diffSettings) +{ + for (auto& [key, currentValue] : currentSettings.items()) { + if (defaultSettings.contains(key)) { + if (currentValue.is_object() && defaultSettings[key].is_object()) { + json subDiff; + CreateSettingsDiff(defaultSettings[key], currentValue, subDiff); + if (!subDiff.empty()) { + diffSettings[key] = subDiff; + } + } else if (currentValue != defaultSettings[key]) { + diffSettings[key] = currentValue; + } + } + } +} \ No newline at end of file diff --git a/src/State.h b/src/State.h index 9b62847e75..f245b39803 100644 --- a/src/State.h +++ b/src/State.h @@ -87,6 +87,7 @@ class State void Load(ConfigMode a_configMode = ConfigMode::USER, bool a_allowReload = true); void Save(ConfigMode a_configMode = ConfigMode::USER); + void SaveUserOverrides(); void PostPostLoad(); bool ValidateCache(CSimpleIniA& a_ini); @@ -308,4 +309,11 @@ class State std::shared_ptr pPerf; bool initialized = false; std::mutex statsMutex; + + // Config override system + void SaveDefaults(); + void SaveFullConfig(ConfigMode a_configMode, const json& settings); + json GetCurrentSettings(); + void MergeJsonSettings(json& target, const json& source); + void CreateSettingsDiff(const json& defaultSettings, const json& currentSettings, json& diffSettings); };