diff --git a/crates/pf-vdisplay/src/vdisplay/routing.rs b/crates/pf-vdisplay/src/vdisplay/routing.rs index f4ced37f..2b96b586 100644 --- a/crates/pf-vdisplay/src/vdisplay/routing.rs +++ b/crates/pf-vdisplay/src/vdisplay/routing.rs @@ -111,9 +111,15 @@ struct OperatorGamescope { #[cfg(target_os = "linux")] fn operator_gamescope() -> &'static OperatorGamescope { OPERATOR_GAMESCOPE.get_or_init(|| { + // Explicit-off grammar, NOT a presence test. These two used to be `var_os(..).is_some()`, + // which read `PUNKTFUNK_GAMESCOPE_ATTACH=0` as ATTACH ON — the exact opposite of what the + // line says, and of every other knob on this host (`env_on` is shared for that reason). + // Anyone turning a shipped `=1` off does it the way the rest of the file works, and the + // rung this feeds outranks a dedicated game session, so a silent inversion here costs the + // client its own display for the whole stream. let ov = with_env_lock(|| OperatorGamescope { - managed: std::env::var_os("PUNKTFUNK_GAMESCOPE_MANAGED").is_some(), - attach: std::env::var_os("PUNKTFUNK_GAMESCOPE_ATTACH").is_some(), + managed: pf_host_config::env_on("PUNKTFUNK_GAMESCOPE_MANAGED").unwrap_or(false), + attach: pf_host_config::env_on("PUNKTFUNK_GAMESCOPE_ATTACH").unwrap_or(false), node: std::env::var("PUNKTFUNK_GAMESCOPE_NODE") .ok() .filter(|v| !v.is_empty()), diff --git a/docs-site/content/docs/bazzite.md b/docs-site/content/docs/bazzite.md index e31ea247..cc4a365a 100644 --- a/docs-site/content/docs/bazzite.md +++ b/docs-site/content/docs/bazzite.md @@ -161,21 +161,27 @@ The only settings that matter (GPU zero-copy is on by default): ```sh PUNKTFUNK_VIDEO_SOURCE=virtual # GPU zero-copy (dmabuf → CUDA → NVENC) is ON by default; auto-falls back to CPU. Set =0 to force CPU. -PUNKTFUNK_GAMESCOPE_ATTACH=1 # Gaming Mode = attach to the box's own session — SDR, and no cursor (see below) ``` ### Gaming Mode: attach vs managed -For Gaming Mode there are two models (pick one; the shipped default is **attach**): +For Gaming Mode there are two models. The template forces **neither** — the host picks per connect, +and on Bazzite (which ships `gamescope-session-plus`) that is **managed**: -- **Attach** (`PUNKTFUNK_GAMESCOPE_ATTACH=1`, the template's default) — the **box** owns its - gamescope session on its own display, and the host attaches to whatever's live without ever - tearing it down (on a headless box, a box-owned autologin session is restarted at the client's - resolution on a mismatch; with a display connected it streams at the box's own mode). Switching - Desktop ↔ Game is rock-solid. -- **Managed** (`PUNKTFUNK_GAMESCOPE_MANAGED=1`, and remove the attach line) — the host takes the - box's gamescope over and relaunches it **headless** at the *client's* exact resolution and - refresh — Game Mode on the virtual screen — restoring the box on idle. +- **Managed** (what you get by default here) — the host takes the box's gamescope over and + relaunches it **headless** at the *client's* exact resolution and refresh — Game Mode on the + virtual screen — restoring the box on idle. This is the model that gives the client a display of + its **own**, and the only one under which a game launched from a client's library gets a + dedicated session. It needs the [`punktfunk` group](#allow-controller-input): the takeover stops + the display manager for the length of the stream, and without that grant it cannot. +- **Attach** (`PUNKTFUNK_GAMESCOPE_ATTACH=1`) — the **box** owns its gamescope session on its own + display, and the host attaches to whatever's live without ever tearing it down (on a headless + box, a box-owned autologin session is restarted at the client's resolution on a mismatch; with a + display connected it streams at the box's own mode). Switching Desktop ↔ Game is rock-solid, and + the cost is that a box with a screen attached serves the client a **mirror** of that screen + rather than its own display. Setting it also outranks a dedicated game session. + +`=0` turns the attach override off, the same as removing the line. Full treatment: [Steam / gamescope → How the host gets a gamescope](/docs/gamescope#how-the-host-gets-a-gamescope). @@ -227,14 +233,17 @@ These apply to the **Gaming Mode (gamescope)** path; the KDE Desktop path is una headless capture can deadlock; between the two, capture works but the Steam overlay (Shift+Tab / the Quick Access Menu) is never painted into the captured node. Bazzite's current gamescope is past both; this only bites if you've pinned an old one. -- **The template pins attach, and that costs you both the cursor and HDR.** The sysext ships the - `punktfunk-gamescope` build, but it only reaches a session the host starts itself — and the - `host.env` template above sets `PUNKTFUNK_GAMESCOPE_ATTACH=1`, where the live session is - Bazzite's own stock gamescope. Comment that line out and let the managed default take over: you - get the compositor-drawn pointer and real HDR. To stay on attach instead, set - `PUNKTFUNK_GAMESCOPE_HDR=0` and `PUNKTFUNK_GAMESCOPE_BIN=/usr/bin/gamescope`. Why each half - breaks: [gamescope → Known limits](/docs/gamescope#known-limits) for the cursor, +- **Forcing attach costs you the cursor, HDR and your own display.** The sysext ships the + `punktfunk-gamescope` build, but it only reaches a session the host starts itself — under + `PUNKTFUNK_GAMESCOPE_ATTACH=1` the live session is Bazzite's own stock gamescope. The managed + default gets you the compositor-drawn pointer, real HDR and a display of the client's own. If you + deliberately stay on attach, also set `PUNKTFUNK_GAMESCOPE_HDR=0` and + `PUNKTFUNK_GAMESCOPE_BIN=/usr/bin/gamescope`. Why each half breaks: + [gamescope → Known limits](/docs/gamescope#known-limits) for the cursor, [HDR → Linux + gamescope](/docs/hdr#linux--gamescope) for the failed connect. + ⚠ Older templates set `PUNKTFUNK_GAMESCOPE_ATTACH=1` for you — if you copied one, delete that + line from `~/.config/punktfunk/host.env`, because an upgrade never rewrites a file you already + have. Those are the two that bite on Bazzite. The full set — touch, mouse modes, the clipboard — is on [gamescope → Known limits](/docs/gamescope#known-limits). diff --git a/docs-site/content/docs/configuration.md b/docs-site/content/docs/configuration.md index 1c14dbd6..b7c4ea52 100644 --- a/docs-site/content/docs/configuration.md +++ b/docs-site/content/docs/configuration.md @@ -87,7 +87,7 @@ the full picture (and [Bazzite](/docs/bazzite) for that distro's specifics). | Setting | Values | Meaning | |---|---|---| -| `PUNKTFUNK_GAMESCOPE_ATTACH` | `1` | **Attach** model: the box owns its gamescope session on its own display (you switch Gaming ↔ Desktop with the Steam UI); the host just 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_ATTACH` | `1` · `0` *(unset = auto)* | **Attach** model: the box owns its gamescope session on its own display (you switch Gaming ↔ Desktop with the Steam UI); the host just 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 — i.e. the client is served a **mirror**, not a display of its own. Setting this also outranks a dedicated game session. No template ships it set; `=0` is the same as leaving it out. | | `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 resolution — Game Mode on the virtual screen — restoring the box on idle. | | `PUNKTFUNK_GAMESCOPE_SESSION` | `steam` | The host owns a `gamescope-session-plus` (Steam) session at the client's mode (headless appliance; no physical session running). | | `PUNKTFUNK_GAMESCOPE_NODE` | `auto` · node id | Discover + capture a **running** gamescope's PipeWire node at a fixed mode. Do **not** combine with `SESSION`. | diff --git a/docs-site/content/docs/gamescope.md b/docs-site/content/docs/gamescope.md index 4109fa91..d799157f 100644 --- a/docs-site/content/docs/gamescope.md +++ b/docs-site/content/docs/gamescope.md @@ -18,8 +18,8 @@ from the install guide for your OS: [Bazzite](/docs/bazzite) or [SteamOS (Host)] ## 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](/docs/bazzite) ships with **attach** chosen instead. +set** — which is what every shipped template does — a box that has gamescope session infrastructure +(Bazzite, SteamOS, Nobara) gets **managed**. - **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; @@ -208,8 +208,8 @@ 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](/docs/bazzite) and letting the managed +the stream has no cursor at all. Removing +`PUNKTFUNK_GAMESCOPE_ATTACH=1` from your `host.env` 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. diff --git a/docs-site/content/docs/hdr.md b/docs-site/content/docs/hdr.md index bdfb47bf..5c0265f8 100644 --- a/docs-site/content/docs/hdr.md +++ b/docs-site/content/docs/hdr.md @@ -76,12 +76,13 @@ available, and offers it. The attached session can't answer that negotiation, so with no picture, the host latches an SDR downgrade for the rest of its life, and the next connect streams — in SDR. -That is exactly what the [Bazzite](/docs/bazzite) template ships: it pins attach *and* the sysext -installs `punktfunk-gamescope`. Either comment `PUNKTFUNK_GAMESCOPE_ATTACH=1` out and let the managed -default take over (you get HDR and the compositor-drawn cursor), or stay on attach and set -`PUNKTFUNK_GAMESCOPE_HDR=0` so the failed attempt never happens. Staying on attach also leaves the -stream with no cursor; [HDR on gamescope](/docs/gamescope#hdr-on-gamescope) has the fix for that -half. +That combination bites on [Bazzite](/docs/bazzite), where the sysext installs `punktfunk-gamescope` +alongside a stock session gamescope. No template pins attach any more, so the managed default gets +you HDR and the compositor-drawn cursor — but an older template did, and an upgrade never rewrites a +`host.env` you already have, so check yours for `PUNKTFUNK_GAMESCOPE_ATTACH=1` and delete the line. +If you deliberately stay on attach, set `PUNKTFUNK_GAMESCOPE_HDR=0` so the failed attempt never +happens. Staying on attach also leaves the stream with no cursor; +[HDR on gamescope](/docs/gamescope#hdr-on-gamescope) has the fix for that half. SDR content rides the same PQ container — the desktop, the Steam overlay, an SDR game — mapped in at `PUNKTFUNK_GAMESCOPE_SDR_NITS` (gamescope's own default is 400). That is the knob when white looks diff --git a/packaging/bazzite/README.md b/packaging/bazzite/README.md index 6e0310c4..93924310 100644 --- a/packaging/bazzite/README.md +++ b/packaging/bazzite/README.md @@ -261,9 +261,6 @@ PUNKTFUNK_VIDEO_SOURCE=virtual # PUNKTFUNK_ZEROCOPY=0 #RUST_LOG=info - -# Gaming Mode = ATTACH: the box owns its gamescope session; the host captures + follows it. -PUNKTFUNK_GAMESCOPE_ATTACH=1 ``` **What each knob means and why these are the Bazzite defaults:** @@ -272,7 +269,7 @@ PUNKTFUNK_GAMESCOPE_ATTACH=1 |---|---|---| | *(no compositor / no anchors)* | — | The host **auto-detects** the live session per connect (Gaming Mode gamescope vs the KDE desktop) and follows switches mid-stream; a `systemctl --user` service inherits the right `XDG_RUNTIME_DIR` and the host derives the bus itself. Pinning `PUNKTFUNK_COMPOSITOR` or hardcoding uid-1000 anchors only breaks this — leave them out. | | `PUNKTFUNK_VIDEO_SOURCE` | `virtual` | Create a per-client virtual output at the client's exact WxH@Hz (the flagship "native resolution, no scaling" mode), vs. `portal` which captures an existing monitor. | -| `PUNKTFUNK_GAMESCOPE_ATTACH` | `1` | Gaming Mode model: the **box** owns its gamescope session; the host attaches to whatever's live and never tears it down. Swap for `PUNKTFUNK_GAMESCOPE_MANAGED=1` to have the host relaunch the gaming session headless at the **client's** exact mode instead (see the template's comments). | +| `PUNKTFUNK_GAMESCOPE_ATTACH` | *(unset)* | Gaming Mode model. Left unset the host picks one per connect, which on Bazzite is **managed**: it relaunches the gaming session headless at the **client's** exact mode, so the client gets a display of its own. Set `=1` to force **attach** instead — the box keeps its session and the client is served a mirror of the box's own screen at the box's mode (see the template's comments). | | `PUNKTFUNK_ZEROCOPY` | `on` *(default)* | GPU zero-copy capture (dmabuf → CUDA → NVENC), on by default. Falls back to CPU automatically if unavailable; set `0` to force the CPU path. | | `RUST_LOG` | (commented) | Uncomment `RUST_LOG=info` for verbose logs while debugging. | diff --git a/packaging/bazzite/host.env b/packaging/bazzite/host.env index eb58e3c8..75b1371d 100644 --- a/packaging/bazzite/host.env +++ b/packaging/bazzite/host.env @@ -20,19 +20,29 @@ PUNKTFUNK_VIDEO_SOURCE=virtual # PUNKTFUNK_COMPOSITOR=kwin|mutter|wlroots|gamescope # PUNKTFUNK_INPUT_BACKEND=libei|wlr|gamescope|uinput # -# GAME MODE = ATTACH (the box owns its session; the host follows). The box decides whether it's in -# Steam Gaming Mode or a Desktop — you switch with the normal Steam UI / "Switch to Desktop". The -# host just ATTACHES to whatever's live and captures it; it never tears the session down. So -# switching Desktop<->Game is rock-solid, and when you disconnect the box STAYS in its current -# mode — reconnecting drops you right back where you were. On a resolution mismatch the host -# restarts the box's own game-mode session at the CLIENT's resolution (a foreign/bare gamescope -# instead streams at its own mode). -PUNKTFUNK_GAMESCOPE_ATTACH=1 +# GAME MODE: nothing here forces a model — the host picks one per connect. On this box (Bazzite +# ships gamescope-session-plus) that is the MANAGED model: on connect the host takes the box's +# gaming session over and relaunches it headless at the CLIENT's exact resolution, then restores +# the box's own session on a debounced idle. That is what gives the client a display of its OWN +# instead of a copy of the TV, and it is also what lets a game launched from a client's library get +# a dedicated session to itself. # -# Opt OUT to the MANAGED model instead (host tears the box's gamescope down on connect and launches -# its OWN at the CLIENT's exact resolution; restores on a debounced idle). Client-mode-following, but -# it does not coexist with a box-owned game-mode session — pick one: -# PUNKTFUNK_GAMESCOPE_MANAGED=1 # (and remove PUNKTFUNK_GAMESCOPE_ATTACH above) +# ⚠ The managed takeover has to stop the display manager for the length of the stream, and that is +# privileged: it works for members of the `punktfunk` group and nobody else. Join it once — +# `sudo usermod -aG punktfunk "$USER"`, then log out and back in. Skip it and the takeover cannot +# stop SDDM, which relogin-loops against it for the whole stream and can starve the game. The host +# checks at startup and says so in its log. +# +# Opt IN to the ATTACH model if you would rather the BOX keep ownership: the host captures whatever +# gamescope is live and never tears it down, so Desktop<->Game switching is rock-solid and the box +# stays in its current mode when you disconnect. The cost is that a box driving a physical display +# streams at the BOX's own mode, MIRRORED — the client does not get its own display — and this +# override outranks a dedicated game session too. Remove the line to go back to auto (`=0` also +# turns it off; any other value is on): +# PUNKTFUNK_GAMESCOPE_ATTACH=1 +# +# Force the MANAGED model on a box where the auto-detection finds no session infrastructure: +# PUNKTFUNK_GAMESCOPE_MANAGED=1 # # Follow a Gaming<->Desktop switch MID-STREAM (rebuild the backend in place, no reconnect). This is # ON BY DEFAULT on Bazzite/SteamOS (the host detects the platform); set =0 to disable it: