-
Notifications
You must be signed in to change notification settings - Fork 213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add config file support #1344
base: master
Are you sure you want to change the base?
Add config file support #1344
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
// Configuration files are installed in /etc/gamescope.d/*.conf | ||
{ | ||
"display_configuration": { | ||
"VLV": { | ||
"0x3003" : { | ||
"refresh_rates": [ 45, 47, 48, 49, 50, 51, 53, 55, 56, 59, | ||
60, 62, 64, 65, 66, 68, 72, 73, 76, 77, | ||
78, 80, 81, 82, 84, 85, 86, 87, 88, 90 ] | ||
}, | ||
"0x3004" : { | ||
"refresh_rates": [ 45, 47, 48, 49, 50, 51, 53, 55, 56, 59, | ||
60, 62, 64, 65, 66, 68, 72, 73, 76, 77, | ||
78, 80, 81, 82, 84, 85, 86, 87, 88, 90 ] | ||
} | ||
} | ||
} | ||
} | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
#include "config.hpp" | ||
|
||
#include <algorithm> | ||
#include <cinttypes> | ||
#include <filesystem> | ||
#include <fstream> | ||
#include <json/json.h> | ||
#include <unistd.h> | ||
|
||
namespace gamescope { | ||
|
||
static std::map<std::pair<std::string, uint16_t>, std::vector<uint32_t>> refresh_rates; | ||
static const char config_dir_name[] = "/etc/gamescope.d"; | ||
|
||
// Parse the refresh_rates array | ||
static int parse_refresh_rates(const Json::Value& rates, const std::string& vendor_id, uint16_t product_id) | ||
{ | ||
if (!rates.isArray()) | ||
{ | ||
fprintf(stderr, "Unexpected value: 'display_configuration.%s.%" PRIu16 ".refresh_rates' sound be an array\n", | ||
vendor_id.c_str(), product_id); | ||
return -1; | ||
} | ||
|
||
std::vector<uint32_t> v; | ||
v.reserve(rates.size()); | ||
for (const auto& iter : rates) | ||
{ | ||
if (!iter.isUInt() || iter.asUInt() > 0xffff) | ||
{ | ||
fprintf(stderr, "Invalid value in 'display_configuration.%s.%" PRIu16 ".refresh_rates': '%s'\n", | ||
vendor_id.c_str(), product_id, iter.asString().c_str()); | ||
return -1; | ||
} | ||
v.push_back(iter.asUInt()); | ||
} | ||
// Overwrite any existing values (from e.g. a previous config file) | ||
refresh_rates.insert_or_assign(std::make_pair(vendor_id, product_id), std::move(v)); | ||
|
||
return 0; | ||
} | ||
|
||
// Parse the 'display_configuration' setting | ||
static int parse_display_config(const Json::Value& display_cfg) | ||
{ | ||
if (!display_cfg.isObject()) | ||
{ | ||
fprintf(stderr, "Unexpected value: 'display_configuration' should be an object\n"); | ||
return -1; | ||
} | ||
|
||
for (const auto& vendor_id : display_cfg.getMemberNames()) | ||
{ | ||
const auto& vendor = display_cfg[vendor_id]; | ||
if (!vendor.isObject()) | ||
{ | ||
fprintf(stderr, "Unexpected value: 'display_configuration.%s' should be an object\n", | ||
vendor_id.c_str()); | ||
return -1; | ||
} | ||
|
||
for (const auto& product_id_str : vendor.getMemberNames()) | ||
{ | ||
char* endptr; | ||
unsigned long product_id = strtoul(product_id_str.c_str(), &endptr, 0); | ||
if (product_id_str.empty() || *endptr != '\0' || product_id > 0xffff) | ||
{ | ||
fprintf(stderr, "Unexpected value: product ID '%s' in 'display_configuration.%s' should be a 16-bit integer\n", | ||
product_id_str.c_str(), vendor_id.c_str()); | ||
return -1; | ||
} | ||
|
||
const auto& product = vendor[product_id_str]; | ||
if (!product.isObject()) | ||
{ | ||
fprintf(stderr, "Unexpected value: 'display_configuration.%s.%s' should be an object\n", | ||
vendor_id.c_str(), product_id_str.c_str()); | ||
return -1; | ||
} | ||
|
||
for (const auto& name : product.getMemberNames()) | ||
{ | ||
int ret = 0; | ||
if (name == "refresh_rates") | ||
{ | ||
ret = parse_refresh_rates(product[name], vendor_id, product_id); | ||
} | ||
else | ||
{ | ||
fprintf(stderr, "Unknown configuration setting 'display_configuration.%s.%s.%s'\n", | ||
vendor_id.c_str(), product_id_str.c_str(), name.c_str()); | ||
} | ||
if (ret < 0) | ||
return ret; | ||
} | ||
} | ||
} | ||
|
||
return 0; | ||
} | ||
|
||
// Read a single configuration file | ||
static int config_read_file(const std::string& file_name) | ||
{ | ||
std::ifstream file(file_name); | ||
if (!file.is_open()) | ||
{ | ||
fprintf(stderr, "Error opening config file '%s'\n", file_name.c_str()); | ||
return -1; | ||
} | ||
|
||
Json::Value root; | ||
Json::CharReaderBuilder builder; | ||
std::string errors; | ||
if (!parseFromStream(builder, file, &root, &errors)) | ||
{ | ||
fprintf(stderr, "Error parsing config file '%s': %s\n", | ||
file_name.c_str(), errors.c_str()); | ||
return -1; | ||
} | ||
|
||
if (!root.isObject()) | ||
{ | ||
fprintf(stderr, "Unexpected format in config file '%s'\n", file_name.c_str()); | ||
return -1; | ||
} | ||
|
||
for (const auto& name : root.getMemberNames()) | ||
{ | ||
int ret = 0; | ||
if (name == "display_configuration") | ||
{ | ||
ret = parse_display_config(root[name]); | ||
} | ||
else | ||
{ | ||
fprintf(stderr, "Unknown configuration setting in '%s': '%s'\n", | ||
file_name.c_str(), name.c_str()); | ||
} | ||
if (ret < 0) | ||
return ret; | ||
} | ||
|
||
return 0; | ||
} | ||
|
||
// Read all configuration files | ||
int config_read() | ||
{ | ||
std::filesystem::path config_dir = config_dir_name; | ||
if (!std::filesystem::is_directory(config_dir)) | ||
return 0; | ||
|
||
if (access(config_dir_name, R_OK | X_OK) != 0) | ||
{ | ||
fprintf(stderr, "Cannot open directory '%s'\n", config_dir_name); | ||
return -1; | ||
} | ||
|
||
std::vector<std::string> files; | ||
for (const auto& iter : std::filesystem::directory_iterator(config_dir)) | ||
{ | ||
const auto& path = iter.path(); | ||
if (std::filesystem::is_regular_file(iter.status()) && path.extension() == ".conf") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm on the fence about using a Overall, not a big deal, will go with whatever you or @Joshua-Ashton think is best There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was actually using |
||
{ | ||
files.push_back(path); | ||
} | ||
} | ||
|
||
std::sort(files.begin(), files.end()); | ||
for (const auto& path : files) | ||
{ | ||
int ret = config_read_file(path); | ||
if (ret < 0) | ||
return ret; | ||
} | ||
|
||
return 0; | ||
} | ||
|
||
// Get the list of valid refresh rates for the specified display | ||
std::span<const uint32_t> config_get_refresh_rates(const char *vendor_id, uint16_t product_id) | ||
{ | ||
const auto& rates = refresh_rates.find(std::make_pair(vendor_id, product_id)); | ||
if (rates == refresh_rates.end()) | ||
return {}; | ||
else | ||
return rates->second; | ||
} | ||
|
||
} // namespace gamescope |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
#pragma once | ||
|
||
#include <span> | ||
#include <stdint.h> | ||
|
||
namespace gamescope | ||
{ | ||
// Read all configurations files | ||
int config_read(); | ||
|
||
// Get the list of valid refresh rates for the specified display, | ||
// or an empty std::span if they are not found. | ||
std::span<const uint32_t> config_get_refresh_rates(const char *vendor_id, uint16_t product_id); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we add an example_config.json to the install output? Sort of as a config documentation with every field commented out.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another option is installing it as
config.json.sample
so it would be ignored by default because of the extension (that's how scripts in.git/hooks
work).