Skip to content

Runtime display selection in DRM backend#2197

Open
matte-schwartz wants to merge 3 commits into
ValveSoftware:masterfrom
matte-schwartz:display-selection-control-with-tests
Open

Runtime display selection in DRM backend#2197
matte-schwartz wants to merge 3 commits into
ValveSoftware:masterfrom
matte-schwartz:display-selection-control-with-tests

Conversation

@matte-schwartz

Copy link
Copy Markdown
Collaborator

The goal for this PR is to provide runtime controls to select a preferred display in the DRM backend without relying on the commandline argument -O. The exact details are explained in the commits, but generally this is what's expected:

❯ gamescopectl
gamescopectl version 3.16.24-4-gbb82e16 (gcc 16.1.1)
gamescope_control info:
  - Connector Name: DP-3
  - Display Make: Dell Inc.
  - Display Model: Dell AW3423DW
  - Display Flags: 0x6
  - ValidRefreshRates: 175
  Available Displays:
  - DP-3 (Dell Inc. Dell AW3423DW) - Flags: 0xe - Identifier: "Dell Inc. Dell AW3423DW 810309971" [current]
  - DP-1 (LG Electronics LG ULTRAGEAR+) - Flags: 0x6 - Identifier: "LG Electronics LG ULTRAGEAR+ 175706"
  Features:
  - Reshade Shaders (1) - Version: 1 - Flags: 0x0
  - Display Info (2) - Version: 1 - Flags: 0x0
  - Pixel Filter (3) - Version: 1 - Flags: 0x0
  - Refresh Cycle Only Change Refresh Rate (4) - Version: 1 - Flags: 0x0
  - Mura Correction (5) - Version: 1 - Flags: 0x0
  - Look (6) - Version: 1 - Flags: 0x0
  - Performance Query (7) - Version: 1 - Flags: 0x0
  - Display Selection (8) - Version: 1 - Flags: 0x0
You can execute any debug command in Gamescope using this tool.
For a list of commands and convars, use 'gamescopectl help'

❯ gamescopectl set_display LG

❯ gamescopectl
gamescopectl version 3.16.24-4-gbb82e16 (gcc 16.1.1)
gamescope_control info:
  - Connector Name: DP-1
  - Display Make: LG Electronics
  - Display Model: LG ULTRAGEAR+
  - Display Flags: 0x6
  - ValidRefreshRates: 165
  Available Displays:
  - DP-3 (Dell Inc. Dell AW3423DW) - Flags: 0x6 - Identifier: "Dell Inc. Dell AW3423DW 810309971"
  - DP-1 (LG Electronics LG ULTRAGEAR+) - Flags: 0xe - Identifier: "LG Electronics LG ULTRAGEAR+ 175706" [current]
  Features:
  - Reshade Shaders (1) - Version: 1 - Flags: 0x0
  - Display Info (2) - Version: 1 - Flags: 0x0
  - Pixel Filter (3) - Version: 1 - Flags: 0x0
  - Refresh Cycle Only Change Refresh Rate (4) - Version: 1 - Flags: 0x0
  - Mura Correction (5) - Version: 1 - Flags: 0x0
  - Look (6) - Version: 1 - Flags: 0x0
  - Performance Query (7) - Version: 1 - Flags: 0x0
  - Display Selection (8) - Version: 1 - Flags: 0x0
You can execute any debug command in Gamescope using this tool.
For a list of commands and convars, use 'gamescopectl help'

you can also run gamescopectl set_display <connector name> rather than substrings of the monitor identifiers. the choice from Steam is meant to be written to ~/.config/gamescope/display.cfg, similar to modes.cfg, but [HACK] DRMBackend: Temporarily write preferred-display file on set_display moves this write into gamescope for testing purposes:

❯ cat ~/.config/gamescope/display.cfg
LG Electronics LG ULTRAGEAR+ 175706

there is an xatom path for this as well:

❯ DISPLAY=:0 xprop -root GAMESCOPE_DISPLAY_AVAILABLE_LIST
GAMESCOPE_DISPLAY_AVAILABLE_LIST(STRING) = "Dell Inc. Dell AW3423DW 810309971|DP-3|0x6|Dell Inc.|Dell AW3423DW\nLG Electronics LG ULTRAGEAR+ 175706|DP-1|0xe|LG Electronics|LG ULTRAGEAR+\n"

❯ DISPLAY=:0 xprop -root -f GAMESCOPE_DISPLAY_PREFERRED_IDENTIFIER 8s -set GAMESCOPE_DISPLAY_PREFERRED_IDENTIFIER "Dell Inc. Dell AW3423DW 810309971"

tests are included so it was easier to check the code around some of the weird edge cases. this is also why display_selection.h is its own header file, to pull into the testing framework.

marked as draft for now so additional testing can be done, but before bringing it out of draft I will drop hack commit.

@matte-schwartz

Copy link
Copy Markdown
Collaborator Author

I'm about to leave on vacation for a few days, but my goal is to finish up testing and undraft this sometime next week.

…election

The output was previously only selectable with --prefer-output at
startup, which gamescope-session can't adjust. Add
available_display_info / available_display_info_done events to
enumerate the connected outputs, set_display / unset_display requests
to pick one by connector name or fall back to --prefer-output, and a
matching set_display console command that also accepts a
make/model/serial substring. A "current" display flag marks the output
being driven.

The connector name is authoritative for the switch, but connectors can
be renumbered on replug, across reboots, or on MST topology changes, so
the selection is persisted as an EDID-derived identifier, "{make}
{model} {serial}", and resolved identifier-first among the connected
outputs. The serial prefers the per-unit descriptor string, since the
numeric serial field is commonly a per-model constant. Physically
identical, serial-less monitors that collide get a " [connector]"
suffix, with the hinted connector as tiebreak. The persisted key is
reloaded at startup from GAMESCOPE_DISPLAY_SELECTION_FILE, and writing
the file is owned by the session/Steam side. display_selection.h is a
standalone header so the resolution logic is unit-testable.

Connected outputs are mirrored into a snapshot rebuilt on the
steamcompmgr thread. It is the only connector state other threads read,
so protocol requests don't stall behind EDID probes or share a lock
with refresh_state, whose EDID parsing can run user Lua that itself
calls set_display. Display info is re-broadcast only when a poll
reconfigured the output or changed the snapshot, and re-setting the
current preference is a no-op. A selection sent before a deferred
backend has initialized its child is queued and replayed at init.
Physically identical, serial-less monitors collide on the same EDID
identifier, so the selection key falls back to a " [connector]" suffix to
tell them apart. However, the connector name keys on connector_type_id,
which the kernel reassigns when the MST sink connectors are torn down and
recreated on reprobe, so the selection jumps around unexpectedly.

Instead, read the DRM MST PATH blob ("mst:<conn>-<port>") and use it as
the tiebreak hint when present. The port numbers are topology-derived
(which port of the dock the sink hangs off), and the "mst:" prefix is the
upstream connector id, which is fixed at output setup and not re-init'd on
a downstream reprobe. So the PATH survives the connector renumber that
connector_type_id does not. Non-MST and single-stream connections have no
PATH and fall back to the connector name as before.
GAMESCOPE_DISPLAY_PREFERRED_IDENTIFIER sets the preferred display by its
EDID identifier, GAMESCOPE_DISPLAY_AVAILABLE_LIST mirrors the connected
outputs. The handler only honors the root window's property - any other
client interning the same atom on its own window would otherwise clear
the preference with the unset root value - and both sides skip no-op
rewrites to avoid notify/reprobe churn.
@matte-schwartz
matte-schwartz force-pushed the display-selection-control-with-tests branch from d85e510 to 42028df Compare July 13, 2026 00:10
@matte-schwartz
matte-schwartz marked this pull request as ready for review July 13, 2026 00:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant