diff --git a/config/display.conf b/config/display.conf new file mode 100644 index 000000000..62350d7a1 --- /dev/null +++ b/config/display.conf @@ -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 ] + } + } + } +} diff --git a/src/config.cpp b/src/config.cpp new file mode 100644 index 000000000..e2cc73d1e --- /dev/null +++ b/src/config.cpp @@ -0,0 +1,191 @@ +#include "config.hpp" + +#include +#include +#include +#include +#include +#include + +namespace gamescope { + +static std::map, std::vector> 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 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 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 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 diff --git a/src/config.hpp b/src/config.hpp new file mode 100644 index 000000000..993c6e1f0 --- /dev/null +++ b/src/config.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include +#include + +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 config_get_refresh_rates(const char *vendor_id, uint16_t product_id); +} diff --git a/src/drm.cpp b/src/drm.cpp index 0b73ebc6a..a0edf2e85 100644 --- a/src/drm.cpp +++ b/src/drm.cpp @@ -27,6 +27,7 @@ #include "backend.h" #include "color_helpers.h" +#include "config.hpp" #include "defer.hpp" #include "drm_include.h" #include "edid.h" @@ -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 ) || @@ -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 ); } } diff --git a/src/main.cpp b/src/main.cpp index 01dd8ca47..7d52d3305 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -19,6 +19,7 @@ #include #include "main.hpp" +#include "config.hpp" #include "steamcompmgr.hpp" #include "rendervulkan.hpp" #include "wlserver.hpp" @@ -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 ) diff --git a/src/meson.build b/src/meson.build index aaf8acea6..cc8df1040 100644 --- a/src/meson.build +++ b/src/meson.build @@ -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')) @@ -94,6 +95,7 @@ required_wlroots_features = ['xwayland'] src = [ 'steamcompmgr.cpp', + 'config.cpp', 'convar.cpp', 'commit.cpp', 'color_helpers.cpp', @@ -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,