Skip to content

Runtime display selection in DRM backend#2197

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

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

Conversation

@matte-schwartz
Copy link
Copy Markdown

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.

…election

Add runtime display output selection to gamescope-control. Previously the
output was only selectable via --prefer-output at startup, which is not
adjustable in gamescope-session.

The interface adds available_display_info / available_display_info_done
events to enumerate the connected outputs a client can switch to,
set_display / unset_display requests to pick one by connector name or
fall back to --prefer-output, and a matching set_display console command
taking either an exact connector name or a make/model/serial substring.

Displays are tracked as follows: the connector name is authoritative for
the actual switch (as --prefer-output already is), but a connector number
can't always be relied upon as a stable identifier. Connectors have a
possibility to be reenumerated when a cable is replugged, across reboots,
or on MST topology changes, so what was DP-1 may become DP-4. This also
depends on what type of device and dock/cable are being used. Instead,
the selection is persisted by an EDID-derived identifier, "{make} {model}
{serial}", and resolved identifier-first. Among the connected outputs,
the one whose identifier matches is chosen, so the monitor is found
regardless of its current connector.

One corner case is that physically identical, serial-less monitors could
collide as the same identifier. In this case, a " [connector]" suffix is
appended, and matching falls back to the connector as a secondary key
where the hinted connector wins if present.

The available-display list also flags the output currently being driven
(a "current" display flag), so a client can show which display is active,
including which of two otherwise-identical monitors.

With this in mind, the expected user experience should be consistent
across multiple scenarios:

- Desktop setups: connectors are stable across boots, so identical
monitors are individually selectable and the choice persists.
- Handheld connected to dock: connectors may reenumerate on replug, so
the EDID identifier carries the selection across regardless of how the
connectors come back.

display_selection is implemented in its own header in order to support
tests. I've also been able to test this on a Steam Deck with third-party
dock, a 7900XTX, an RTX 5090, and an Intel Lunar Lake APU with a Steam
Deck dock to confirm that all scenarios behave as expected.
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.
…splay

Until Steam learns about GAMESCOPE_DISPLAY_SELECTION_FILE, write it from
both setter paths (drm_set_preferred_connector for the protocol/CLI path,
drm_set_preferred_display_identifier for the GAMESCOPE_DISPLAY_PREFERRED_IDENTIFIER
xprop path) so runtime selections stick across restarts regardless of
which channel set them.

Revert once Steam owns the writes.
@matte-schwartz
Copy link
Copy Markdown
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.

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