Skip to content

Commit

Permalink
Add config file support
Browse files Browse the repository at this point in the history
Load config files from /etc/gamescope.d/*.conf in json format.

This currently allows the user to specify the allowed refresh rates
for different displays.
  • Loading branch information
bertogg committed May 30, 2024
1 parent 0065946 commit 8ca8583
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 4 deletions.
17 changes: 17 additions & 0 deletions config/display.conf
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 ]
}
}
}
}
191 changes: 191 additions & 0 deletions src/config.cpp
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")
{
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
14 changes: 14 additions & 0 deletions src/config.hpp
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);
}
14 changes: 11 additions & 3 deletions src/drm.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

#include "backend.h"
#include "color_helpers.h"
#include "config.hpp"
#include "defer.hpp"
#include "drm_include.h"
#include "edid.h"
Expand Down Expand Up @@ -2099,6 +2100,10 @@ namespace gamescope

drm_log.infof("Connector %s -> %s - %s", m_Mutable.szName, m_Mutable.szMakePNP, m_Mutable.szModel );

m_Mutable.ValidDynamicRefreshRates = config_get_refresh_rates( m_Mutable.szMakePNP, pProduct->product );
if (!m_Mutable.ValidDynamicRefreshRates.empty())
drm_log.infof("Got refresh rates from the configuration file");

const bool bSteamDeckDisplay =
( m_Mutable.szMakePNP == "WLC"sv && m_Mutable.szModel == "ANX7530 U"sv ) ||
( m_Mutable.szMakePNP == "ANX"sv && m_Mutable.szModel == "ANX7530 U"sv ) ||
Expand All @@ -2114,17 +2119,20 @@ namespace gamescope
if ( pProduct->product == kPIDGalileoSDC )
{
m_Mutable.eKnownDisplay = GAMESCOPE_KNOWN_DISPLAY_STEAM_DECK_OLED_SDC;
m_Mutable.ValidDynamicRefreshRates = std::span( s_kSteamDeckOLEDRates );
if (m_Mutable.ValidDynamicRefreshRates.empty())
m_Mutable.ValidDynamicRefreshRates = std::span( s_kSteamDeckOLEDRates );
}
else if ( pProduct->product == kPIDGalileoBOE )
{
m_Mutable.eKnownDisplay = GAMESCOPE_KNOWN_DISPLAY_STEAM_DECK_OLED_BOE;
m_Mutable.ValidDynamicRefreshRates = std::span( s_kSteamDeckOLEDRates );
if (m_Mutable.ValidDynamicRefreshRates.empty())
m_Mutable.ValidDynamicRefreshRates = std::span( s_kSteamDeckOLEDRates );
}
else
{
m_Mutable.eKnownDisplay = GAMESCOPE_KNOWN_DISPLAY_STEAM_DECK_LCD;
m_Mutable.ValidDynamicRefreshRates = std::span( s_kSteamDeckLCDRates );
if (m_Mutable.ValidDynamicRefreshRates.empty())
m_Mutable.ValidDynamicRefreshRates = std::span( s_kSteamDeckLCDRates );
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include <float.h>

#include "main.hpp"
#include "config.hpp"
#include "steamcompmgr.hpp"
#include "rendervulkan.hpp"
#include "wlserver.hpp"
Expand Down Expand Up @@ -709,6 +710,9 @@ int main(int argc, char **argv)
}
}

if ( gamescope::config_read() != 0 )
return 1;

#if defined(__linux__) && HAVE_LIBCAP
cap_t caps = cap_get_proc();
if ( caps != nullptr )
Expand Down
4 changes: 3 additions & 1 deletion src/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ thread_dep = dependency('threads')
cap_dep = dependency('libcap', required: get_option('rt_cap'))
epoll_dep = dependency('epoll-shim', required: false)
glm_dep = dependency('glm')
jsoncpp_dep = dependency('jsoncpp')
sdl_dep = dependency('SDL2', required: get_option('sdl2_backend'))
stb_dep = dependency('stb')
avif_dep = dependency('libavif', version: '>=1.0.0', required: get_option('avif_screenshots'))
Expand Down Expand Up @@ -94,6 +95,7 @@ required_wlroots_features = ['xwayland']

src = [
'steamcompmgr.cpp',
'config.cpp',
'convar.cpp',
'commit.cpp',
'color_helpers.cpp',
Expand Down Expand Up @@ -161,7 +163,7 @@ endforeach
include_directories : [reshade_include],
dependencies: [
dep_wayland, dep_x11, dep_xdamage, dep_xcomposite, dep_xrender, dep_xext, dep_xfixes,
dep_xxf86vm, dep_xres, glm_dep, drm_dep, wayland_server,
dep_xxf86vm, dep_xres, glm_dep, drm_dep, wayland_server, jsoncpp_dep,
xkbcommon, thread_dep, sdl_dep, wlroots_dep,
vulkan_dep, liftoff_dep, dep_xtst, dep_xmu, cap_dep, epoll_dep, pipewire_dep, librt_dep,
stb_dep, displayinfo_dep, openvr_dep, dep_xcursor, avif_dep, dep_xi,
Expand Down

0 comments on commit 8ca8583

Please sign in to comment.