Games launching on the wrong monitor has been bothering me since forever...
Last updated on

How to fix games on Linux launching on the wrong monitor (if XWayland)


The problem

Since I switched to Linux full-send however long ago, a bit of a pain point has been games launching on the wrong monitor. A good-old Win-Shift-Left or whatever would work in most cases and just be an annoyance. In other cases, the game wouldn’t render correctly when switching to another monitor.

If you have this problem and are running Wayland, the part of issue is that Proton will launch games through XWayland. On top of Wayland not having the concept of a primary monitor (if I’m not mistaken), it’s X’s concept of primary matter that matters. As such, an ad-hoc fix is to use xrandr --output <monitor> --primary to switch X’s primary monitor.

The problem with xrandr is that this isn’t sticky. I think .xinitrc works for XWayland, so there’s one option. In my case, my OLED monitor turns off completely after some time of inactivity — a behavior I’d prefer to keep. When this happens, the primary monitor switches to my other monitor. When I turn back on my actually primary monitor, it doesn’t switch back.

I’ve tried finding a quick and easy solution for quite some time, to no avail. This means we’ll have to write our own solution.

Potential approaches

Feel free to skip this section, since it won’t contain a working solution.

In principle, there are a few approaches to take:

  • udev rules
  • systemd device units
  • systemd path units
  • A script with an infinite loop (via KDE autostart scripts, in my case)

systemd device units

systemd device units 🔗 are systemd units that trigger whenever a udev ACTION=="change" event happens. By default, GPU devices don’t get a systemd device unit. To add one, we need to create a udev rule that adds a systemd tag.

KERNEL=="card0", SUBSYSTEM=="drm", ACTION=="change", TAG+="systemd"

Once this rule runs for the first time, a dev-dri-card0.device systemd unit will appear.

This won’t work for us, though, because user services can’t refer to system units, which includes device units.

systemd path units

systemd path units 🔗 could also hypothetically work. As one would expect, they trigger when the folder or file pointed to changes.

This was the first thing I tried, actually, and the unit never triggered for me. They look something like this, incidentally.

[Unit]
Description=blah blah blah

[Path]
PathChanged=/sys/class/drm/card0

[Install]
WantedBy=default.target

It didn’t appear to be a permission issue, since some other udev rule gives it uaccess, and other files within that directory didn’t work, either. It’s by far the easiest solution if it works, so it may be worth a shot. In my case, I had to go to plan b.

Fixing via udev rules

Ultimately, I chose to use udev rules to launch a script. First, the udev rule:

# "level" 99- works fine
KERNEL=="card0", SUBSYSTEM=="drm", ACTION=="change", TAG+="systemd", RUN+="/usr/bin/su tim -c '/home/tim/primary-monitor.sh'"

Since scripts are run as root, we need to use su. One thing that messed with me for a while is that the full path to su needs to be provided.

And our script!

#!/usr/bin/env bash

monitor="DP-3"

# seem to need some amount of sleep in practice, and 5sec should be more than enough
sleep 5

if [[ -z "$XAUTHORITY" ]]; then
    export XAUTHORITY="~/.Xauthority"
    if [[ ! -f "$XAUTHORITY" ]]; then
        # at least on my distro (I think due to sddm), the file is here
        export XAUTHORITY=$(find /run/user/$(id -u) -maxdepth 1 -iname 'xauth*' 2>/dev/null | head -n 1)
    fi
fi

if [[ -z "$DISPLAY" ]]; then
    export DISPLAY=':0'
fi


xrandr --output "$monitor" --primary

The checks to make sure proper XAUTHORITY and DISPLAY variables are defined are necessary in our case, since the root user won’t have them set, of course. The [[ -z ]] if statements are included to make things a bit simpler if executing in one’s own shell, but they certainly aren’t necessary. Feel free to remove these if desired.

In my case, a tricky part was the Xauthority file, which is (probably) generated by SDDM on my machine. You may need to adjust this for your machine, and the easiest way to find the path is simply by printenving to find it!

Be sure to reload the udev rules, as well.

sudo udevadm control --reload

Give things a test by turning off and on your primary monitor, and running xrandr to see if the primary monitor is set correctly.

xrandr | grep -P "^[^ ]"

If it’s not working, be sure to check the logs. The log level can also be changed, though I won’t cover it here.

journalctl -u systemd-udevd

You might also consider running the script in an empty environment to verify that the extra logic is working.

env -i bash
./script.sh

Running at logon

The only thing missing with udev rules is running the script the first time on logon. There are plenty of ways to do it, but why not a systemd user service?

[Unit]
Description=Fix primary monitor when it comes back

[Install]
WantedBy=default.target

[Service]
ExecStart=%h/OneDrive/scripts/primary-monitor.sh
Type=oneshot

Just gotta enable (and run) it!

systemctl --user enable --now primary-monitor.service