Files
punktfunk/docs-site/content/docs/gamescope.md
T
enricobuehler fb309e0262
ci / bun-nix (pull_request) Successful in 33s
ci / web (pull_request) Successful in 1m11s
apple / swift (pull_request) Successful in 1m38s
apple / screenshots (pull_request) Skipped
ci / docs-site (pull_request) Successful in 2m9s
ci / rust-arm64 (pull_request) Successful in 3m25s
android / android (pull_request) Successful in 4m28s
ci / rust (pull_request) Successful in 19m29s
fix(pf-vdisplay): the takeover blamed polkit for a group it never named, and offered two remedies that cannot work
Field triage on Nobara, 2026-08-09. Every connect degraded to ATTACH — which on that box mirrors a
game-mode session the host never configured, and looked like a black screen on every connect. The
host said:

    the packaged pf-dm-helper polkit action is missing or was denied (reinstall the punktfunk
    package, or install the display-manager polkit rule from the docs)

Every clause of that was wrong. The action was installed, `allow_any`, and its exec.path annotation
matched the installed helper; pkexec authorized it and RAN the helper. The helper refused, and said
exactly why:

    pf-dm-helper: user 'nobara-user' is not in the 'punktfunk' group — refusing.
      Grant it with: sudo usermod -aG punktfunk nobara-user   (then re-login)

That text never reached the log, because `dm_helper` ran the helper with `.status()` — which
discards stderr and collapses the exit code to a bool. The one thing that would have ended the
investigation in seconds was thrown away at the call site, and the caller then guessed. Neither
suggested remedy adds anyone to a group, so a reader who followed both stayed broken and learned the
docs were useless. It fails soft, with no error and no failed unit, so nobody finds it on purpose.

Now: `.output()`, and four failure modes that stay distinguishable because they need different
fixes — helper not installed, pkexec could not run it, polkit denied it (pkexec's own 126/127), and
the helper ran and refused, whose stderr rides through VERBATIM rather than being re-described. Null
stdin too, so a pkexec that decides to prompt gets EOF instead of parking a stream thread on a tty
read.

The same gate gates the `linger` verb, so on a sessionless host an unjoined user fails there first —
carrying the reason there as well, or the misdiagnosis just moves one message earlier.

A new startup preflight says it before a stream is being built rather than during one, gated so it
cannot nag a box that would never attempt a takeover: not root, a display-manager alias exists, a
managed session launcher exists, a packaged helper exists, and the user is not in the group. It reads
membership from the user database rather than this process's groups, deliberately: that is what the
helper reads (it runs as root and resolves the caller from the database), so `usermod -aG` satisfies
the DM gate immediately and the warning stops. Using `getgroups()` would keep warning on a box where
the takeover already works.

Packaging said the group was for "the virtual Steam Deck pad (usbip)" — so anyone without a Deck pad
correctly skipped it and landed here by following instructions properly. All three scriptlets now
lead with Game Mode, name both grants, and record that creating the group is necessary and NOT
sufficient. Docs get the same treatment: the group is an admonition above the DM-flavor list in
gamescope.md, a black-screen entry in troubleshooting.md that tells the reader to read the quoted
reason FIRST, and the per-distro install pages no longer frame it as pad-only.
2026-08-09 23:15:34 +02:00

17 KiB

title, description
title description
Steam / gamescope Configure a gamescope/Steam host — how the host gets a gamescope, session following, and limits.

gamescope is the compositor behind Steam Gaming Mode — the couch/handheld game UI on Bazzite, SteamOS, or any distro running a gamescope session. The host auto-detects gamescope from your live session, so you rarely need to set anything here. It also follows a Gaming ↔ Desktop switch mid-stream — flip between Gaming Mode and the desktop with Steam's normal UI and the host re-targets whatever's running without a reconnect.

This page covers the gamescope-specific choices. To get a host running on an appliance box, start from the install guide for your OS: Bazzite or SteamOS (Host).

New here? Read Security & Safe Use first — a streaming host is remote control of the machine, so keep it on a trusted LAN or VPN and require pairing.

How the host gets a gamescope

There are three models; the host picks one per session, and you rarely have to. With nothing set, a box that has gamescope session infrastructure (Bazzite, SteamOS, Nobara) gets managed; the Bazzite template ships with attach chosen instead.

  • Attach (PUNKTFUNK_GAMESCOPE_ATTACH=1) — the box owns its gamescope session and decides Gaming vs Desktop via the normal Steam UI. Game Mode stays on the box's own (physical) display; the host attaches to whatever's live and never tears it down, so switching Desktop ↔ Game is rock-solid and disconnecting leaves the box where it was. When the box is headless (no display connected) and the session is its own autologin unit, the host restarts it at the client's resolution on a mismatch; a box driving a physical display — and any foreign or bare gamescope — is streamed at its own mode.
  • Managed (the infra-detected default; force with PUNKTFUNK_GAMESCOPE_MANAGED=1) — the host takes the box's gamescope session over and relaunches it headless at the client's exact resolution and refresh — Game Mode runs on the virtual screen, physical displays drop out of it — restoring the box on idle after disconnect.
  • Bare spawn (the default on a plain distro with no gamescope session infrastructure and no gamescope already running, and the route a dedicated game launch takes unless you've forced managed or attach) — the host starts its own headless gamescope per session at the client's mode and runs the session's launch command (or PUNKTFUNK_GAMESCOPE_APP) inside it. Nothing on the box is taken over, because there is nothing to take over.

Nobara and other autologin display managers

The managed takeover has to stop the box's Gaming Mode session to free Steam — and when that session is a display-manager autologin, it has to stop the display manager too, for the length of the stream. That is a privileged operation, and the privilege is granted to one group.

Join the punktfunk group on any box you stream Game Mode from. The takeover's root helper runs for members of that group and for nobody else, so this one command is what authorizes it:

sudo usermod -aG punktfunk "$USER"   # then log out and back in

Your package created the group at install time and put nobody in it, on purpose: it also gates the usbip nodes the virtual Steam Deck pad attaches through, and writing those can present arbitrary emulated USB hardware — so joining stays a deliberate act, on a machine you trust. Skip it and nothing fails loudly. Every takeover degrades to mirroring the box's own session (below), which on a box whose panel is off reads as a black screen on every connect. The host checks this at startup on any box that will need the takeover and says so in its log; the symptom side is Game Mode: black screen on connect.

How the takeover gets that privilege depends on the display manager driving the autologin:

  • SDDM (Bazzite, SteamOS): SDDM survives having the session unit masked, so a box without the grant still streams — at the cost of SDDM relogin-looping against the takeover for the whole stream, which churns logind sessions and can starve the game.

  • plasmalogin (Nobara) and other display managers: masking is fatal there (the autologin start-limit-kills the display manager), so the host stops the display manager itself and restarts it afterwards. The packages ship that privilege: a root helper (/usr/libexec/punktfunk/pf-dm-helper, or /usr/lib/punktfunk/pf-dm-helper from the Arch package) behind its own polkit action (io.unom.punktfunk.dm-helper), invoked automatically when the plain systemctl verbs are denied. The helper only stops/restores the unit the display-manager.service symlink points at, the same class of local-seat operation these distros already authorize for their own session switcher (Nobara's os-session-select).

    Installed from a tarball, or prefer not to ship the allow_any action? Remove the .policy file and use a polkit rule scoped to your user instead (adjust the unit and user names to your box) — the host tries the plain verbs first, so the rule takes precedence:

    // /etc/polkit-1/rules.d/49-punktfunk-dm.rules
    polkit.addRule(function(action, subject) {
        if (action.id == "org.freedesktop.systemd1.manage-units" &&
            action.lookup("unit") == "plasmalogin.service" &&
            subject.user == "YOUR_USER") {
            return polkit.Result.YES;
        }
    });
    

    With no privilege path at all the host degrades safely: it attaches to the live Gaming Mode session instead (Game Mode stays on the box's display at the box's own resolution, mirrored to the client — if your monitor stays on and the stream runs at the desktop's resolution, this is what happened; check the host log for "managed takeover unavailable"). That log line now quotes the privileged path's own reason for refusing, so read it before changing anything: by far the most common one is not in the 'punktfunk' group, which the group command above fixes and neither a reinstall nor a polkit rule does. If the display-manager restart ever loses its privilege mid-restore, PUNKTFUNK_RECOVER_SESSION_CMD (see Configuration) is fired as the fallback.

    Lingering is required here, and the host turns it on for you the first time it takes the box over. Stopping the display manager ends your last login session, and without loginctl enable-linger logind stops your systemd --user manager about ten seconds later — taking the host with it, mid-stream, with the display manager down and nothing left to bring it back. If lingering can't be enabled the host refuses the takeover and degrades to attach instead (above) rather than risk that. Run sudo loginctl enable-linger "$USER" once, as the setup guides ask; loginctl disable-linger "$USER" reverts it. (A host with no login session of its own turns lingering on through the same helper, so a missing group grant surfaces here first — the log says "enabling lingering failed" and then quotes the same reason.)

    With the takeover authorized the in-stream session switch round-trips in managed mode: Steam's "Switch to Desktop" inside the streamed Game Mode returns the box to its desktop session and the stream follows it there; the desktop's "Return to Gaming Mode" switches it forward again.

Stream the screen the box is already driving

There is a fourth thing you can ask for, and it isn't a gamescope model at all: mirror the head Gaming Mode is lighting. A Gaming Mode gamescope is the DRM master of a real connector, so that head is listed by punktfunk-host list-monitors and appears in the web console under Virtual displays → Streamed screen. Pick it there, or pin it from host.env:

PUNKTFUNK_CAPTURE_MONITOR=HDMI-A-1

The host then attaches to the session's own composited output: nothing is stopped, nothing is relaunched, no mode is imposed, and what you see is exactly what is on the TV. That is the difference from managed, which deliberately takes the session over and blanks the panel.

Only the one head the session drives is listed — a nested or headless gamescope (including the per-session ones the host spawns itself) has none of its own, so the picker is empty there. Full details, including what the setting turns off, are in Stream a real monitor instead.

Session following

PUNKTFUNK_SESSION_WATCH follows a Gaming ↔ Desktop switch mid-stream — the host rebuilds the backend in place, with no reconnect. It is on by default on Bazzite/SteamOS; set 0 to disable. One host service covers both faces of the box: it streams Gaming Mode over gamescope and the desktop over its own compositor, and re-targets whichever is live on each switch.

Start the host

On an appliance box (Bazzite, SteamOS) the install guide already enables the host service for you. On any other distro running a gamescope session, just start it — the host auto-detects the live gamescope session and picks the model for it:

systemctl --user enable --now punktfunk-host

Which of the three it picked is logged per session:

journalctl --user -u punktfunk-host | grep 'gamescope sub-mode'

Then bring up The Web Console to arm pairing.

gamescope knobs

Every gamescope setting — the three models above, PUNKTFUNK_GAMESCOPE_NODE, the bare-spawn flags (APP, SPLASH, STEAM, GRAB_CURSOR), the binary override, the two HDR knobs, and PUNKTFUNK_SESSION_WATCH — lives in the host.env reference, under gamescope / session following: Configuration. Leave them unset to auto-detect; set one only to force a model.

Two are worth naming here, because the sections below turn on them:

  • PUNKTFUNK_GAMESCOPE_HDR — on by default; see HDR on gamescope.
  • PUNKTFUNK_GAMESCOPE_BIN — forces one gamescope binary. Unset, the host prefers punktfunk-gamescope on PATH and falls back to gamescope.

HDR on gamescope

Games can render HDR on a headless gamescope today, but a stock gamescope's capture output is 8-bit SDR: its PipeWire node offers only 8-bit formats, and it tone-maps the composite down before handing it over. So a stock setup streams SDR — correctly, including a correct SDR rendition of an HDR game — and there is nothing to configure. This section is the gamescope half; the rest of the chain, and what to check when a stream comes out SDR, is on HDR.

To stream real HDR you need punktfunk-gamescope: gamescope plus a small patch that adds the 10-bit BT.2020 PQ formats to that node (offered upstream as gamescope#2126). It installs under its own name and does not replace your system gamescope — your Gaming Mode keeps using that one.

  • Bazzite / Fedora Atomic — included in the Punktfunk sysext; punktfunk-sysext update gets it.
  • Fedora, Nobara and other RPM boxessudo dnf install punktfunk-gamescope from the same Punktfunk repo the host comes from.
  • Debian / Ubuntusudo apt install punktfunk-gamescope from the Punktfunk apt repo.
  • Arch — the punktfunk-gamescope package.
  • SteamOS (Steam Deck installer) — built and wired automatically by scripts/steamdeck/install.sh / update.sh.
  • NixOSservices.punktfunk.host.gamescopeHdr (default true).
  • Anything elsebash packaging/gamescope/build-punktfunk-gamescope.sh from the source tree.

HDR is attempted by default once the build is present (PUNKTFUNK_GAMESCOPE_HDR=0 forces SDR).

The build only reaches sessions the host starts itself — managed, PUNKTFUNK_GAMESCOPE_SESSION, or a bare spawn. In attach mode the running session is the box's own, started by the display manager with the distro's gamescope, so it offers neither the 10-bit formats nor the in-node cursor — but the host answers both questions by asking the installed binary, so an installed punktfunk-gamescope makes it believe the attached session has them. Attach plus that build is the combination to avoid: HDR → Linux + gamescope has what it costs you and the two ways out.

The cursor is the half this page owns. The host leaves the pointer to the compositor whenever the installed build can paint it (below) — so on an attached session, which can't, nothing draws it and the stream has no cursor at all. Commenting PUNKTFUNK_GAMESCOPE_ATTACH=1 out of the Bazzite template and letting the managed default take over fixes that along with HDR. To stay on attach, point PUNKTFUNK_GAMESCOPE_BIN at your distro's own gamescope (/usr/bin/gamescope) instead: the host goes back to compositing the cursor itself, and — since the HDR answer comes from the same binary — stops attempting HDR too.

One thing to know beyond HDR itself: the pointer rides on the same build. When the compositor paints the cursor into the capture node the host stops blending one in, which is also what frees the session to take the encoder's fastest source — a front end with no blend stage. That is why punktfunk-gamescope is worth installing even on a box where you never turn HDR on.

Known limits

These apply to the Gaming Mode (gamescope) path only; the desktop path is unaffected.

  • gamescope 3.16.22 or newer is required; 3.16.23 or newer for the Steam overlay. Below 3.16.22, headless capture can deadlock against PipeWire 1.6. Between 3.16.22 and 3.16.23 capture works, but gamescope doesn't paint the Steam overlay (Shift+Tab / the Quick Access Menu) into its capture node, so the overlay is missing from an otherwise perfect picture. Either case is logged at startup with the version found. Bazzite's and SteamOS's current gamescope is past both; this only bites if you've pinned an old one.
  • On a stock gamescope, Gaming Mode reports the wrong refresh rate — and offers no resolutions. A headless gamescope has no EDID, and upstream's headless connector advertises no display modes and no refresh rates at all. Steam's in-session display settings then show a single refresh entry and an empty resolution list, and that one entry is whatever the session was launched with — or 60 Hz if the launch flag went missing. Games that pace themselves to the display will hold themselves there, even though the stream is running at your client's full rate (the client's own fps counter keeps reading correctly, because the encoder repeats held frames — so the counter is not the thing to trust here; an in-game fps readout is). punktfunk-gamescope publishes the real mode and rate, and PUNKTFUNK_GAMESCOPE_REFRESH_RATES=60,90,120 puts more than one entry in that menu. If the host log says "the session did not start at the mode we asked for", a file in /etc/gamescope-session-plus/sessions.d/ is overriding GAMESCOPE_BIN or setting GAMESCOPECMD.
  • The performance overlay (fps / frametime / stats) needs the patched build. It is mangoapp, which gamescope draws as an external overlay — a layer upstream's capture composite has never included on any version, so on a stock gamescope you can turn the overlay on and it simply will not appear in the stream. There is no host-side substitute: the host cannot reconstruct another process's overlay window. punktfunk-gamescope paints it into the capture stream.
  • The cursor comes from the compositor when it can, and from the host otherwise. A stock gamescope leaves the pointer out of its captured image, so the host reads it separately and draws it into every frame — a full pass over the picture, and the fastest encode source cannot blend at all. punktfunk-gamescope paints the pointer into the capture instead, so the host stops redrawing it and the frame reaches the encoder untouched — but only in a session the host starts itself, not in attach mode (see HDR on gamescope above).
  • Touch arrives as a single-finger pointer. gamescope's virtual input device has no touchscreen, so the host maps a client's touchscreen to an absolute pointer: taps click exactly where you touch and drags work, but multi-touch gestures (pinch) aren't available in Gaming Mode. The desktop path has full multi-touch, and the client's other two touch modes — trackpad and direct pointer — are unaffected either way, because they send ordinary mouse events.
  • Desktop (absolute) mouse mode is unavailable. A client asking for it quietly stays captured against a Gaming Mode session, and the mouse-mode shortcut has nothing to switch to.
  • There is no clipboard. A gamescope session offers neither mechanism the host can read and write a clipboard through, so clipboard sharing does nothing in Gaming Mode, even with both of its switches on.
  • HDR needs the Punktfunk gamescope build — see HDR on gamescope above.

To stream the KDE Plasma desktop of a Steam box instead, see KDE Plasma. To bring up the web console and pair a client, see The Web Console.