Field triage on the Bazzite .41 box (canary 0.28.0-0.ci12912, Deck client) surfaced two independent black-screen mechanisms. Both are fixed here; both were verified against the live box.
Symptom: connect, see Big Picture, then black — while host, encoder, gamescope and client all report healthy. The client stats gave it away: 60 fps decoding at 0.1 Mb/s against a 130 Mb/s target — a perfectly delivered stream of black frames.
Cause: Bazzite's hhd-ui (Handheld Daemon overlay) sets GAMESCOPE_NO_FOCUS=1 on its window once at init and never clears it; show/hide runs over the STEAM_OVERLAY protocol (MangoHud sets the same atom). No gamescope implements the atom — not upstream at our pin, not Bazzite's ba148 fork (checked by strings). hhd-ui crash-loops under a headless takeover (Electron, SIGTRAP every few minutes) and stamps Steam's appid 769, so each respawn's mapped-but-unpainted fullscreen window was an ordinary focus candidate for the focused app — and steamcompmgr picked it over real Big Picture. On the box: GAMESCOPE_FOCUSED_WINDOW named the hhd-ui window with GAMESCOPE_NO_FOCUS(CARDINAL)=1 sitting right on it; killing hhd-ui flipped focus back and the picture returned the same second (0.2 → 15.9 Mb/s).
Fix: wire the atom exactly like GAMESCOPE_EXTERNAL_OVERLAY — read at map, PropertyNotify-tracked with MakeFocusDirty, skipped by both focus-candidate collectors (X11 and XDG). Compositing and appID untouched: a NO_FOCUS window still paints if the baselayer protocol brings it into view; it is only barred from being chosen. The full 0001..0008 series git am-applies clean on the bare 5fb8dce4 pin. Banner +pfhdr5 → +pfhdr6 (no new capability — the bump is so a field banner distinguishes a build that can lose its composite this way from one that cannot; README's ledger gains the missing +pfhdr5 row and the reconciled bump rule).
2. pf-capture: the truncated first attempt no longer latches the sticky downgrades
This is the pf-capture sweep's verified-but-unfixed bug #6, and .41 demonstrated the damage live: the retry loop's deliberately short 2.5 s first-frame attempt expired at connect (gamescope cold start — frames arrive ~5 s after negotiation), and the timeout diagnosis latched its verdicts process-wide and permanently: HDR capture off for the source, and/or the raw-dmabuf / EGL→CUDA offers off. Every later session in that host process ran silently degraded to SDR and/or CPU capture.
Fix: the truncated attempt is declared provisional end to end. New Capturer::next_frame_within_provisional (default delegates; only the portal capturer distinguishes), and the timeout classification is split into the pure classify_first_frame_timeout + timeout_convicts: a provisional expiry names the same suspect in the error text but latches nothing — only full-length attempts hand down negotiation verdicts. Forced PUNKTFUNK_ZEROCOPY=1 behavior is unchanged (keeps erroring loudly, never latched).
Tests:pf_capture::linux::first_frame_timeout_tests (4) pin the latch policy and the classification precedence. Gates run in the amd64 CI image: cargo clippy -p pf-capture --all-targets --locked -D warnings, cargo test -p pf-capture first_frame_timeout (4/4), cargo check -p punktfunk-host.
Not in this PR (same field session, separate follow-ups)
The 21:09 incident where pf-dm-helper refused the sddm restart against an in-flight suspend transaction and left the box with no graphical session, plus the restore path logging INFO "restored the display manager" immediately after two Access-denied systemctl attempts.
The sleep-inhibitor acquisition failure (InteractiveAuthorizationRequired) under a passive viewer.
GRID 2 refusing to start on .41 — reproduced with punktfunk fully out of the loop (stock game mode, every Proton version); instrumented with PROTON_LOG=1 on the box, still under investigation.
Field triage on the Bazzite `.41` box (canary `0.28.0-0.ci12912`, Deck client) surfaced two independent black-screen mechanisms. Both are fixed here; both were verified against the live box.
## 1. gamescope patch 0008 (`+pfhdr6`): honor `GAMESCOPE_NO_FOCUS`
**Symptom:** connect, see Big Picture, then black — while host, encoder, gamescope and client all report healthy. The client stats gave it away: **60 fps decoding at 0.1 Mb/s against a 130 Mb/s target** — a perfectly delivered stream of black frames.
**Cause:** Bazzite's hhd-ui (Handheld Daemon overlay) sets `GAMESCOPE_NO_FOCUS=1` on its window once at init and never clears it; show/hide runs over the `STEAM_OVERLAY` protocol (MangoHud sets the same atom). **No gamescope implements the atom** — not upstream at our pin, not Bazzite's ba148 fork (checked by `strings`). hhd-ui crash-loops under a headless takeover (Electron, SIGTRAP every few minutes) and stamps Steam's appid 769, so each respawn's mapped-but-unpainted fullscreen window was an ordinary focus candidate for the focused app — and steamcompmgr picked it over real Big Picture. On the box: `GAMESCOPE_FOCUSED_WINDOW` named the hhd-ui window with `GAMESCOPE_NO_FOCUS(CARDINAL)=1` sitting right on it; **killing hhd-ui flipped focus back and the picture returned the same second** (0.2 → 15.9 Mb/s).
**Fix:** wire the atom exactly like `GAMESCOPE_EXTERNAL_OVERLAY` — read at map, PropertyNotify-tracked with `MakeFocusDirty`, skipped by both focus-candidate collectors (X11 and XDG). Compositing and `appID` untouched: a NO_FOCUS window still paints if the baselayer protocol brings it into view; it is only barred from being *chosen*. The full 0001..0008 series `git am`-applies clean on the bare `5fb8dce4` pin. Banner `+pfhdr5` → `+pfhdr6` (no new capability — the bump is so a field banner distinguishes a build that can lose its composite this way from one that cannot; README's ledger gains the missing `+pfhdr5` row and the reconciled bump rule).
## 2. pf-capture: the truncated first attempt no longer latches the sticky downgrades
This is the pf-capture sweep's verified-but-unfixed **bug #6**, and `.41` demonstrated the damage live: the retry loop's deliberately short 2.5 s first-frame attempt expired at connect (gamescope cold start — frames arrive ~5 s after negotiation), and the timeout diagnosis latched its verdicts **process-wide and permanently**: HDR capture off for the source, and/or the raw-dmabuf / EGL→CUDA offers off. Every later session in that host process ran silently degraded to SDR and/or CPU capture.
**Fix:** the truncated attempt is declared provisional end to end. New `Capturer::next_frame_within_provisional` (default delegates; only the portal capturer distinguishes), and the timeout classification is split into the pure `classify_first_frame_timeout` + `timeout_convicts`: a provisional expiry names the same suspect in the error text but latches nothing — only full-length attempts hand down negotiation verdicts. Forced `PUNKTFUNK_ZEROCOPY=1` behavior is unchanged (keeps erroring loudly, never latched).
**Tests:** `pf_capture::linux::first_frame_timeout_tests` (4) pin the latch policy and the classification precedence. Gates run in the amd64 CI image: `cargo clippy -p pf-capture --all-targets --locked -D warnings`, `cargo test -p pf-capture first_frame_timeout` (4/4), `cargo check -p punktfunk-host`.
## Not in this PR (same field session, separate follow-ups)
- The 21:09 incident where `pf-dm-helper` refused the sddm restart against an in-flight suspend transaction and left the box with no graphical session, plus the restore path logging INFO "restored the display manager" immediately after two Access-denied `systemctl` attempts.
- The sleep-inhibitor acquisition failure (`InteractiveAuthorizationRequired`) under a passive viewer.
- GRID 2 refusing to start on `.41` — reproduced with punktfunk fully out of the loop (stock game mode, every Proton version); instrumented with `PROTON_LOG=1` on the box, still under investigation.
The pipeline retry loop deliberately shortens its first attempt's first-frame wait to 2.5s so a
stream bound during a gamescope re-init fails over quickly. But the portal capturer's timeout
diagnosis treated EVERY expiry as a verdict: it latched whichever offer it implicated — HDR
capture off for the source, the raw-dmabuf offer off, the EGL→CUDA offer off — process-wide and
permanently, when the attempt was truncated by design and a gamescope cold start routinely
delivers nothing inside that window while accepting every offer a few seconds later (observed on
.41: pid 1962 hit the expiry at connect and every later session in that process ran silently
degraded). This is bug #6 from the pf-capture sweep, verified then and unfixed until now.
The truncated attempt is now declared PROVISIONAL end to end: a new
`Capturer::next_frame_within_provisional` (default: delegates) lets the retry loop say "this
budget is the schedule, not a verdict", and the portal capturer's timeout classification — split
out as the pure `classify_first_frame_timeout` + `timeout_convicts`, with tests — names the same
suspect in the error text but latches nothing unless the expired budget was full-length.
Patch 0008: honor GAMESCOPE_NO_FOCUS in steamcompmgr's focus selection. hhd (Handheld Daemon)
sets the atom once at init on its hhd-ui overlay window and never clears it; MangoHud sets it
too; show/hide for these clients runs over the STEAM_OVERLAY protocol. NOTHING consumed the atom
— not upstream gamescope, not Bazzite's fork (checked ba148 by strings) — so a
mapped-but-unpainted hhd-ui window (it crash-loops under a headless punktfunk takeover and remaps
on every respawn, stamping Steam's appid 769) was an ordinary focus candidate, and steamcompmgr
picked it over Big Picture. The composite, and the stream fed from it, went black while every
health signal stayed green: on .41 the client sat decoding 60 fps at 0.1 Mb/s of black,
GAMESCOPE_FOCUSED_WINDOW named the hhd-ui window with GAMESCOPE_NO_FOCUS(CARDINAL)=1 on it, and
killing hhd-ui brought the picture back the same second.
The patch wires the atom exactly like GAMESCOPE_EXTERNAL_OVERLAY — read at map,
PropertyNotify-tracked with MakeFocusDirty, skipped by both focus-candidate collectors (X11 and
XDG) — and touches neither compositing nor appID, so a NO_FOCUS window still paints if the
baselayer protocol brings it into view; it is only barred from being CHOSEN. Applies cleanly on
the full 0001..0008 series from the bare 5fb8dce4 pin (verified with git am).
Banner +pfhdr5 → +pfhdr6, PKGBUILD 3.16.25.pfhdr6-1; README gains the 0008 row, the missing
+pfhdr5 ledger row, and the reconciled bump rule (a bugfix bumps the level only when field triage
must read the difference off a box's banner — 0007's crash-loop, 0008's lost composite).
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Field triage on the Bazzite
.41box (canary0.28.0-0.ci12912, Deck client) surfaced two independent black-screen mechanisms. Both are fixed here; both were verified against the live box.1. gamescope patch 0008 (
+pfhdr6): honorGAMESCOPE_NO_FOCUSSymptom: connect, see Big Picture, then black — while host, encoder, gamescope and client all report healthy. The client stats gave it away: 60 fps decoding at 0.1 Mb/s against a 130 Mb/s target — a perfectly delivered stream of black frames.
Cause: Bazzite's hhd-ui (Handheld Daemon overlay) sets
GAMESCOPE_NO_FOCUS=1on its window once at init and never clears it; show/hide runs over theSTEAM_OVERLAYprotocol (MangoHud sets the same atom). No gamescope implements the atom — not upstream at our pin, not Bazzite's ba148 fork (checked bystrings). hhd-ui crash-loops under a headless takeover (Electron, SIGTRAP every few minutes) and stamps Steam's appid 769, so each respawn's mapped-but-unpainted fullscreen window was an ordinary focus candidate for the focused app — and steamcompmgr picked it over real Big Picture. On the box:GAMESCOPE_FOCUSED_WINDOWnamed the hhd-ui window withGAMESCOPE_NO_FOCUS(CARDINAL)=1sitting right on it; killing hhd-ui flipped focus back and the picture returned the same second (0.2 → 15.9 Mb/s).Fix: wire the atom exactly like
GAMESCOPE_EXTERNAL_OVERLAY— read at map, PropertyNotify-tracked withMakeFocusDirty, skipped by both focus-candidate collectors (X11 and XDG). Compositing andappIDuntouched: a NO_FOCUS window still paints if the baselayer protocol brings it into view; it is only barred from being chosen. The full 0001..0008 seriesgit am-applies clean on the bare5fb8dce4pin. Banner+pfhdr5→+pfhdr6(no new capability — the bump is so a field banner distinguishes a build that can lose its composite this way from one that cannot; README's ledger gains the missing+pfhdr5row and the reconciled bump rule).2. pf-capture: the truncated first attempt no longer latches the sticky downgrades
This is the pf-capture sweep's verified-but-unfixed bug #6, and
.41demonstrated the damage live: the retry loop's deliberately short 2.5 s first-frame attempt expired at connect (gamescope cold start — frames arrive ~5 s after negotiation), and the timeout diagnosis latched its verdicts process-wide and permanently: HDR capture off for the source, and/or the raw-dmabuf / EGL→CUDA offers off. Every later session in that host process ran silently degraded to SDR and/or CPU capture.Fix: the truncated attempt is declared provisional end to end. New
Capturer::next_frame_within_provisional(default delegates; only the portal capturer distinguishes), and the timeout classification is split into the pureclassify_first_frame_timeout+timeout_convicts: a provisional expiry names the same suspect in the error text but latches nothing — only full-length attempts hand down negotiation verdicts. ForcedPUNKTFUNK_ZEROCOPY=1behavior is unchanged (keeps erroring loudly, never latched).Tests:
pf_capture::linux::first_frame_timeout_tests(4) pin the latch policy and the classification precedence. Gates run in the amd64 CI image:cargo clippy -p pf-capture --all-targets --locked -D warnings,cargo test -p pf-capture first_frame_timeout(4/4),cargo check -p punktfunk-host.Not in this PR (same field session, separate follow-ups)
pf-dm-helperrefused the sddm restart against an in-flight suspend transaction and left the box with no graphical session, plus the restore path logging INFO "restored the display manager" immediately after two Access-deniedsystemctlattempts.InteractiveAuthorizationRequired) under a passive viewer..41— reproduced with punktfunk fully out of the loop (stock game mode, every Proton version); instrumented withPROTON_LOG=1on the box, still under investigation.