diff --git a/lsfg-vk-common/include/lsfg-vk-common/configuration/config.hpp b/lsfg-vk-common/include/lsfg-vk-common/configuration/config.hpp index 93fe37b..0916db9 100644 --- a/lsfg-vk-common/include/lsfg-vk-common/configuration/config.hpp +++ b/lsfg-vk-common/include/lsfg-vk-common/configuration/config.hpp @@ -40,36 +40,69 @@ namespace ls { Pacing pacing{Pacing::None}; }; - /// automatically updating configuration - class Configuration { + /// parsed configuration file + class ConfigFile { public: - /// create a new configuration + /// create a default configuration file at the given path + /// @param path path to configuration file /// @throws ls::error on failure - Configuration(); + static void createDefaultConfigFile(const std::filesystem::path& path); - /// check if the configuration is out of date + /// load the default configuration /// @throws ls::error on failure - /// @return true if the configuration is out of date - bool isUpToDate(); - - /// reload the configuration from disk + ConfigFile(); + /// load configuration from file + /// @param path path to configuration file /// @throws ls::error on failure - void reload(); + ConfigFile(const std::filesystem::path& path); /// get the global configuration /// @return global configuration - [[nodiscard]] const GlobalConf& getGlobalConf() const { return global; } - + [[nodiscard]] auto& global() { return this->globalConf; } /// get the game profiles /// @return list of game profiles - [[nodiscard]] const std::vector& getProfiles() const { return profiles; } - private: - std::filesystem::path path; - std::chrono::time_point timestamp; - bool from_env{}; + [[nodiscard]] auto& profiles() { return this->profileConfs; } - GlobalConf global; - std::vector profiles; + /// get the global configuration + /// @return global configuration + [[nodiscard]] const auto& global() const { return this->globalConf; } + /// get the game profiles + /// @return list of game profiles + [[nodiscard]] const auto& profiles() const { return this->profileConfs; } + + /// write the configuration back to file + /// @param path path to configuration file + /// @throws ls::error on failure + void write(const std::filesystem::path& path) const; + private: + GlobalConf globalConf{}; + std::vector profileConfs; }; + /// configuration watcher with additional environment support + class WatchedConfig { + public: + /// create a new configuration watcher + /// @throws ls::error on failure + WatchedConfig(); + + /// reload the configuration from disk if it has changed + /// @throws ls::error on failure + /// @return true if the configuration was reloaded + bool update(); + + /// access the underlying configuration file + /// @return configuration file + [[nodiscard]] const auto& get() const { return this->configFile; } + private: + ConfigFile configFile; + + std::filesystem::path path; + std::chrono::time_point last_timestamp; + }; + + /// find the configuration file in the most common locations + /// @return path to configuration file + std::filesystem::path findConfigurationFile(); + } diff --git a/lsfg-vk-common/include/lsfg-vk-common/configuration/detection.hpp b/lsfg-vk-common/include/lsfg-vk-common/configuration/detection.hpp index 48045ed..4cc4979 100644 --- a/lsfg-vk-common/include/lsfg-vk-common/configuration/detection.hpp +++ b/lsfg-vk-common/include/lsfg-vk-common/configuration/detection.hpp @@ -36,5 +36,5 @@ namespace ls { /// @param id identification data /// @return ident pair if found std::optional> findProfile( - const Configuration& config, const Identification& id); + const ConfigFile& config, const Identification& id); } diff --git a/lsfg-vk-common/src/configuration/config.cpp b/lsfg-vk-common/src/configuration/config.cpp index cec9a06..a3c44e8 100644 --- a/lsfg-vk-common/src/configuration/config.cpp +++ b/lsfg-vk-common/src/configuration/config.cpp @@ -1,7 +1,9 @@ #include "lsfg-vk-common/configuration/config.hpp" #include "lsfg-vk-common/helpers/errors.hpp" +#include #include +#include #include #include #include @@ -9,37 +11,76 @@ #include #include -#define TOML_ENABLE_FORMATTERS 0 #include using namespace ls; +void ConfigFile::createDefaultConfigFile(const std::filesystem::path& path) { + try { + std::filesystem::create_directories(path.parent_path()); + if (!std::filesystem::exists(path.parent_path())) + throw ls::error("unable to create configuration directory"); + + std::ofstream ofs(path); + if (!ofs.is_open()) + throw ls::error("unable to create default configuration file"); + + ofs << R"(version = 2 + + [global] + # dll = '/media/games/Lossless Scaling/Lossless.dll' # if you don't have LS in the default location + allow_fp16 = true # this will improve give a MASSIVE performance boost on AMD, but be super slow on older (!) NVIDIA GPUs + + [[profile]] + name = "4x FG / 85% [Performance]" + active_in = [ # see the wiki for more info + 'vkcube', + 'vkcubepp' + ] + # gpu = 'NVIDIA GeForce RTX 5080' # see the wiki for more info + multiplier = 4 + flow_scale = 0.85 + performance_mode = true + pacing = 'none' # see the wiki for more info + + [[profile]] + name = "2x FG / 100%" + active_in = 'GenshinImpact.exe' + gpu = 'NVIDIA GeForce RTX 5080' + multiplier = 2 + )"; + ofs.close(); + } catch (const std::filesystem::filesystem_error& e) { + throw ls::error("unable to create default configuration file", e); + } +} + +ConfigFile::ConfigFile() { + this->globalConf = { + .allow_fp16 = true + }; + this->profileConfs.emplace_back(GameConf { + .name = "4x FG / 85% [Performance]", + .active_in = { + "vkcube", + "vkcubepp" + }, + .multiplier = 4, + .flow_scale = 0.85F, + .performance_mode = true, + .pacing = Pacing::None + }); + this->profileConfs.emplace_back(GameConf { + .name = "2x FG / 100%", + .active_in = { + "GenshinImpact.exe" + }, + .gpu = "NVIDIA GeForce RTX 5080", + .multiplier = 2 + }); +} + namespace { - constexpr char const* DEFAULT_CONFIG = R"(version = 2 - -[global] -# dll = '/media/games/Lossless Scaling/Lossless.dll' # if you don't have LS in the default location -allow_fp16 = true # this will improve give a MASSIVE performance boost on AMD, but be super slow on older (!) NVIDIA GPUs - -[[profile]] -name = "4x FG / 85% [Performance]" -active_in = [ # see the wiki for more info - 'vkcube', - 'vkcubepp' -] -# gpu = 'NVIDIA GeForce RTX 5080' # see the wiki for more info -multiplier = 4 -flow_scale = 0.85 -performance_mode = true -pacing = 'none' # see the wiki for more info - -[[profile]] -name = "2x FG / 100%" -active_in = 'GenshinImpact.exe' -gpu = 'NVIDIA GeForce RTX 5080' -multiplier = 2 -)"; - /// parse an activity array from toml value std::vector activityFromString(const toml::node_view& val) { std::vector active_in{}; @@ -63,28 +104,6 @@ multiplier = 2 return Pacing::None; throw ls::error("unknown pacing method: " + str); } - /// try to find the config - std::filesystem::path findPath() { - // always honor LSFGVK_CONFIG if set - const char* envPath = std::getenv("LSFGVK_CONFIG"); - if (envPath && *envPath != '\0') - return{envPath}; - - // then check the XDG overriden location - const char* xdgPath = std::getenv("XDG_CONFIG_HOME"); - if (xdgPath && *xdgPath != '\0') - return std::filesystem::path(xdgPath) - / "lsfg-vk" / "conf.toml"; - - // fallback to typical user home - const char* homePath = std::getenv("HOME"); - if (homePath && *homePath != '\0') - return std::filesystem::path(homePath) - / ".config" / "lsfg-vk" / "conf.toml"; - - // finally, use system-wide config - return "/etc/lsfg-vk/conf.toml"; - } /// parse the global configuration GlobalConf parseGlobalConf(const toml::table& tbl) { const GlobalConf conf{ @@ -168,77 +187,131 @@ multiplier = 2 } } -Configuration::Configuration() : - path(findPath()), - from_env(std::getenv("LSFGVK_ENV") != nullptr) { - if (this->from_env) { - this->global = parseGlobalConfFromEnv(); - this->profiles.push_back(parseGameConfFromEnv()); - return; - } - - if (std::filesystem::exists(this->path)) - return; - +ConfigFile::ConfigFile(const std::filesystem::path& path) { + toml::table table; try { - std::filesystem::create_directories(this->path.parent_path()); - if (!std::filesystem::exists(this->path.parent_path())) - throw ls::error("unable to create configuration directory"); - - std::ofstream ofs(this->path); - if (!ofs.is_open()) - throw ls::error("unable to create default configuration file"); - - ofs << DEFAULT_CONFIG; - ofs.close(); - } catch (const std::filesystem::filesystem_error& e) { - throw ls::error("unable to create default configuration file", e); - } -} - -bool Configuration::isUpToDate() { - if (this->from_env) - return true; - - try { - return std::filesystem::last_write_time(this->path) == this->timestamp; - } catch (const std::filesystem::filesystem_error& e) { - throw ls::error("unable to access configuration file", e); - } -} - -void Configuration::reload() { - try { - this->timestamp = std::filesystem::last_write_time(this->path); - } catch (const std::filesystem::filesystem_error& e) { - throw ls::error("unable to access configuration file", e); - } - - GlobalConf global{}; - std::vector profiles{}; - - toml::table tbl; - try { - tbl = toml::parse_file(this->path.string()); + table = toml::parse_file(path.string()); } catch (const toml::parse_error& e) { throw ls::error("unable to parse configuration", e); } - auto vrs = tbl["version"]; - if (!vrs || !vrs.is_integer() || *vrs.as_integer() != 2) + auto version = table["version"]; + if (!version || !version.is_integer() || *version.as_integer() != 2) throw ls::error("unsupported configuration version"); - auto gbl = tbl["global"]; - if (gbl && gbl.is_table()) { - global = parseGlobalConf(*gbl.as_table()); + auto global = table["global"]; + if (global && global.is_table()) { + this->globalConf = parseGlobalConf(*global.as_table()); } - auto pfls = tbl["profile"]; - if (pfls && pfls.is_array_of_tables()) { - for (const auto& pfl : *pfls.as_array()) - profiles.push_back(parseGameConf(*pfl.as_table())); - } - - this->global = std::move(global); - this->profiles = std::move(profiles); + auto profiles = table["profile"]; + if (profiles && profiles.is_array_of_tables()) + for (const auto& profile : *profiles.as_array()) + this->profileConfs.push_back(parseGameConf(*profile.as_table())); +} + +void ConfigFile::write(const std::filesystem::path& path) const { + toml::table table; + table.insert("version", 2); + + toml::table global; + if (this->globalConf.dll) + global.insert("dll", *this->globalConf.dll); + global.insert("allow_fp16", this->globalConf.allow_fp16); + table.insert("global", global); + + toml::array profiles; + for (const auto& conf : this->profileConfs) { + toml::table profile; + profile.insert("name", conf.name); + + if (!conf.active_in.empty()) { + if (conf.active_in.size() == 1) { + profile.insert("active_in", conf.active_in.front()); + } else { + toml::array active_in; + for (const auto& entry : conf.active_in) + active_in.push_back(entry); + profile.insert("active_in", active_in); + } + } + if (conf.gpu) + profile.insert("gpu", conf.gpu.value_or("")); + profile.insert("multiplier", static_cast(conf.multiplier)); + profile.insert("flow_scale", conf.flow_scale); + profile.insert("performance_mode", conf.performance_mode); + switch (conf.pacing) { + case Pacing::None: + profile.insert("pacing", "none"); + break; + } + + profiles.push_back(profile); + } + table.insert("profile", profiles); + + try { + std::ofstream ofs(path); + if (!ofs.is_open()) + throw ls::error("unable to open configuration file for writing"); + + ofs << toml::toml_formatter { + table, + toml::v3::format_flags::relaxed_float_precision + } << '\n'; + ofs.close(); + } catch (const std::exception& e) { + throw ls::error("unable to write configuration file", e); + } +} + +WatchedConfig::WatchedConfig() : path(findConfigurationFile()) { + if (std::getenv("LSFGVK_ENV")) { + auto& config = this->configFile; + config.global() = parseGlobalConfFromEnv(); + config.profiles().push_back(parseGameConfFromEnv()); + + return; + } + + if (!std::filesystem::exists(this->path)) + ConfigFile::createDefaultConfigFile(this->path); + + this->configFile = ConfigFile(this->path); +} + +bool WatchedConfig::update() { + if (std::getenv("LSFGVK_ENV")) + return false; + + const auto now = std::filesystem::last_write_time(this->path); + if (now == this->last_timestamp) + return false; + + ConfigFile new_config{this->path}; + this->last_timestamp = now; + this->configFile = std::move(new_config); + return true; +} + +std::filesystem::path ls::findConfigurationFile() { + // always honor LSFGVK_CONFIG if set + const char* envPath = std::getenv("LSFGVK_CONFIG"); + if (envPath && *envPath != '\0') + return{envPath}; + + // then check the XDG overriden location + const char* xdgPath = std::getenv("XDG_CONFIG_HOME"); + if (xdgPath && *xdgPath != '\0') + return std::filesystem::path(xdgPath) + / "lsfg-vk" / "conf.toml"; + + // fallback to typical user home + const char* homePath = std::getenv("HOME"); + if (homePath && *homePath != '\0') + return std::filesystem::path(homePath) + / ".config" / "lsfg-vk" / "conf.toml"; + + // finally, use system-wide config + return "/etc/lsfg-vk/conf.toml"; } diff --git a/lsfg-vk-common/src/configuration/detection.cpp b/lsfg-vk-common/src/configuration/detection.cpp index 35cf2d5..bf55bcd 100644 --- a/lsfg-vk-common/src/configuration/detection.cpp +++ b/lsfg-vk-common/src/configuration/detection.cpp @@ -83,12 +83,12 @@ Identification ls::identify() { } std::optional> ls::findProfile( - const Configuration& config, const Identification& id) { - const auto& profiles = config.getProfiles(); + const ConfigFile& config, const Identification& id) { + const auto& profiles = config.profiles(); // check for the environment option first if (std::getenv("LSFGVK_ENV") != nullptr) - return std::make_pair(IdentType::OVERRIDE, config.getProfiles().front()); + return std::make_pair(IdentType::OVERRIDE, profiles.front()); // then override first if (id.override.has_value()) { diff --git a/lsfg-vk-layer/src/instance.cpp b/lsfg-vk-layer/src/instance.cpp index ad2d987..3a3c5cb 100644 --- a/lsfg-vk-layer/src/instance.cpp +++ b/lsfg-vk-layer/src/instance.cpp @@ -84,8 +84,7 @@ namespace { Root::Root() { // find active profile - this->config.reload(); - const auto& profile = findProfile(this->config, ls::identify()); + const auto& profile = findProfile(this->config.get(), ls::identify()); if (!profile.has_value()) return; @@ -109,8 +108,7 @@ Root::Root() { } void Root::update() { - if (!this->config.isUpToDate()) - this->config.reload(); + this->config.update(); } void Root::modifyInstanceCreateInfo(VkInstanceCreateInfo& createInfo, @@ -202,7 +200,7 @@ void Root::createSwapchainContext(const vk::Vulkan& vk, const auto& profile = *this->active_profile; if (!this->backend.has_value()) { // emplace backend late, due to loader bug - const auto& global = this->config.getGlobalConf(); + const auto& global = this->config.get().global(); setenv("DISABLE_LSFGVK", "1", 1); // NOLINT (c++-include) diff --git a/lsfg-vk-layer/src/instance.hpp b/lsfg-vk-layer/src/instance.hpp index ad40272..0c2ed00 100644 --- a/lsfg-vk-layer/src/instance.hpp +++ b/lsfg-vk-layer/src/instance.hpp @@ -67,7 +67,7 @@ namespace lsfgvk::layer { /// @param swapchain swapchain handle void removeSwapchainContext(VkSwapchainKHR swapchain); private: - ls::Configuration config; + ls::WatchedConfig config; std::optional active_profile; ls::lazy backend; diff --git a/lsfg-vk-ui/CMakeLists.txt b/lsfg-vk-ui/CMakeLists.txt index 1e22b62..3b7b8bf 100644 --- a/lsfg-vk-ui/CMakeLists.txt +++ b/lsfg-vk-ui/CMakeLists.txt @@ -27,7 +27,8 @@ set_target_properties(lsfg-vk-ui PROPERTIES target_compile_options(lsfg-vk-ui PRIVATE -Wno-ctad-maybe-unsupported -Wno-unsafe-buffer-usage-in-libc-call - -Wno-global-constructors) + -Wno-global-constructors + -Wno-unsafe-buffer-usage) target_link_libraries(lsfg-vk-ui PUBLIC lsfg-vk-common diff --git a/lsfg-vk-ui/src/backend.cpp b/lsfg-vk-ui/src/backend.cpp index 5a3ed83..2f11337 100644 --- a/lsfg-vk-ui/src/backend.cpp +++ b/lsfg-vk-ui/src/backend.cpp @@ -5,6 +5,8 @@ #include #include #include +#include +#include #include #include @@ -12,11 +14,20 @@ using namespace lsfgvk::ui; Backend::Backend() { // load configuration - ls::Configuration config; - config.reload(); + ls::ConfigFile config{}; - this->m_global = config.getGlobalConf(); - this->m_profiles = config.getProfiles(); + auto path = ls::findConfigurationFile(); + if (std::filesystem::exists(path)) { + try { + config = ls::ConfigFile(path); + } catch (const std::exception&) { + std::cerr << "the configuration file is invalid, it has been backed up to '.old'\n"; + std::filesystem::rename(path, path.string() + ".old"); + } + } + + this->m_global = config.global(); + this->m_profiles = config.profiles(); // create profile list model QStringList profiles; // NOLINT (IWYU) @@ -30,14 +41,22 @@ Backend::Backend() { this->m_profile_index = 0; // spawn saving thread - std::thread([this]() { + std::thread([this, path]() { while (true) { std::this_thread::sleep_for(std::chrono::milliseconds(500)); if (!this->m_dirty.exchange(false)) continue; - std::cerr << "configuration updated >:3" << '\n'; + ls::ConfigFile config{}; + config.global() = this->m_global; + config.profiles() = this->m_profiles; + + try { + config.write(path); + } catch (const std::exception& e) { + std::cerr << "unable to write configuration:\n- " << e.what() << "\n"; + } } }).detach(); } diff --git a/ui/src/config.rs b/ui/src/config.rs deleted file mode 100644 index 7710eb6..0000000 --- a/ui/src/config.rs +++ /dev/null @@ -1,172 +0,0 @@ -use std::sync::{Arc, OnceLock, RwLock}; - -use anyhow::Context; - -pub mod structs; -pub use structs::*; - -/// Find the configuration file path based on environment variables -fn find_config_file() -> String { - if let Some(path) = std::env::var("LSFG_CONFIG").ok() { - return path; - } - - if let Some(xdg) = std::env::var("XDG_CONFIG_HOME").ok() { - return format!("{}/lsfg-vk/conf.toml", xdg); - } - - if let Some(home) = std::env::var("HOME").ok() { - return format!("{}/.config/lsfg-vk/conf.toml", home); - } - - "conf.toml".to_string() -} - -static CONFIG: OnceLock>> = OnceLock::new(); -static CONFIG_WRITER: OnceLock> = OnceLock::new(); - -pub fn default_config() -> TomlConfig { - TomlConfig { - version: 1, - global: TomlGlobal { - dll: None, - no_fp16: false - }, - game: vec![ - TomlGame { - exe: String::from("vkcube"), - multiplier: Multiplier::from(4), - flow_scale: FlowScale::from(0.7), - performance_mode: true, - hdr_mode: false, - experimental_present_mode: PresentMode::Vsync, - }, - TomlGame { - exe: String::from("benchmark"), - multiplier: Multiplier::from(4), - flow_scale: FlowScale::from(1.0), - performance_mode: true, - hdr_mode: false, - experimental_present_mode: PresentMode::Vsync, - }, - TomlGame { - exe: String::from("GenshinImpact.exe"), - multiplier: Multiplier::from(3), - flow_scale: FlowScale::from(1.0), - performance_mode: false, - hdr_mode: false, - experimental_present_mode: PresentMode::Vsync, - }, - ] - } -} - -/// -/// Load the configuration from the file and create a writer. -/// -pub fn load_config() -> Result<(), anyhow::Error> { - // load the configuration file - let path = find_config_file(); - if !std::path::Path::new(&path).exists() { - let conf = default_config(); - save_config(&conf) - .context("Failed to create default configuration")?; - } - let data = std::fs::read(path) - .context("Failed to read conf.toml")?; - let mut config: TomlConfig = toml::from_slice(&data) - .context("Failed to parse conf.toml")?; - - // remove duplicate entries - config.game.sort_by_key(|e| e.exe.clone()); - config.game.dedup_by_key(|e| e.exe.clone()); - config.game.retain(|e| !e.exe.is_empty()); - - // create the configuration writer thread - let (tx, rx) = std::sync::mpsc::channel::<()>(); - CONFIG.set(Arc::new(RwLock::new(config))) - .ok().context("Failed to set configuration state")?; - CONFIG_WRITER.set(tx) - .ok().context("Failed to set configuration writer")?; - - std::thread::spawn(move || { - let config = CONFIG.get().unwrap(); - loop { - // wait for a signal to write the configuration - if let Err(_) = rx.recv() { - break; - } - - // wait a bit to avoid excessive writes - std::thread::sleep(std::time::Duration::from_millis(200)); - - // empty the channel - while rx.try_recv().is_ok() {} - - // write the configuration - if let Ok(config) = config.try_read() { - if let Err(e) = save_config(&config) { - eprintln!("Failed to save configuration: {}", e); - } - } else { - eprintln!("Failed to read configuration state"); - } - } - }); - Ok(()) -} - -/// -/// Get a snapshot of the current configuration -/// -pub fn get_config() -> Result { - let conf = CONFIG.get() - .expect("Configuration not loaded") - .try_read() - .map(|config| config.clone()); - if let Ok(config) = conf { - return Ok(config) - } - - anyhow::bail!("Failed to read configuration state") -} - -/// -/// Safely edit the configuration. -/// -pub fn edit_config(f: F) -> Result<(), anyhow::Error> -where - F: FnOnce(&mut TomlConfig) -{ - let mut config = CONFIG.get() - .expect("Configuration not loaded") - .write() - .map_err(|_| anyhow::anyhow!("Failed to acquire write lock on configuration"))?; - - f(&mut config); - - CONFIG_WRITER.get().unwrap().send(()) - .context("Failed to send configuration update signal") -} - -/// -/// Save the configuration to the file -/// -/// # Arguments -/// -/// `config` - The configuration to save -/// -pub fn save_config(config: &TomlConfig) -> Result<(), anyhow::Error> { - let path = find_config_file(); - - let parent = std::path::Path::new(&path).parent() - .context("Failed to get parent directory of config path")?; - std::fs::create_dir_all(parent) - .context("Failed to create config directory")?; - - let data = toml::to_string(config) - .context("Failed to serialize conf.toml")?; - std::fs::write(path, data) - .context("Failed to write conf.toml")?; - Ok(()) -}