The Goal

Part 1 ended with a working setup and one major limitation: the TV gaming session on seat1 was always-on. To stop it - for GPU passthrough to a Windows VM, or to run Steam on the desk instead - required restarting all of LightDM, which also killed the Sway session on seat0 and lost all open windows.

The goal: replace LightDM’s static seat1 management with something that can be stopped and started independently, without touching seat0.

First Attempt: greetd

greetd is a minimal display manager where each instance runs as an independent systemd service. The idea: keep LightDM managing seat0, move seat1 to a separate greetd-seat1.service that can be stopped and started on demand.

Two problems appeared immediately. First, greetd always switches the VT when starting a session - blanking the desk monitors with no way to recover short of Ctrl+Alt+F to a known VT. There is no config option to disable this.

Second, and more fundamentally: greetd creates logind sessions via a TTY, and TTYs (/dev/tty1 through /dev/tty63) belong to seat0 by definition. The resulting logind session always registers on seat0, regardless of what GREETD_SEAT says in the service environment. A session on seat0 does not have access to seat1’s DRM devices. gamescope crashed immediately.

seat1 here is a DRM-only seat - no VTs, no keyboard, no mouse. To create a logind session that registers on seat1, the display manager needs to communicate with logind using a seat1 device directly, not a TTY. LightDM does this, which is why the Part 1 setup worked. greetd does not have this capability.

The Pivot: seatd

seatd is a minimal seat management daemon. Instead of creating logind sessions, compositors connect to seatd directly over a socket and request access to specific DRM devices - no TTY, no logind session involved. The plan: drop the seat1 display manager entirely and run gamescope as a systemd user service, with seatd as the device access backend.

seatd Configuration

seatd by default binds VTs when managing sessions. When gamescope exits, seatd restores VT state, which blanks the desk monitors and requires a manual VT switch to recover. A drop-in disables this:

# /etc/systemd/system/seatd.service.d/override.conf
[Service]
Environment=SEATD_VTBOUND=0

The Gamescope User Service

gamescope picks up seatd automatically when the daemon is running - libseat (the library gamescope uses for device access) detects seatd and uses it without explicit configuration. WLR_DRM_DEVICES=/dev/dri/card1 pins gamescope to the dGPU:

# ~/.config/systemd/user/gamescope-tv.service
[Unit]
Description=Gamescope TV Session

[Service]
Type=simple
Environment=WLR_DRM_DEVICES=/dev/dri/card1
ExecStart=%h/.local/bin/gamescope-steam-session
Restart=on-failure
RestartSec=3

[Install]
WantedBy=default.target

The session script is simplified from Part 1. systemd’s Restart=on-failure replaces the while true restart loop. exec replaces the shell process so systemd tracks the correct PID - without it, systemctl stop would send SIGTERM to the bash wrapper rather than to gamescope:

#!/bin/bash
# ~/.local/bin/gamescope-steam-session
exec &> "$HOME/.local/share/gamescope-session.log"

export PULSE_SINK="alsa_output.pci-0000_03_00.1.hdmi-stereo-extra3"

exec gamescope \
    --backend drm \
    -W 3840 -H 2160 \
    -r 120 \
    -f \
    -e \
    --prefer-output HDMI-A-1 \
    --prefer-vk-device 1002:7550 \
    --force-grab-cursor \
    --adaptive-sync \
    --hdr-enabled \
    -- steam-native -gamepadui

Clearing the Leftover LightDM Session

The first start produced a familiar error:

[gamescope] [Error] [liftoff] drmModeAtomicCommit: Permission denied

The LightDM greeter from the old seat1 configuration was still alive, holding DRM master on card1. loginctl terminate-seat seat1 killed it. After that, systemctl --user start gamescope-tv brought up Steam Big Picture on the TV, and systemctl --user stop gamescope-tv shut it down cleanly, leaving the Sway session on seat0 untouched.

Note: loginctl terminate-seat seat1 is a one-time migration step - only needed when transitioning from the Part 1 LightDM setup.

Updating the Libvirt Hooks

The libvirt hooks that handle GPU passthrough to the Windows VM now stop and start the gamescope service rather than all of LightDM. The hook structure comes from VFIO-Tools - scripts are dispatched per-VM into subdirectories and run in alphabetical order within each lifecycle directory. The stop script gets a 00- prefix to run before the existing gpu-passthrough.sh; the start script gets 99- to run after the GPU is returned:

# /etc/libvirt/hooks/qemu.d/windows/prepare/begin/00-seat1-stop.sh
#!/bin/bash
systemctl --user -M oleksandr@ stop gamescope-tv
sleep 2
# /etc/libvirt/hooks/qemu.d/windows/release/end/99-seat1-start.sh
#!/bin/bash
systemctl --user -M oleksandr@ start gamescope-tv

systemctl --user -M oleksandr@ reaches the user’s systemd instance from a root-level libvirt hook without needing sudo. The sleep 2 in the stop script gives gamescope time to fully release DRM master before the GPU unbind begins.

What Works

systemctl --user start gamescope-tv and systemctl --user stop gamescope-tv start and stop the TV session independently. The Sway session on seat0 is unaffected in either case. GPU passthrough hooks stop and restart the session cleanly around VM startup without touching the desktop session.

What Doesn’t

Moving the desk mouse moves the cursor in Steam Big Picture on the TV. Keyboard input reaches Steam. The desk input devices are bleeding into the TV session despite seat1 having no input devices assigned to it.

This turns out to be deeper than it looks. Part 3 traces it to root cause and fixes it.