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

Update 2026-06-01: Calling loginctl terminate-seat seat1 a “one-time migration step” was wrong. LightDM has its own udev-based seat detection, independent of logind and unaffected by logind-load-seats=false. The Part 1 udev rule tags card1 as master-of-seat for seat1, so LightDM spawns a greeter on seat1 on every boot. Since seat1 has no keyboard, that greeter never logs in and sits forever holding DRM master on the dGPU - so the next systemctl --user start gamescope-tv hits the exact drmModeAtomicCommit: Permission denied below again. The real fix is to delete the seat1 udev rule entirely; see the update in Part 3.

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. But, as the update above notes, terminate-seat only clears the current greeter - the real fix is in Part 3.

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.