A compositor pin silently vetoed dedicated game sessions — and a mid-bring-up mode switch was killing GNOME outright #121

Merged
enricobuehler merged 1 commits from worktree-dedicated-session-pin-and-recovery into main 2026-08-08 15:38:40 +00:00
Owner

Three faults found while triaging "I can't launch games via the dedicated game session any more" on a GNOME host.

The pin vetoed game_session=dedicated, silently

PUNKTFUNK_COMPOSITOR is documented as which backend to drive, but it also quietly discarded the dedicated route: resolve_compositor gated it on !overridden and logged nothing either way. A host whose pin was a forgotten validation leftover therefore kept displaying "dedicated" in the console while every launch landed in the desktop instead — for 30 days on the box that surfaced this, the only evidence being the absence of a log line.

The pin still wins, since it is the operator's explicit hand-set knob, but it now says so and names itself.

The same pin left the recovery hook unreachable

A pin puts its backend into available() unconditionally and skips apply_session_env's XDG_CURRENT_DESKTOP scrub, so pick_compositor could never return None — the one place try_recover_session() is called from. A pinned host whose gnome-shell had segfaulted spent every connect on 8 doomed RemoteDesktop.CreateSession: ServiceUnknown retries while the operator's configured PUNKTFUNK_RECOVER_SESSION_CMD sat unreachable behind that arm.

Liveness is now read on both paths, and a pin aimed at a dead session takes the recovery exit with an error naming the pin. Compositor::needs_live_session() exempts gamescope, which stands its own session up — pinning it on a headless box stays supported, and a test guards that.

A mode switch accepted before bring-up finished took down the whole desktop

A switch the control task accepted before the pipeline existed was served the long way round: build at the now-stale mode, then immediately rebuild at the new one in the stream loop. That burns a display create, capture attach and encoder open on every such connect — and because the rebuild is deliberately create-before-drop, it stands up two Mutter RecordVirtual monitors ~400 ms apart, which segfaults mutter 50.4 inside meta_monitor_manager_rebuild:

14:58:30.219  mode switch accepted mode=3200x2400@120   ← queued; NO pipeline exists yet
14:58:32.445  punktfunk/1 virtual display mode=2752x2064 ← built at the STALE mode
14:58:32.464  gnome-shell: Added virtual monitor Meta-0
14:58:33.489  rebuilding pipeline for mode switch new_mode=3200x2400
14:58:33.503  gnome-shell: Added virtual monitor Meta-1
14:58:33.574  gnome-shell SEGFAULT

The blast radius is the whole box: gnome-shell dies, the GDM greeter takes the seat, and GDM auto-login is once-per-boot so the desktop never returns on its own. The game launched moments earlier loses its display and segfaults in libX11 — which makes it look like the game failed and sends triage down the wrong path. Reproduced three times with byte-identical stacks, including on a fresh 0.26.0 canary.

Bring-up now drains the reconfig channel and builds once at the newest queued mode, so the second monitor is never created. Inline path only — a prepared pipeline already exists at the old mode, so adopting there would only desync the variable from the display. The H2/H3 correction ack the replaced rebuild would have sent is carried over.

Note this removes the trigger, not the mutter bug; anything else that adds two virtual monitors in quick succession will still kill it.

Verification

Gated at full CI parity on a real Linux host (x86_64), not the Mac — punktfunk-host does not build on macOS at all, so a Mac check proves nothing here:

  • cargo clippy --workspace --all-targets --locked -- -D warnings — clean
  • cargo fmt --all --check — clean
  • punktfunk-host + pf-vdisplay suites — green, including two new tests covering the pin/liveness matrix

The gate was proved non-vacuous against a planted compile_error! first; an earlier run had looked green while never compiling the host crate at all.

Confirmed on glass afterwards: the dedicated route fired for the first time on that box (dedicated game session — routing to a headless gamescope spawn at the client mode, compositor="gamescope"), and it worked with no desktop session live at all — which is the point.

Three faults found while triaging "I can't launch games via the dedicated game session any more" on a GNOME host. ## The pin vetoed `game_session=dedicated`, silently `PUNKTFUNK_COMPOSITOR` is documented as *which backend to drive*, but it also quietly discarded the dedicated route: `resolve_compositor` gated it on `!overridden` and logged nothing either way. A host whose pin was a forgotten validation leftover therefore kept displaying "dedicated" in the console while every launch landed in the desktop instead — for 30 days on the box that surfaced this, the only evidence being the *absence* of a log line. The pin still wins, since it is the operator's explicit hand-set knob, but it now says so and names itself. ## The same pin left the recovery hook unreachable A pin puts its backend into `available()` unconditionally **and** skips `apply_session_env`'s `XDG_CURRENT_DESKTOP` scrub, so `pick_compositor` could never return `None` — the one place `try_recover_session()` is called from. A pinned host whose gnome-shell had segfaulted spent every connect on 8 doomed `RemoteDesktop.CreateSession: ServiceUnknown` retries while the operator's configured `PUNKTFUNK_RECOVER_SESSION_CMD` sat unreachable behind that arm. Liveness is now read on both paths, and a pin aimed at a dead session takes the recovery exit with an error naming the pin. `Compositor::needs_live_session()` exempts gamescope, which stands its own session up — pinning it on a headless box stays supported, and a test guards that. ## A mode switch accepted before bring-up finished took down the whole desktop A switch the control task accepted before the pipeline existed was served the long way round: build at the now-stale mode, then immediately rebuild at the new one in the stream loop. That burns a display create, capture attach and encoder open on every such connect — and because the rebuild is deliberately create-before-drop, it stands up two Mutter `RecordVirtual` monitors ~400 ms apart, which segfaults mutter 50.4 inside `meta_monitor_manager_rebuild`: ``` 14:58:30.219 mode switch accepted mode=3200x2400@120 ← queued; NO pipeline exists yet 14:58:32.445 punktfunk/1 virtual display mode=2752x2064 ← built at the STALE mode 14:58:32.464 gnome-shell: Added virtual monitor Meta-0 14:58:33.489 rebuilding pipeline for mode switch new_mode=3200x2400 14:58:33.503 gnome-shell: Added virtual monitor Meta-1 14:58:33.574 gnome-shell SEGFAULT ``` The blast radius is the whole box: gnome-shell dies, the GDM greeter takes the seat, and GDM auto-login is once-per-boot so the desktop never returns on its own. The game launched moments earlier loses its display and segfaults in libX11 — which makes it look like the *game* failed and sends triage down the wrong path. Reproduced three times with byte-identical stacks, including on a fresh `0.26.0` canary. Bring-up now drains the `reconfig` channel and builds **once** at the newest queued mode, so the second monitor is never created. Inline path only — a prepared pipeline already exists at the old mode, so adopting there would only desync the variable from the display. The H2/H3 correction ack the replaced rebuild would have sent is carried over. Note this removes the *trigger*, not the mutter bug; anything else that adds two virtual monitors in quick succession will still kill it. ## Verification Gated at full CI parity on a real Linux host (x86_64), not the Mac — `punktfunk-host` does not build on macOS at all, so a Mac check proves nothing here: - `cargo clippy --workspace --all-targets --locked -- -D warnings` — clean - `cargo fmt --all --check` — clean - `punktfunk-host` + `pf-vdisplay` suites — green, including two new tests covering the pin/liveness matrix The gate was proved non-vacuous against a planted `compile_error!` first; an earlier run had looked green while never compiling the host crate at all. Confirmed on glass afterwards: the dedicated route fired for the first time on that box (`dedicated game session — routing to a headless gamescope spawn at the client mode`, `compositor="gamescope"`), and it worked with no desktop session live at all — which is the point.
enricobuehler added 1 commit 2026-08-08 15:37:48 +00:00
fix(host): a compositor pin silently vetoed dedicated game sessions
ci / bun-nix (pull_request) Successful in 26s
ci / web (pull_request) Successful in 1m4s
apple / swift (pull_request) Successful in 1m46s
apple / screenshots (pull_request) Skipped
ci / docs-site (pull_request) Successful in 1m59s
ci / rust-arm64 (pull_request) Successful in 2m31s
android / android (pull_request) Successful in 7m23s
ci / rust (pull_request) Successful in 8m5s
f23e0df64c
`PUNKTFUNK_COMPOSITOR` is documented as "which backend to drive", but it also
quietly discarded `game_session=dedicated`: `resolve_compositor` gated the
dedicated route on `!overridden` and logged nothing either way. A host whose pin
was a forgotten validation leftover therefore went on displaying "dedicated" in
the console while every launch landed in the desktop instead — for 30 days on the
box that surfaced this, the only evidence being the ABSENCE of a log line.

The pin still wins, since it is the operator's explicit hand-set knob, but it now
says so and names itself.

Two further holes the same triage turned up:

- The pin put its backend into `available()` unconditionally AND skipped
  `apply_session_env`'s `XDG_CURRENT_DESKTOP` scrub, so `pick_compositor` could
  never return `None` — the one place `try_recover_session()` is called from. A
  pinned host whose gnome-shell had segfaulted therefore spent every connect on 8
  doomed `RemoteDesktop.CreateSession: ServiceUnknown` retries while the
  operator's configured `PUNKTFUNK_RECOVER_SESSION_CMD` sat unreachable behind
  that arm. Liveness is now read on both paths, and a pin aimed at a dead session
  takes the recovery exit with an error naming the pin. `needs_live_session()`
  exempts gamescope, which stands its own session up — pinning it on a headless
  box stays supported.

- A mode switch accepted before the pipeline existed was served the long way
  round: build at the now-stale mode, then immediately rebuild at the new one in
  the stream loop. That burns a display create, capture attach and encoder open
  on every such connect, and because the rebuild is deliberately
  create-before-drop it stands up two Mutter `RecordVirtual` monitors ~400 ms
  apart — which segfaults mutter 50.4 inside `meta_monitor_manager_rebuild` and
  takes down the whole desktop session, along with the game just launched into
  it (so the GAME looks like what crashed). Bring-up now adopts the newest queued
  mode and builds once, carrying over the H2/H3 correction ack that the replaced
  rebuild would have sent.

Verified on a real Linux host (192.168.1.21, x86_64): `cargo clippy --workspace
--all-targets --locked -- -D warnings`, `cargo fmt --all --check` and the
punktfunk-host + pf-vdisplay test suites all clean. The gate was proved
non-vacuous against a planted `compile_error!`.
enricobuehler merged commit d402e9b996 into main 2026-08-08 15:38:40 +00:00
enricobuehler deleted branch worktree-dedicated-session-pin-and-recovery 2026-08-08 15:38:40 +00:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: unom/punktfunk#121