refactor(cleanup): write configuration serializer

This commit is contained in:
PancakeTAS 2025-12-23 08:19:13 +01:00
parent 70a1bf3092
commit 781cde93bd
No known key found for this signature in database
9 changed files with 272 additions and 320 deletions

View file

@ -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<GameConf>& getProfiles() const { return profiles; }
private:
std::filesystem::path path;
std::chrono::time_point<std::chrono::file_clock> timestamp;
bool from_env{};
[[nodiscard]] auto& profiles() { return this->profileConfs; }
GlobalConf global;
std::vector<GameConf> 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<GameConf> 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<std::chrono::file_clock> last_timestamp;
};
/// find the configuration file in the most common locations
/// @return path to configuration file
std::filesystem::path findConfigurationFile();
}

View file

@ -36,5 +36,5 @@ namespace ls {
/// @param id identification data
/// @return ident pair if found
std::optional<std::pair<IdentType, GameConf>> findProfile(
const Configuration& config, const Identification& id);
const ConfigFile& config, const Identification& id);
}

View file

@ -1,7 +1,9 @@
#include "lsfg-vk-common/configuration/config.hpp"
#include "lsfg-vk-common/helpers/errors.hpp"
#include <cstdint>
#include <cstdlib>
#include <exception>
#include <filesystem>
#include <fstream>
#include <optional>
@ -9,37 +11,76 @@
#include <utility>
#include <vector>
#define TOML_ENABLE_FORMATTERS 0
#include <toml.hpp>
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<std::string> activityFromString(const toml::node_view<const toml::node>& val) {
std::vector<std::string> 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<GameConf> 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<int64_t>(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";
}

View file

@ -83,12 +83,12 @@ Identification ls::identify() {
}
std::optional<std::pair<IdentType, GameConf>> 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()) {

View file

@ -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)

View file

@ -67,7 +67,7 @@ namespace lsfgvk::layer {
/// @param swapchain swapchain handle
void removeSwapchainContext(VkSwapchainKHR swapchain);
private:
ls::Configuration config;
ls::WatchedConfig config;
std::optional<ls::GameConf> active_profile;
ls::lazy<lsfgvk::backend::Instance> backend;

View file

@ -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

View file

@ -5,6 +5,8 @@
#include <QStringList>
#include <QString>
#include <chrono>
#include <exception>
#include <filesystem>
#include <iostream>
#include <thread>
@ -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();
}

View file

@ -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<Arc<RwLock<TomlConfig>>> = OnceLock::new();
static CONFIG_WRITER: OnceLock<std::sync::mpsc::Sender<()>> = 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<TomlConfig, anyhow::Error> {
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: 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(())
}