Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
14 changes: 14 additions & 0 deletions src/Feature.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include "FeatureConstraints.h"
#include "FeatureVersions.h"
#ifdef TRACY_ENABLE
# include <Tracy/Tracy.hpp>
Expand Down Expand Up @@ -138,6 +139,19 @@ struct Feature
*/
virtual void RegisterWeatherVariables() {}

/**
* @brief Returns constraints this feature imposes on other features' settings
*
* Features override this to declare runtime incompatibilities with other features.
* The constraint system will automatically:
* - Force the target setting to the specified value
* - Disable the UI control for the constrained setting
* - Show a tooltip explaining which features caused the constraint
*
* @return Vector of constraints this feature currently imposes (empty if none)
*/
virtual std::vector<FeatureConstraints::Constraint> GetActiveConstraints() const { return {}; }

virtual bool ValidateCache(CSimpleIniA& a_ini);
virtual void WriteDiskCacheInfo(CSimpleIniA& a_ini);
virtual void ClearShaderCache() {}
Expand Down
96 changes: 96 additions & 0 deletions src/FeatureConstraints.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#include "FeatureConstraints.h"
#include "Feature.h"

#include <unordered_set>

namespace FeatureConstraints
{
ConstraintResult GetConstraints(const SettingId& setting)
{
ConstraintResult result;

for (auto* feature : Feature::GetFeatureList()) {
if (!feature->loaded)
continue;

auto constraints = feature->GetActiveConstraints();
for (const auto& constraint : constraints) {
if (constraint.targetSetting == setting) {
if (!result.isConstrained) {
result.isConstrained = true;
result.forcedValue = constraint.forcedValue;
} else if (constraint.forcedValue != result.forcedValue) {
// Two features disagree on the forced value; first one wins.
// Log once so it surfaces during development / testing.
logger::warn("[FeatureConstraints] Conflict on {}.{}: {} wants {}, but {} already forced {}",
setting.featureShortName, setting.settingPath,
feature->GetName(), FormatConstraintValue(constraint.forcedValue),
result.sources[0].featureName, FormatConstraintValue(result.forcedValue));
}
result.sources.push_back({ feature->GetName(),
feature->GetShortName(),
constraint.reason,
constraint.recommendDisableAtBoot });
}
Comment thread
alandtse marked this conversation as resolved.
}
}

return result;
}

std::vector<std::pair<SettingId, ConstraintResult>> GetAllActiveConstraints()
{
std::vector<std::pair<SettingId, ConstraintResult>> allConstraints;
std::unordered_set<std::string> processedKeys; // featureShortName|settingPath for O(1) lookup

for (auto* feature : Feature::GetFeatureList()) {
if (!feature->loaded)
continue;

auto constraints = feature->GetActiveConstraints();
for (const auto& constraint : constraints) {
std::string key = constraint.targetSetting.featureShortName + "|" + constraint.targetSetting.settingPath;
if (processedKeys.insert(key).second) {
auto result = GetConstraints(constraint.targetSetting);
if (result.isConstrained) {
allConstraints.push_back({ constraint.targetSetting, result });
}
}
}
}

return allConstraints;
}

std::string BuildConstraintTooltip(const ConstraintResult& result)
{
if (!result.isConstrained || result.sources.empty())
return "";

std::string tooltip = "This setting is constrained by:\n";
for (const auto& src : result.sources) {
tooltip += "\n- " + src.featureName + ":\n " + src.reason;
if (src.recommendDisableAtBoot) {
tooltip += "\n (Consider disabling this feature at boot for best compatibility)";
}
}

tooltip += "\n\nForced value: " + FormatConstraintValue(result.forcedValue);

return tooltip;
}

std::string FormatConstraintValue(const std::variant<bool, int, float>& value)
{
if (std::holds_alternative<bool>(value)) {
return std::get<bool>(value) ? "Enabled" : "Disabled";
} else if (std::holds_alternative<int>(value)) {
return std::to_string(std::get<int>(value));
} else if (std::holds_alternative<float>(value)) {
char buf[32];
snprintf(buf, sizeof(buf), "%.2f", std::get<float>(value));
return buf;
}
return "Unknown";
}
}
90 changes: 90 additions & 0 deletions src/FeatureConstraints.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#pragma once

#include <string>
#include <variant>
#include <vector>

namespace FeatureConstraints
{
/**
* @brief Identifies a specific setting that can be constrained
*/
struct SettingId
{
std::string featureShortName; // e.g., "VR"
std::string settingPath; // e.g., "EnableDepthBufferCullingExterior"

bool operator==(const SettingId& other) const
{
return featureShortName == other.featureShortName && settingPath == other.settingPath;
}
};

/**
* @brief A constraint that one feature places on another feature's setting
*/
struct Constraint
{
SettingId targetSetting; // Which setting is affected
std::variant<bool, int, float> forcedValue; // Value to force
std::string reason; // UI tooltip explanation
bool recommendDisableAtBoot = false; // Suggest disabling the source feature entirely
};

/**
* @brief Result of checking constraints on a setting
*/
struct ConstraintResult
{
bool isConstrained = false;
std::variant<bool, int, float> forcedValue;

struct Source
{
std::string featureName; // Display name (e.g. "Terrain Blending")
std::string featureShortName; // Menu navigation key (e.g. "TerrainBlending")
std::string reason;
bool recommendDisableAtBoot;
};
std::vector<Source> sources;

/**
* @brief Check if any source recommends disabling at boot
*/
bool AnyRecommendDisableAtBoot() const
{
for (const auto& src : sources) {
if (src.recommendDisableAtBoot)
return true;
}
return false;
}
};

/**
* @brief Query if a setting is constrained by any active feature
* @param setting The setting to check
* @return ConstraintResult with all sources causing the constraint
*/
ConstraintResult GetConstraints(const SettingId& setting);

/**
* @brief Get all active constraints across all features
* @return Vector of setting IDs and their constraint results
*/
std::vector<std::pair<SettingId, ConstraintResult>> GetAllActiveConstraints();

/**
* @brief Build a formatted tooltip string for a constrained setting
* @param result The constraint result to format
* @return Formatted string suitable for ImGui tooltip
*/
std::string BuildConstraintTooltip(const ConstraintResult& result);

/**
* @brief Format a constraint value as a string for display
* @param value The variant value to format
* @return String representation of the value
*/
std::string FormatConstraintValue(const std::variant<bool, int, float>& value);
}
37 changes: 37 additions & 0 deletions src/Features/TerrainBlending.cpp
Original file line number Diff line number Diff line change
@@ -1,18 +1,55 @@
#include "TerrainBlending.h"

#include "Deferred.h"
#include "Globals.h"
#include "ShaderCache.h"
#include "State.h"
#include "VR.h"

NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(
TerrainBlending::Settings,
Enabled)

std::vector<FeatureConstraints::Constraint> TerrainBlending::GetActiveConstraints() const
{
std::vector<FeatureConstraints::Constraint> constraints;

// Only impose constraints when the feature is loaded, enabled, and we're in VR
if (!loaded || !settings.Enabled || !globals::game::isVR) {
return constraints;
}

// Terrain Blending has visual issues with VR depth buffer culling in exteriors
constraints.push_back({ { "VR", "EnableDepthBufferCullingExterior" },
false,
"Terrain Blending has visual issues with VR depth buffer culling in exteriors.",
false });

return constraints;
}

void TerrainBlending::DrawSettings()
{
bool wasEnabled = settings.Enabled;
ImGui::Checkbox("Enable Terrain Blending", (bool*)&settings.Enabled);

// Show warning if enabling in VR and depth culling is currently enabled
if (globals::game::isVR && settings.Enabled && !wasEnabled) {
// Check if VR depth culling exterior is currently enabled
auto& vr = globals::features::vr;
if (vr.settings.EnableDepthBufferCullingExterior) {
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f),
"Note: VR Depth Buffer Culling (Exteriors) will be disabled while this feature is enabled.");
}
}

if (auto _tt = Util::HoverTooltipWrapper()) {
ImGui::Text("Enable seamless blending between terrain and objects.");
if (globals::game::isVR) {
ImGui::Separator();
ImGui::TextColored(ImVec4(1.0f, 0.8f, 0.2f, 1.0f), "VR Note:");
ImGui::TextWrapped("When enabled in VR, this feature requires disabling Depth Buffer Culling in exteriors to prevent visual issues.");
}
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/Features/TerrainBlending.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ struct TerrainBlending : Feature
};
}
virtual inline bool HasShaderDefine(RE::BSShader::Type) override { return true; }
virtual bool SupportsVR() override { return true; }
Comment thread
alandtse marked this conversation as resolved.
virtual std::vector<FeatureConstraints::Constraint> GetActiveConstraints() const override;

struct Settings
{
Expand Down Expand Up @@ -110,5 +112,4 @@ struct TerrainBlending : Feature
logger::info("[Terrain Blending] Installed hooks");
}
};
virtual bool SupportsVR() override { return false; };
};
29 changes: 27 additions & 2 deletions src/Features/Upscaling.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ void Upscaling::PostPostLoad()
logger::info("[Upscaling] Installed hooks");
}

Upscaling::UpscaleMethod Upscaling::GetUpscaleMethod()
Upscaling::UpscaleMethod Upscaling::GetUpscaleMethod() const
{
if (streamline.featureDLSS)
return (UpscaleMethod)settings.upscaleMethod;
Expand Down Expand Up @@ -1091,7 +1091,7 @@ bool Upscaling::IsFrameGenerationActive() const
return d3d12SwapChainActive && settings.frameGenerationMode && fidelityFX.isFrameGenActive && !globals::game::isVR;
}

bool Upscaling::IsUpscalingActive()
bool Upscaling::IsUpscalingActive() const
{
auto method = GetUpscaleMethod();

Expand All @@ -1107,6 +1107,31 @@ bool Upscaling::IsUpscalingActive()
return resolutionScale.x < .99f;
}

std::vector<FeatureConstraints::Constraint> Upscaling::GetActiveConstraints() const
{
std::vector<FeatureConstraints::Constraint> constraints;

if (!IsUpscalingActive()) {
return constraints;
}

// When upscaling is active in VR, depth buffer culling must be disabled
// because upscalers modify the depth buffer, causing incorrect occlusion
if (globals::game::isVR) {
constraints.push_back({ { "VR", "EnableDepthBufferCullingExterior" },
false,
"Upscaling modifies the depth buffer, causing incorrect VR occlusion tests in exteriors.",
false });

constraints.push_back({ { "VR", "EnableDepthBufferCullingInterior" },
false,
"Upscaling modifies the depth buffer, causing incorrect VR occlusion tests in interiors.",
false });
}

return constraints;
}

/**
* @brief Retrieves the current frame time for frame generation.
*
Expand Down
6 changes: 4 additions & 2 deletions src/Features/Upscaling.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ struct Upscaling : Feature
};
}

virtual std::vector<FeatureConstraints::Constraint> GetActiveConstraints() const override;

float2 jitter = { 0, 0 };

enum class UpscaleMethod
Expand Down Expand Up @@ -90,7 +92,7 @@ struct Upscaling : Feature
// FG FPS Measurement for Overlay
bool IsFrameGenerationActive() const;
float GetFrameGenerationFrameTime() const;
bool IsUpscalingActive();
bool IsUpscalingActive() const;

// Feature interface overrides
virtual void DrawSettings() override;
Expand All @@ -108,7 +110,7 @@ struct Upscaling : Feature
virtual void PostPostLoad() override;
virtual void SetupResources() override;

UpscaleMethod GetUpscaleMethod();
UpscaleMethod GetUpscaleMethod() const;

void CheckResources(UpscaleMethod a_upscalemethod);
void CreateUpscalingTextureResources(UpscaleMethod a_upscalemethod);
Expand Down
Loading