/** * @file cvar.h * @brief cvar (configuration variable) system * * @section cvar_defining Defining CVars * * Define cvars in a single .cpp file using REXCVAR_DEFINE_* macros: * @code * // In flags.cpp or your module's .cpp file * REXCVAR_DEFINE_BOOL(my_flag, false, "Category", "Description"); * REXCVAR_DEFINE_INT32(my_int, 42, "Category", "An integer setting"); * REXCVAR_DEFINE_STRING(my_string, "default", "Category", "A string setting"); * @endcode * * Available types: BOOL, INT32, INT64, UINT32, UINT64, DOUBLE, STRING, COMMAND * * @section cvar_declaring Declaring CVars (for use in other files) * * @code * // In a header or other .cpp that needs access * REXCVAR_DECLARE(bool, my_flag); * REXCVAR_DECLARE(int32_t, my_int); * REXCVAR_DECLARE(std::string, my_string); * @endcode * * @section cvar_access Getting and Setting Values * * @code * // Type-safe access (preferred for known cvars) * bool value = REXCVAR_GET(my_flag); * REXCVAR_SET(my_flag, true); * * // String-based access (for dynamic/runtime lookup) * std::string str_val = rex::cvar::GetFlagByName("my_flag"); * rex::cvar::SetFlagByName("my_flag", "true"); * @endcode * * @section cvar_metadata Adding Metadata * * Chain metadata methods after the DEFINE macro: * @code * REXCVAR_DEFINE_INT32(scale, 1, "GPU", "Resolution scale") * .range(1, 8) * .lifecycle(rex::cvar::Lifecycle::kRequiresRestart); * * REXCVAR_DEFINE_STRING(backend, "d3d12", "GPU", "Render backend") * .allowed({"d3d12", "vulkan"}) * .lifecycle(rex::cvar::Lifecycle::kInitOnly); * * REXCVAR_DEFINE_BOOL(debug_overlay, false, "Debug", "Show overlay") * .debug_only(); * @endcode * * @section cvar_guidelines Metadata Guidelines * * - .lifecycle(kInitOnly) - Device/backend selection that cannot change after init * - .lifecycle(kRequiresRestart) - Settings that need restart to take effect * - .lifecycle(kHotReload) - Default; can change at runtime with immediate effect * - .range(min, max) - Numeric bounds validation * - .allowed({...}) - String enum validation * - .debug_only() - Mark as debug-only (for filtering in release UIs) * - .validator(fn) - Custom validation function * * @section cvar_query Querying CVars * * @code * // List all cvars * auto all = rex::cvar::ListFlags(); * * // Query by category or lifecycle * auto gpu_flags = rex::cvar::ListFlagsByCategory("GPU"); * auto init_only = rex::cvar::ListFlagsByLifecycle(rex::cvar::Lifecycle::kInitOnly); * * // Get metadata for a specific cvar * const auto* info = rex::cvar::GetFlagInfo("my_flag"); * if (info) { * // Access info->lifecycle, info->constraints, info->description, etc. * } * * // Check for modified values * auto modified = rex::cvar::ListModifiedFlags(); * @endcode * * @copyright Copyright (c) 2026 Tom Clay * @license BSD 3-Clause License */ #pragma once #include #include #include #include #include #include #include #include #include namespace rex::cvar { //============================================================================= // Initialization API //============================================================================= std::vector Init(int argc, char** argv); void LoadConfig(const std::filesystem::path& config_path); void ApplyEnvironment(); void FinalizeInit(); bool IsFinalized(); void SaveConfig(const std::filesystem::path& config_path); //============================================================================= // Flag Registry //============================================================================= enum class FlagType { Boolean, Int32, Int64, Uint32, Uint64, Double, String, Command }; // Lifecycle: when can this flag be modified? enum class Lifecycle { kInitOnly, // Can only be set during initialization (before FinalizeInit) kHotReload, // Can be changed at runtime with immediate effect kRequiresRestart // Can be changed, but only takes effect after restart }; // Validation constraints struct Constraints { std::optional min; std::optional max; std::vector allowed_values; std::function custom_validator; bool HasRangeConstraint() const { return min.has_value() || max.has_value(); } bool HasAllowedValues() const { return !allowed_values.empty(); } }; struct FlagEntry { std::string name; FlagType type; std::string category; std::string description; std::function setter; std::function getter; std::function command_callback; Lifecycle lifecycle = Lifecycle::kHotReload; Constraints constraints; std::string default_value; bool is_debug_only = false; }; std::vector& GetRegistry(); /** * Returns the registered entry's index, or nullopt if the name was already * registered (logged at ERROR). */ std::optional RegisterFlag(FlagEntry entry); /** * Removes a flag from the registry. Used by `FlagRegistrar`'s destructor so * that DLL unload tears down the lambdas captured in each FlagEntry. */ void UnregisterFlag(std::string_view name); bool SetFlagByName(std::string_view name, std::string_view value); std::string GetFlagByName(std::string_view name); // Typed registry query. Cross-DLL access path that does not require linking // the DLL where the cvar is defined. Slower than REXCVAR_GET (string parse + // hash lookup), so prefer REXCVAR_GET when the defining DLL is already on the // link line. Returns a value-initialized T when the cvar is missing or its // stored string fails to parse. template T Query(std::string_view name); template <> bool Query(std::string_view name); template <> int32_t Query(std::string_view name); template <> int64_t Query(std::string_view name); template <> uint32_t Query(std::string_view name); template <> uint64_t Query(std::string_view name); template <> double Query(std::string_view name); template <> std::string Query(std::string_view name); std::vector ListFlags(); std::vector ListFlagsByCategory(std::string_view category); std::vector ListFlagsByLifecycle(Lifecycle lc); const FlagEntry* GetFlagInfo(std::string_view name); std::vector GetPendingRestartFlags(); void ClearPendingRestartFlags(); void ResetToDefault(std::string_view name); void ResetAllToDefaults(); bool HasNonDefaultValue(std::string_view name); std::vector ListModifiedFlags(); std::string SerializeToTOML(); std::string SerializeToTOML(std::string_view category); /// Callback invoked when a CVAR value changes /// @param name The CVAR name /// @param new_value The new value as a string using ChangeCallback = std::function; /// Register a callback to be invoked when a specific CVAR changes void RegisterChangeCallback(std::string_view name, ChangeCallback callback); /// Unregister all callbacks for a specific CVAR void UnregisterChangeCallbacks(std::string_view name); /** * RAII handle for a registered flag. Destructor unregisters by name; on * duplicate-name registration `owned_name_` is empty so chain methods and * the destructor become no-ops and the original owner's entry is untouched. */ struct FlagRegistrar { std::string owned_name_; // empty when registration was rejected explicit FlagRegistrar(FlagEntry e) { std::string name = e.name; if (RegisterFlag(std::move(e)).has_value()) { owned_name_ = std::move(name); } } FlagRegistrar(FlagRegistrar&& other) noexcept : owned_name_(std::move(other.owned_name_)) { other.owned_name_.clear(); } ~FlagRegistrar() { if (!owned_name_.empty()) { UnregisterFlag(owned_name_); } } // Chain methods mutate the registered entry by name lookup. FlagRegistrar&& range(double min_val, double max_val) && { apply_([=](FlagEntry& entry) { entry.constraints.min = min_val; entry.constraints.max = max_val; }); return std::move(*this); } FlagRegistrar&& allowed(std::initializer_list values) && { std::vector vals(values); apply_([vals = std::move(vals)](FlagEntry& entry) { entry.constraints.allowed_values = vals; }); return std::move(*this); } FlagRegistrar&& lifecycle(Lifecycle lc) && { apply_([=](FlagEntry& entry) { entry.lifecycle = lc; }); return std::move(*this); } FlagRegistrar&& debug_only() && { apply_([](FlagEntry& entry) { entry.is_debug_only = true; }); return std::move(*this); } FlagRegistrar&& validator(std::function fn) && { apply_([fn = std::move(fn)](FlagEntry& entry) mutable { entry.constraints.custom_validator = std::move(fn); }); return std::move(*this); } // Non-copyable (prevent double registration) FlagRegistrar(const FlagRegistrar&) = delete; FlagRegistrar& operator=(const FlagRegistrar&) = delete; FlagRegistrar& operator=(FlagRegistrar&&) = delete; private: void apply_(std::function fn); }; inline bool ParseDouble(std::string_view s, double& out) { if (s.empty()) return false; char* end = nullptr; std::string str(s); out = std::strtod(str.c_str(), &end); return end != str.c_str() && *end == '\0'; } } // namespace rex::cvar //============================================================================= // CVar Macros //============================================================================= // Declare a cvar (use in headers and TUs that need to read it). // The accessor function returns a reference to the cvar's storage. Storage // lives as a static-local inside whichever DLL contains the matching // REXCVAR_DEFINE_*. Cross-DLL access goes through the import lib. #define REXCVAR_DECLARE(type, name) type& FLAGS_##name##_storage_() // Get a cvar value #define REXCVAR_GET(name) (FLAGS_##name##_storage_()) // Set a cvar value #define REXCVAR_SET(name, value) (FLAGS_##name##_storage_() = (value)) // Cross-module typed query that goes through the cvar registry by name. // Use this when the defining DLL is not on the consumer's link line (e.g., // across one-way subsystem dependencies where adding the reverse link would // create a cycle). Slower than REXCVAR_GET; prefer REXCVAR_GET when possible. #define REXCVAR_QUERY(type, name) (::rex::cvar::Query(#name)) // Define cvars (use in one .cpp file per cvar) // The FlagRegistrar registers the flag in its destructor, allowing method chaining. #define REXCVAR_DEFINE_BOOL(name, default_val, category, desc) \ bool& FLAGS_##name##_storage_() { \ static bool storage = (default_val); \ return storage; \ } \ static auto _cvar_reg_##name = \ ::rex::cvar::FlagRegistrar({#name, \ ::rex::cvar::FlagType::Boolean, \ category, \ desc, \ [](std::string_view v) { \ bool val = (v == "true" || v == "1" || v == "yes"); \ FLAGS_##name##_storage_() = val; \ return true; \ }, \ []() { return FLAGS_##name##_storage_() ? "true" : "false"; }, \ []() { return; }, \ ::rex::cvar::Lifecycle::kHotReload, \ {}, \ (default_val) ? "true" : "false", \ false}) #define REXCVAR_DEFINE_INT32(name, default_val, category, desc) \ int32_t& FLAGS_##name##_storage_() { \ static int32_t storage = (default_val); \ return storage; \ } \ static auto _cvar_reg_##name = \ ::rex::cvar::FlagRegistrar({#name, \ ::rex::cvar::FlagType::Int32, \ category, \ desc, \ [](std::string_view v) { \ int32_t val = 0; \ auto [ptr, ec] = \ std::from_chars(v.data(), v.data() + v.size(), val); \ if (ec != std::errc()) \ return false; \ FLAGS_##name##_storage_() = val; \ return true; \ }, \ []() { return std::to_string(FLAGS_##name##_storage_()); }, \ []() { return; }, \ ::rex::cvar::Lifecycle::kHotReload, \ {}, \ std::to_string(default_val), \ false}) #define REXCVAR_DEFINE_INT64(name, default_val, category, desc) \ int64_t& FLAGS_##name##_storage_() { \ static int64_t storage = (default_val); \ return storage; \ } \ static auto _cvar_reg_##name = \ ::rex::cvar::FlagRegistrar({#name, \ ::rex::cvar::FlagType::Int64, \ category, \ desc, \ [](std::string_view v) { \ int64_t val = 0; \ auto [ptr, ec] = \ std::from_chars(v.data(), v.data() + v.size(), val); \ if (ec != std::errc()) \ return false; \ FLAGS_##name##_storage_() = val; \ return true; \ }, \ []() { return std::to_string(FLAGS_##name##_storage_()); }, \ []() { return; }, \ ::rex::cvar::Lifecycle::kHotReload, \ {}, \ std::to_string(default_val), \ false}) #define REXCVAR_DEFINE_UINT32(name, default_val, category, desc) \ uint32_t& FLAGS_##name##_storage_() { \ static uint32_t storage = (default_val); \ return storage; \ } \ static auto _cvar_reg_##name = \ ::rex::cvar::FlagRegistrar({#name, \ ::rex::cvar::FlagType::Uint32, \ category, \ desc, \ [](std::string_view v) { \ uint32_t val = 0; \ auto [ptr, ec] = \ std::from_chars(v.data(), v.data() + v.size(), val); \ if (ec != std::errc()) \ return false; \ FLAGS_##name##_storage_() = val; \ return true; \ }, \ []() { return std::to_string(FLAGS_##name##_storage_()); }, \ []() { return; }, \ ::rex::cvar::Lifecycle::kHotReload, \ {}, \ std::to_string(default_val), \ false}) #define REXCVAR_DEFINE_UINT64(name, default_val, category, desc) \ uint64_t& FLAGS_##name##_storage_() { \ static uint64_t storage = (default_val); \ return storage; \ } \ static auto _cvar_reg_##name = \ ::rex::cvar::FlagRegistrar({#name, \ ::rex::cvar::FlagType::Uint64, \ category, \ desc, \ [](std::string_view v) { \ uint64_t val = 0; \ auto [ptr, ec] = \ std::from_chars(v.data(), v.data() + v.size(), val); \ if (ec != std::errc()) \ return false; \ FLAGS_##name##_storage_() = val; \ return true; \ }, \ []() { return std::to_string(FLAGS_##name##_storage_()); }, \ []() { return; }, \ ::rex::cvar::Lifecycle::kHotReload, \ {}, \ std::to_string(default_val), \ false}) #define REXCVAR_DEFINE_DOUBLE(name, default_val, category, desc) \ double& FLAGS_##name##_storage_() { \ static double storage = (default_val); \ return storage; \ } \ static auto _cvar_reg_##name = \ ::rex::cvar::FlagRegistrar({#name, \ ::rex::cvar::FlagType::Double, \ category, \ desc, \ [](std::string_view v) { \ double val = 0; \ if (!::rex::cvar::ParseDouble(v, val)) \ return false; \ FLAGS_##name##_storage_() = val; \ return true; \ }, \ []() { return std::to_string(FLAGS_##name##_storage_()); }, \ []() { return; }, \ ::rex::cvar::Lifecycle::kHotReload, \ {}, \ std::to_string(default_val), \ false}) #define REXCVAR_DEFINE_STRING(name, default_val, category, desc) \ std::string& FLAGS_##name##_storage_() { \ static std::string storage = (default_val); \ return storage; \ } \ static auto _cvar_reg_##name = \ ::rex::cvar::FlagRegistrar({#name, \ ::rex::cvar::FlagType::String, \ category, \ desc, \ [](std::string_view v) { \ FLAGS_##name##_storage_() = std::string(v); \ return true; \ }, \ []() { return FLAGS_##name##_storage_(); }, \ []() { return; }, \ ::rex::cvar::Lifecycle::kHotReload, \ {}, \ default_val, \ false}) #define REXCVAR_DEFINE_COMMAND(name, callback, category, desc) \ std::function& FLAGS_##name##_storage_() { \ static std::function storage = (callback); \ return storage; \ } \ static auto _cvar_reg_##name = \ ::rex::cvar::FlagRegistrar({#name, \ ::rex::cvar::FlagType::Command, \ category, \ desc, \ [](std::string_view) { return false; }, \ []() { return ""; }, \ callback, \ ::rex::cvar::Lifecycle::kHotReload, \ {}, \ "", \ false}) namespace rex::cvar { namespace testing { class ScopedLifecycleOverride { public: ScopedLifecycleOverride(); ~ScopedLifecycleOverride(); ScopedLifecycleOverride(const ScopedLifecycleOverride&) = delete; ScopedLifecycleOverride& operator=(const ScopedLifecycleOverride&) = delete; }; void ResetAllForTesting(); } // namespace testing } // namespace rex::cvar