xenia-canary/src/xenia/config.cc

269 lines
9.1 KiB
C++
Raw Normal View History

/**
******************************************************************************
* Xenia : Xbox 360 Emulator Research Project *
******************************************************************************
* Copyright 2020 Ben Vanik. All rights reserved. *
* Released under the BSD license - see LICENSE in the root for more details. *
******************************************************************************
*/
#include "config.h"
#include "third_party/fmt/include/fmt/format.h"
2020-12-31 13:04:50 +00:00
#include "xenia/base/assert.h"
#include "xenia/base/cvar.h"
#include "xenia/base/filesystem.h"
#include "xenia/base/logging.h"
#include "xenia/base/string.h"
#include "xenia/base/string_buffer.h"
std::shared_ptr<cpptoml::table> ParseFile(
const std::filesystem::path& filename) {
std::ifstream file(filename);
if (!file.is_open()) {
throw cpptoml::parse_exception(xe::path_to_utf8(filename) +
2019-08-04 17:20:51 +00:00
" could not be opened for parsing");
}
// since cpptoml can't parse files with a UTF-8 BOM we need to skip them
char bom[3]{};
file.read(bom, sizeof(bom));
if (file.fail() || bom[0] != '\xEF' || bom[1] != '\xBB' || bom[2] != '\xBF') {
file.clear();
file.seekg(0);
}
if (xe::utf8::ends_with(filename.string(), ".config.toml")) {
std::string config_content{};
std::string config_line{};
// dump contents of loaded config file
// content is expected to be utf8
while (std::getline(file, config_line)) {
config_content.append(fmt::format("{}\n", config_line));
}
XELOGI("Loading config: {}\n{}", xe::path_to_utf8(filename), config_content);
}
2023-07-29 16:04:36 +00:00
file.clear();
file.seekg(0);
2019-08-04 17:20:51 +00:00
cpptoml::parser p(file);
return p.parse();
}
CmdVar(config, "", "Specifies the target config to load.");
2020-12-31 13:04:50 +00:00
DEFINE_uint32(
defaults_date, 0,
"Do not modify - internal version of the default values in the config, for "
"seamless updates if default value of any option is changed.",
"Config");
namespace config {
2019-10-06 13:40:56 +00:00
std::string config_name = "xenia-canary.config.toml";
std::filesystem::path config_folder;
std::filesystem::path config_path;
std::string game_config_suffix = ".config.toml";
2019-10-06 13:40:56 +00:00
bool sortCvar(cvar::IConfigVar* a, cvar::IConfigVar* b) {
if (a->category() < b->category()) return true;
if (a->category() > b->category()) return false;
if (a->name() < b->name()) return true;
return false;
}
std::shared_ptr<cpptoml::table> ParseConfig(
const std::filesystem::path& config_path) {
try {
2019-08-04 17:20:51 +00:00
return ParseFile(config_path);
} catch (cpptoml::parse_exception e) {
xe::FatalError(fmt::format("Failed to parse config file '{}':\n\n{}",
xe::path_to_utf8(config_path), e.what()));
return nullptr;
}
}
2020-12-31 13:04:50 +00:00
void ReadConfig(const std::filesystem::path& file_path,
bool update_if_no_version_stored) {
if (!cvar::ConfigVars) {
return;
}
const auto config = ParseConfig(file_path);
2020-12-31 13:04:50 +00:00
// Loading an actual global config file that exists - if there's no
// defaults_date in it, it's very old (before updating was added at all, thus
// all defaults need to be updated).
auto defaults_date_cvar =
dynamic_cast<cvar::ConfigVar<uint32_t>*>(cv::cv_defaults_date);
assert_not_null(defaults_date_cvar);
defaults_date_cvar->SetConfigValue(0);
for (auto& it : *cvar::ConfigVars) {
auto config_var = static_cast<cvar::IConfigVar*>(it.second);
auto config_key = config_var->category() + "." + config_var->name();
if (config->contains_qualified(config_key)) {
config_var->LoadConfigValue(config->get_qualified(config_key));
}
}
2020-12-31 13:04:50 +00:00
uint32_t config_defaults_date = defaults_date_cvar->GetTypedConfigValue();
if (update_if_no_version_stored || config_defaults_date) {
cvar::IConfigVarUpdate::ApplyUpdates(config_defaults_date);
}
2020-02-28 20:30:48 +00:00
XELOGI("Loaded config: {}", xe::path_to_utf8(file_path));
}
void ReadGameConfig(const std::filesystem::path& file_path) {
2020-12-31 13:04:50 +00:00
if (!cvar::ConfigVars) {
return;
}
const auto config = ParseConfig(file_path);
for (auto& it : *cvar::ConfigVars) {
auto config_var = static_cast<cvar::IConfigVar*>(it.second);
auto config_key = config_var->category() + "." + config_var->name();
if (config->contains_qualified(config_key)) {
config_var->LoadGameConfigValue(config->get_qualified(config_key));
}
}
2020-02-28 20:30:48 +00:00
XELOGI("Loaded game config: {}", xe::path_to_utf8(file_path));
}
void SaveConfig() {
[UI] Image post-processing and full presentation/window rework [GPU] Add FXAA post-processing [UI] Add FidelityFX FSR and CAS post-processing [UI] Add blue noise dithering from 10bpc to 8bpc [GPU] Apply the DC PWL gamma ramp closer to the spec, supporting fully white color [UI] Allow the GPU CP thread to present on the host directly, bypassing the UI thread OS paint event [UI] Allow variable refresh rate (or tearing) [UI] Present the newest frame (restart) on DXGI [UI] Replace GraphicsContext with a far more advanced Presenter with more coherent surface connection and UI overlay state management [UI] Connect presentation to windows via the Surface class, not native window handles [Vulkan] Switch to simpler Vulkan setup with no instance/device separation due to interdependencies and to pass fewer objects around [Vulkan] Lower the minimum required Vulkan version to 1.0 [UI/GPU] Various cleanup, mainly ComPtr usage [UI] Support per-monitor DPI awareness v2 on Windows [UI] DPI-scale Dear ImGui [UI] Replace the remaining non-detachable window delegates with unified window event and input listeners [UI] Allow listeners to safely destroy or close the window, and to register/unregister listeners without use-after-free and the ABA problem [UI] Explicit Z ordering of input listeners and UI overlays, top-down for input, bottom-up for drawing [UI] Add explicit window lifecycle phases [UI] Replace Window virtual functions with explicit desired state, its application, actual state, its feedback [UI] GTK: Apply the initial size to the drawing area [UI] Limit internal UI frame rate to that of the monitor [UI] Hide the cursor using a timer instead of polling due to no repeated UI thread paints with GPU CP thread presentation, and only within the window
2022-01-29 10:22:03 +00:00
if (config_path.empty()) {
return;
}
2020-12-31 13:04:50 +00:00
// All cvar defaults have been updated on loading - store the current date.
auto defaults_date_cvar =
dynamic_cast<cvar::ConfigVar<uint32_t>*>(cv::cv_defaults_date);
assert_not_null(defaults_date_cvar);
defaults_date_cvar->SetConfigValue(
cvar::IConfigVarUpdate::GetLastUpdateDate());
std::vector<cvar::IConfigVar*> vars;
2020-12-31 13:04:50 +00:00
if (cvar::ConfigVars) {
for (const auto& s : *cvar::ConfigVars) {
vars.push_back(s.second);
}
}
std::sort(vars.begin(), vars.end(), [](auto a, auto b) {
if (a->category() < b->category()) return true;
if (a->category() > b->category()) return false;
if (a->name() < b->name()) return true;
return false;
});
// we use our own write logic because cpptoml doesn't
// allow us to specify comments :(
std::string last_category;
bool last_multiline_description = false;
xe::StringBuffer sb;
for (auto config_var : vars) {
if (config_var->is_transient()) {
continue;
}
if (last_category != config_var->category()) {
if (!last_category.empty()) {
2020-04-13 17:23:04 +00:00
sb.Append('\n', 2);
}
last_category = config_var->category();
last_multiline_description = false;
sb.AppendFormat("[{}]\n", last_category);
} else if (last_multiline_description) {
last_multiline_description = false;
sb.Append('\n');
}
auto value = config_var->config_value();
size_t line_count;
if (xe::utf8::find_any_of(value, "\n") == std::string_view::npos) {
auto line = fmt::format("{} = {}", config_var->name(),
config_var->config_value());
sb.Append(line);
line_count = xe::utf8::count(line);
} else {
auto lines = xe::utf8::split(value, "\n");
auto first = lines.cbegin();
sb.AppendFormat("{} = {}\n", config_var->name(), *first);
auto last = std::prev(lines.cend());
for (auto it = std::next(first); it != last; ++it) {
sb.Append(*it);
sb.Append('\n');
}
sb.Append(*last);
line_count = xe::utf8::count(*last);
}
const size_t value_alignment = 50;
const auto& description = config_var->description();
if (!description.empty()) {
if (line_count < value_alignment) {
sb.Append(' ', value_alignment - line_count);
}
if (xe::utf8::find_any_of(description, "\n") == std::string_view::npos) {
sb.AppendFormat("\t# {}\n", config_var->description());
} else {
auto lines = xe::utf8::split(description, "\n");
auto first = lines.cbegin();
sb.Append("\t# ");
sb.Append(*first);
sb.Append('\n');
for (auto it = std::next(first); it != lines.cend(); ++it) {
sb.Append(' ', value_alignment);
sb.Append("\t# ");
sb.Append(*it);
sb.Append('\n');
}
last_multiline_description = true;
}
}
}
// save the config file
xe::filesystem::CreateParentFolder(config_path);
auto handle = xe::filesystem::OpenFile(config_path, "wb");
if (!handle) {
XELOGE("Failed to open '{}' for writing.", xe::path_to_utf8(config_path));
} else {
fwrite(sb.buffer(), 1, sb.length(), handle);
fclose(handle);
}
}
void SetupConfig(const std::filesystem::path& config_folder) {
config::config_folder = config_folder;
// check if the user specified a specific config to load
if (!cvars::config.empty()) {
config_path = xe::to_path(cvars::config);
if (std::filesystem::exists(config_path)) {
2020-12-31 13:04:50 +00:00
// An external config file may contain only explicit overrides - in this
// case, it will likely not contain the defaults version; don't update
// from the version 0 in this case. Or, it may be a full config - in this
// case, if it's recent enough (created at least in 2021), it will contain
// the version number - updates the defaults in it.
ReadConfig(config_path, false);
return;
}
}
// if the user specified a --config argument, but the file doesn't exist,
// let's also load the default config
if (!config_folder.empty()) {
config_path = config_folder / config_name;
if (std::filesystem::exists(config_path)) {
2020-12-31 13:04:50 +00:00
ReadConfig(config_path, true);
}
2020-12-31 13:04:50 +00:00
// Re-save the loaded config to present the most up-to-date list of
// parameters to the user, if new options were added, descriptions were
// updated, or default values were changed.
SaveConfig();
}
}
void LoadGameConfig(const std::string_view title_id) {
const auto game_config_folder = config_folder / "config";
const auto game_config_path =
game_config_folder / (std::string(title_id) + game_config_suffix);
if (std::filesystem::exists(game_config_path)) {
ReadGameConfig(game_config_path);
}
}
} // namespace config