Three fixes to the same mistake: deciding what the Vulkan Video backend can do
from a table in our heads rather than from the driver, and routing everything
that didn't fit to libav VAAPI — where a session loses real RFI recovery and
the cursor blend for no reason the hardware asked for.
**Capability probe, per codec AND depth.** `probe_encode_support`'s "is there
an encode queue" boolean becomes `VulkanEncodeCaps { supported, eight_bit,
ten_bit }`, answered by `vkGetPhysicalDeviceVideoCapabilitiesKHR` against the
very profile chain the session open builds. So the dispatcher's prediction
cannot disagree with reality: a capable device keeps the Vulkan path, an
incapable one routes to VAAPI BEFORE burning a failed open, and the
cursor-blend mirror stays honest for free. This is the shape the direct-SDK
NVENC path already uses for its codec GUIDs.
**AV1 10-bit.** It was excluded on a guess about driver coverage; now the
device answers. `color_config()` carries `high_bitdepth` + the BT.2020/PQ CICP
triplet in both the `StdVideoAV1ColorConfig` and the sequence-header OBU we
bit-pack ourselves — they must stay identical or the driver's frame OBUs parse
against a header we didn't write. `high_bitdepth` sits BEFORE the CICP bytes,
so getting it wrong doesn't just mislabel the depth, it puts every following
field one bit out of phase; the new test reads the packed bits back.
**Zero-CSC RGB-direct in HDR.** The EFC probe assumed BT.709 and BGRA. It now
asks for the model this session's colourimetry needs (`MODEL_YCBCR_2020` for
10-bit — the extension has always had it) and for the CAPTURED format as an
encode-source format, and the session create-info selects the matching model.
An HDR session with no pointer to composite therefore hands the captured
buffer straight to the fixed-function front end and runs no host CSC at all.
Sessions that DO composite a pointer keep the compute CSC, unchanged: the EFC
cannot blend, and that rule outranks everything.
Also: `can_encode_10bit` on AMD/Intel now reports the union of VAAPI's and
Vulkan Video's answers instead of VAAPI's alone. `open_amd_intel` tries Vulkan
first and falls back, so either one being able to encode Main10 makes the
session 10-bit-capable — answering `false` because only one of them said yes
stranded encodable HDR sessions at 8 bits.
186 lines
12 KiB
Markdown
186 lines
12 KiB
Markdown
---
|
|
title: Steam / gamescope
|
|
description: Configure a gamescope/Steam host — attach vs managed, 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](/docs/bazzite) or [SteamOS (Host)](/docs/steamos-host).
|
|
|
|
> New here? Read [Security & Safe Use](/docs/security) first — a streaming host is remote control of
|
|
> the machine, so keep it on a trusted LAN or VPN and require pairing.
|
|
|
|
## Attach vs managed
|
|
|
|
There are two mutually-exclusive models for a gamescope box; pick one. With **nothing set**, a box
|
|
that has gamescope session infrastructure (Bazzite, SteamOS, Nobara) gets **managed**; the
|
|
[Bazzite template](/docs/bazzite) 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.
|
|
|
|
### Nobara and other autologin display managers
|
|
|
|
The managed takeover has to stop the box's Gaming Mode session to free Steam. How it does that
|
|
depends on the display manager driving the autologin:
|
|
|
|
- **SDDM** (Bazzite, SteamOS): handled automatically — no setup.
|
|
- **plasmalogin** (Nobara) and other display managers: the host must stop the display manager
|
|
itself for the length of the stream and restart it afterwards, which needs privilege. The
|
|
packages ship that privilege: a root helper (`/usr/libexec/punktfunk/pf-dm-helper`) behind its
|
|
own polkit action (`io.unom.punktfunk.dm-helper`), invoked automatically when the plain
|
|
`systemctl` verbs are denied — no setup. 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:
|
|
|
|
```js
|
|
// /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"). If the display-manager
|
|
restart ever loses its privilege mid-restore, `PUNKTFUNK_RECOVER_SESSION_CMD` (see
|
|
[Configuration](/docs/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.
|
|
|
|
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.
|
|
|
|
## 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:
|
|
|
|
```sh
|
|
systemctl --user enable --now punktfunk-host
|
|
```
|
|
|
|
Then bring up [The Web Console](/docs/web-console) to arm pairing.
|
|
|
|
## gamescope knobs
|
|
|
|
The gamescope-specific settings in `host.env`. Leave them unset to auto-detect; set one only to force
|
|
a model. See the full [Configuration reference](/docs/configuration) for every other knob.
|
|
|
|
| Setting | Values | Meaning |
|
|
|---|---|---|
|
|
| `PUNKTFUNK_GAMESCOPE_ATTACH` | `1` | **Attach** model: the box owns its gamescope session (on its own display); the host captures whatever's live and never tears it down. On a **headless** box the box-owned autologin session is restarted at the client's resolution on a mismatch; a box driving a physical display, and any foreign/bare gamescope, streams at its own mode. |
|
|
| `PUNKTFUNK_GAMESCOPE_MANAGED` | `1` | **Managed** model (the default where session infra is detected): the host takes the box's gamescope over and relaunches it headless at the client's exact mode, restoring on idle. |
|
|
| `PUNKTFUNK_GAMESCOPE_SESSION` | `steam` | The host owns a `gamescope-session-plus` (Steam) session at the client's mode — a headless appliance with no physical session running. |
|
|
| `PUNKTFUNK_GAMESCOPE_NODE` | `auto` · node id | Discover and capture a **running** gamescope's PipeWire node at a fixed mode. Do **not** combine with `SESSION`. |
|
|
| `PUNKTFUNK_GAMESCOPE_APP` | command | For an ad-hoc bare-gamescope session, the nested command to run (e.g. `vkcube`). |
|
|
| `PUNKTFUNK_SESSION_WATCH` | `1` · `0` | Follow a Gaming ↔ Desktop switch mid-stream (rebuild in place, no reconnect). On by default on Bazzite/SteamOS; set `0` to disable. |
|
|
| `PUNKTFUNK_GAMESCOPE_HDR` | `1` · `0` *(default off)* | Allow HDR (10-bit BT.2020 PQ) sessions. Needs `punktfunk-gamescope` — see below. |
|
|
| `PUNKTFUNK_GAMESCOPE_SDR_NITS` | e.g. `400` | On an HDR session, how bright SDR content (the desktop, the Steam overlay, an SDR game) is inside the PQ container. gamescope's default is 400. |
|
|
| `PUNKTFUNK_GAMESCOPE_BIN` | path | Force a specific gamescope binary. Otherwise the host prefers `punktfunk-gamescope` on `PATH`, then `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.
|
|
|
|
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](https://github.com/ValveSoftware/gamescope/issues/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.
|
|
- **Arch / SteamOS** — the `punktfunk-gamescope` package.
|
|
- **NixOS** — `services.punktfunk.host.gamescopeHdr = true;` (it also sets the env var below).
|
|
- **Anything else** — `bash packaging/gamescope/build-punktfunk-gamescope.sh` from the source tree.
|
|
|
|
Then turn it on:
|
|
|
|
```sh
|
|
PUNKTFUNK_GAMESCOPE_HDR=1
|
|
```
|
|
|
|
and check what the host thinks it can do:
|
|
|
|
```sh
|
|
punktfunk-host hdr-probe
|
|
```
|
|
|
|
A session goes HDR when **all** of: the host allows it (`PUNKTFUNK_10BIT`, on by default, plus the
|
|
knob above), the resolved gamescope offers the 10-bit formats, the client advertises 10-bit
|
|
decoding with HDR enabled in its settings, the codec is HEVC or AV1 (never H.264), and the GPU
|
|
encodes Main10. Anything missing and the session streams 8-bit SDR — decided **before** the stream
|
|
starts, so you never get a mislabelled picture. The client overlay shows `10-bit · HDR (BT.2020
|
|
PQ)` when it worked.
|
|
|
|
Two things to know:
|
|
|
|
- **SDR content rides the same PQ stream.** The desktop, the Steam overlay and SDR games are mapped
|
|
into the HDR container at `PUNKTFUNK_GAMESCOPE_SDR_NITS` (400 by default). If white looks too
|
|
bright or too dim on your TV, that is the knob.
|
|
- **On AMD and Intel, HDR follows what the GPU can encode.** The host asks the driver whether it
|
|
can encode your codec at 10 bits and picks the fast path when it can — which current AMD and
|
|
Intel GPUs do for HEVC, and newer ones for AV1 too. On a GPU that declines, the session still
|
|
streams HDR through a slower path, but the mouse pointer gamescope leaves out of its capture
|
|
can't be drawn back in there. `punktfunk-host hdr-probe` reports what your box answered.
|
|
|
|
## Known limits
|
|
|
|
These apply to the **Gaming Mode (gamescope)** path only; the desktop path is unaffected.
|
|
|
|
- **gamescope 3.16.22 or newer is required.** Older versions can deadlock during capture. Bazzite's
|
|
and SteamOS's current gamescope is fine; this only bites if you've pinned an old one.
|
|
- **The mouse cursor isn't included in the captured image** — a gamescope limitation for now.
|
|
- **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.
|
|
- **HDR needs the punktfunk gamescope build.** A stock gamescope's capture output is 8-bit SDR, so
|
|
sessions stream SDR — correctly, including SDR versions of HDR games. Install
|
|
`punktfunk-gamescope` (gamescope plus a small patch that teaches its capture node the 10-bit
|
|
BT.2020 PQ formats) and set `PUNKTFUNK_GAMESCOPE_HDR=1`, and a 10-bit-capable client streams
|
|
true HDR10. See [HDR on gamescope](#hdr-on-gamescope) below.
|
|
|
|
To stream the KDE Plasma desktop of a Steam box instead, see [KDE Plasma](/docs/kde). To bring up the
|
|
web console and pair a client, see [The Web Console](/docs/web-console).
|