Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 109 additions & 10 deletions src/Backends/DRMBackend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/sysmacros.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
Expand Down Expand Up @@ -1198,6 +1199,82 @@ gamescope_liftoff_log_handler(enum liftoff_log_priority liftoff_priority, const
liftoff_log_scope.vlogf(priority, fmt, args);
}

// Resolve the value of --prefer-drm to an absolute DRM primary node path.
//
// Accepts:
// * "/dev/dri/cardX" (absolute path, used as-is)
// * "cardX" (short form, prefixed with /dev/dri/)
// * "/dev/dri/by-path/pci-...-card" (symlink, resolved to its target)
//
// Returns the canonical path on success, or an empty string if the path does
// not exist (realpath() also doubles as the existence check here).
static std::string resolve_prefer_drm_device( const char *pszInput )
{
if ( pszInput == nullptr || pszInput[0] == '\0' )
return std::string();

std::string sPath;
if ( pszInput[0] != '/' )
sPath = std::string( "/dev/dri/" ) + pszInput;
else
sPath = pszInput;

char szResolved[PATH_MAX];
if ( realpath( sPath.c_str(), szResolved ) == nullptr )
return std::string();

return std::string( szResolved );
}

// Resolve and validate --prefer-drm (g_sPreferredDrmDevice). On success returns
// the canonical DRM primary-node path; on failure logs a clear error and
// returns an empty string. Only call this when --prefer-drm was actually given.
//
// We deliberately do NOT open() the device to validate it: under seatd/logind
// the compositor process usually cannot open the primary node directly (that is
// the session manager's job), so a plain open() would spuriously fail with
// EACCES on perfectly valid setups. Inspect the device number instead -- DRM
// primary node minors are 0-63, render nodes are 128+. The authoritative KMS
// check (drmIsKMS) still runs later once the session opens the fd.
static std::string resolve_and_validate_prefer_drm_device()
{
std::string sDrmPath = resolve_prefer_drm_device( g_sPreferredDrmDevice );
if ( sDrmPath.empty() )
{
drm_log.errorf( "--prefer-drm: could not resolve DRM device '%s' (does not exist?)", g_sPreferredDrmDevice );
return std::string();
}

struct stat drmStat = {};
if ( stat( sDrmPath.c_str(), &drmStat ) != 0 || !S_ISCHR( drmStat.st_mode ) )
{
drm_log.errorf( "--prefer-drm: '%s' is not a character device", sDrmPath.c_str() );
return std::string();
}
if ( minor( drmStat.st_rdev ) >= 64 )
{
drm_log.errorf( "--prefer-drm: '%s' is not a DRM primary node; pass a KMS device such as /dev/dri/cardX (not a render node)", sDrmPath.c_str() );
return std::string();
}

return sDrmPath;
}

namespace gamescope
{
// Pre-flight check run from main() before the DRM backend (and the heavy
// Vulkan/session init) is created, so an invalid --prefer-drm fails fast with
// a clear message instead of tearing down a half-initialized backend later.
// Returns true when --prefer-drm is unset or resolves to a valid primary node.
bool DRMBackendCheckPreferredDevice()
{
if ( g_sPreferredDrmDevice == nullptr )
return true;

return !resolve_and_validate_prefer_drm_device().empty();
}
}

bool init_drm(struct drm_t *drm, int width, int height, int refresh)
{
load_pnps();
Expand All @@ -1209,20 +1286,39 @@ bool init_drm(struct drm_t *drm, int width, int height, int refresh)
drm->preferred_refresh = refresh;

drm->device_name = nullptr;
dev_t dev_id = 0;
if (vulkan_primary_dev_id(&dev_id)) {
drmDevice *drm_dev = nullptr;
if (drmGetDeviceFromDevId(dev_id, 0, &drm_dev) != 0) {
drm_log.errorf("Failed to find DRM device with device ID %" PRIu64, (uint64_t)dev_id);
if ( g_sPreferredDrmDevice != nullptr )
{
// User explicitly selected a DRM/KMS device for scanout via --prefer-drm.
// This decouples the scanout device from the Vulkan render device
// (--prefer-vk-device), which is needed for hybrid/eGPU/muxless setups
// where a headless render GPU composites onto a display attached to a
// different GPU. The value was already resolved and validated up-front by
// gamescope::DRMBackendCheckPreferredDevice() in main(), so this should
// not normally fail here.
std::string sDrmPath = resolve_and_validate_prefer_drm_device();
if ( sDrmPath.empty() )
return false;
}
assert(drm_dev->available_nodes & (1 << DRM_NODE_PRIMARY));
drm->device_name = strdup(drm_dev->nodes[DRM_NODE_PRIMARY]);
drm_log.infof("opening DRM node '%s'", drm->device_name);

drm->device_name = strdup( sDrmPath.c_str() );
drm_log.infof( "--prefer-drm: using DRM scanout device '%s'", drm->device_name );
}
else
{
drm_log.infof("warning: picking an arbitrary DRM device");
dev_t dev_id = 0;
if (vulkan_primary_dev_id(&dev_id)) {
drmDevice *drm_dev = nullptr;
if (drmGetDeviceFromDevId(dev_id, 0, &drm_dev) != 0) {
drm_log.errorf("Failed to find DRM device with device ID %" PRIu64, (uint64_t)dev_id);
return false;
}
assert(drm_dev->available_nodes & (1 << DRM_NODE_PRIMARY));
drm->device_name = strdup(drm_dev->nodes[DRM_NODE_PRIMARY]);
drm_log.infof("opening DRM node '%s'", drm->device_name);
}
else
{
drm_log.infof("warning: picking an arbitrary DRM device");
}
}

drm->fd = wlsession_open_kms( drm->device_name );
Expand Down Expand Up @@ -1306,6 +1402,9 @@ bool init_drm(struct drm_t *drm, int width, int height, int refresh)
drm_log.infof(" %s (%s)", pConnector->GetName(), status_str);
}

if ( g_sOutputName != nullptr )
drm_log.infof( "preferred output (--prefer-output): %s", g_sOutputName );

drm->connector_priorities = parse_connector_priorities( g_sOutputName );

if (!setup_best_connector(drm, true, true)) {
Expand Down
6 changes: 6 additions & 0 deletions src/backends.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,10 @@ namespace gamescope
class COpenVRBackend;
class CHeadlessBackend;
class CWaylandBackend;

// Pre-flight validation for --prefer-drm. Returns true when --prefer-drm is
// unset or resolves to a valid DRM primary node; logs an error and returns
// false otherwise. Lets main() reject bad input before the DRM backend and
// Vulkan/session init are brought up.
bool DRMBackendCheckPreferredDevice();
}
20 changes: 20 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ const struct option *gamescope_options = (struct option[]){
{ "fsr-sharpness", required_argument, nullptr, 0 },
{ "rt", no_argument, nullptr, 0 },
{ "prefer-vk-device", required_argument, 0 },
{ "prefer-drm", required_argument, 0 },
{ "expose-wayland", no_argument, 0 },
{ "mouse-sensitivity", required_argument, nullptr, 's' },
{ "mangoapp", no_argument, nullptr, 0 },
Expand Down Expand Up @@ -202,6 +203,9 @@ const char usage[] =
" -e, --steam enable Steam integration\n"
" --xwayland-count create N xwayland servers\n"
" --prefer-vk-device prefer Vulkan device for compositing (ex: 1002:7300)\n"
" --prefer-drm prefer DRM/KMS device for scanout/output, independent of the Vulkan device\n"
" (ex: /dev/dri/card0, card1, /dev/dri/by-path/pci-0000:00:02.0-card)\n"
" also read from $GAMESCOPE_PREFER_DRM if the flag is not given\n"
" --use-rotation-shader use rotation shader for rotating the screen\n"
" --force-orientation rotate the internal display (left, right, normal, upsidedown)\n"
" --force-windows-fullscreen force windows inside of gamescope to be the size of the nested display (fullscreen)\n"
Expand Down Expand Up @@ -729,6 +733,7 @@ int g_nPreferredOutputWidth = 0;
int g_nPreferredOutputHeight = 0;
bool g_bExposeWayland = false;
const char *g_sOutputName = nullptr;
const char *g_sPreferredDrmDevice = nullptr;
bool g_bDebugLayers = false;
bool g_bForceDisableColorMgmt = false;
bool g_bRt = false;
Expand Down Expand Up @@ -846,6 +851,8 @@ int main(int argc, char **argv)
sscanf( optarg, "%X:%X", &vendorID, &deviceID );
g_preferVendorID = vendorID;
g_preferDeviceID = deviceID;
} else if (strcmp(opt_name, "prefer-drm") == 0) {
g_sPreferredDrmDevice = optarg;
} else if (strcmp(opt_name, "immediate-flips") == 0) {
cv_tearing_enabled = true;
} else if (strcmp(opt_name, "force-grab-cursor") == 0) {
Expand Down Expand Up @@ -957,6 +964,19 @@ int main(int argc, char **argv)
{
#if HAVE_DRM
case gamescope::GamescopeBackend::DRM:
// --prefer-drm may also be supplied via the environment (e.g. set by
// the session, like VULKAN_ADAPTER), so a split render/scanout setup
// needs no per-game launch options. The CLI flag takes precedence.
if ( !g_sPreferredDrmDevice )
{
const char *pszEnvDrm = getenv( "GAMESCOPE_PREFER_DRM" );
if ( pszEnvDrm && pszEnvDrm[0] )
g_sPreferredDrmDevice = pszEnvDrm;
}
// Validate --prefer-drm up-front so bad input fails fast with a clear
// message, before Vulkan/session init is brought up.
if ( !gamescope::DRMBackendCheckPreferredDevice() )
return 1;
gamescope::IBackend::Set<gamescope::CDRMBackend>();
break;
#endif
Expand Down
1 change: 1 addition & 0 deletions src/main.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ extern bool g_bGrabbed;

extern float g_mouseSensitivity;
extern const char *g_sOutputName;
extern const char *g_sPreferredDrmDevice;
extern std::vector<uint32_t> g_customRefreshRates;

enum class GamescopeUpscaleFilter : uint32_t
Expand Down