Mid-stream "Switch to Desktop" worked; "Return to Gaming Mode" never came up — Steam sat on its "Switch to Desktop…" modal forever, and only a reboot fixed it. Field-hit on the .41 bazzite VM.
The mask outlived the window it defends
A managed takeover runtime-masks the box's gamescope-session-plus@* unit so its session supervisor cannot restart it underneath our Steam. The only unmask_unit() call was in do_restore_tv_session, scheduled on client disconnect — so for the whole stream the box could not re-enter its own game mode.
One load-bearing comment was wrong, which is why this was invisible
mask_unit's doc said that on images whose sddm helper execs the session script directly, "the relogin never touches the unit, so the mask blocks nothing". Reading the script on the box says otherwise. /usr/bin/gamescope-session-plus — the one /etc/sddm/wayland-session execs — ends with:
So the mask stops that unit, and with it every entry into game mode — including the user's own deliberate return. What it fails to stop is the relogin loop: sddm keeps trying regardless, which is why stopping the DM is the real storm defense. Both are true at once, and conflating them hid this bug for ten days. The doc is corrected in place.
Two more measured facts that made this so hard to pin down:
systemctl --user unmaskdoes not clear a --runtime mask (verified on .41: still masked-runtime afterwards). Only unmask --runtime does.
The mask symlink lives in /run/user/1000/systemd/user/ — tmpfs. Hence "it works right after a reboot", and hence no non-reboot remedy ever stuck.
The fix
The mask's sound lifetime is shorter than the takeover's: it ends the moment the box stops being ours. The mid-stream session watcher already detects exactly that, so it now lifts the mask on a confirmed switch to a desktop session — ahead of the compositor_for_kind arm, because a switch we cannot follow still has to unbar the return.
Masking itself is unchanged; the belt-and-braces window it defends is real.
Why not "on the transition into Gaming"? That trigger cannot work: the watcher can never observe Gaming while the mask is on, because the mask is precisely what stops that session from starting — it would wait on the thing it is meant to enable. So the lift fires on the way out, which always precedes the return. Gaming and None deliberately do not lift: a takeover's own managed session reads as Gaming, and one momentarily down between relaunches reads as None — lifting on either would void the mask for the whole stream, in exactly the SDDM-storm window it exists for.
A second, worse leak fixed on the way
honor_session_select_switch consumed STOPPED_AUTOLOGIN — the only record of what carries a mask — without unmasking. Under a DM-stop takeover the disconnect restore then found an empty list and lifted nothing, so that mask outlived not just the stream but the boot. It is also what silently broke that path's own step 1, since the DM's autologin heads back into game mode through precisely that unit.
The lift is idempotent, keeps the restart list intact (the disconnect restore still owes those units a start), and every hand-back path now routes through it.
Verification
pf-vdisplay clippy + 138/0 tests on Linux; punktfunk-host clippy --all-targets clean on Linux; rustfmt clean.
New decision-table test for switch_ends_mask_window.
New #[ignore]d test driving real systemctl --user against a throwaway unit: masked → survives Gaming/None → lifted by a desktop switch → restart list intact → idempotent. cargo test -p pf-vdisplay -- --ignored the_mask_comes_off
Proven non-vacuous by planting "Gaming also lifts", which fails it on the during-stream assert.
⏳On-glass repro still owed. The full mid-stream Desktop→Game switch with the patched binary was not run: it needs a packaged build, because a scratch path voids the KWin per-exe grant and black-screens the Desktop leg the repro depends on. Strong indirect evidence the hook fires — .41's own journal carries session watcher: mid-stream switch … from=Gaming to=DesktopKde at 16:34 on 2026-08-10, the exact hook point, three lines above the new call.
Mid-stream "Switch to Desktop" worked; **"Return to Gaming Mode" never came up** — Steam sat on its "Switch to Desktop…" modal forever, and only a reboot fixed it. Field-hit on the .41 bazzite VM.
## The mask outlived the window it defends
A managed takeover runtime-masks the box's `gamescope-session-plus@*` unit so its session supervisor cannot restart it underneath our Steam. The only `unmask_unit()` call was in `do_restore_tv_session`, scheduled on client **disconnect** — so for the whole stream the box could not re-enter its own game mode.
## One load-bearing comment was wrong, which is why this was invisible
`mask_unit`'s doc said that on images whose sddm helper execs the session script directly, "the relogin never touches the unit, so **the mask blocks nothing**". Reading the script on the box says otherwise. `/usr/bin/gamescope-session-plus` — the one `/etc/sddm/wayland-session` execs — *ends* with:
```sh
systemctl --user --wait start gamescope-session-plus@${CLIENT}.service &
wait
```
So the mask stops that unit, and with it **every** entry into game mode — including the user's own deliberate return. What it fails to stop is the **relogin loop**: sddm keeps trying regardless, which is why stopping the DM is the real storm defense. Both are true at once, and conflating them hid this bug for ten days. The doc is corrected in place.
Two more measured facts that made this so hard to pin down:
- `systemctl --user unmask` **does not clear a `--runtime` mask** (verified on .41: still `masked-runtime` afterwards). Only `unmask --runtime` does.
- The mask symlink lives in `/run/user/1000/systemd/user/` — tmpfs. Hence "it works right after a reboot", and hence no non-reboot remedy ever stuck.
## The fix
The mask's sound lifetime is **shorter than the takeover's**: it ends the moment the box stops being ours. The mid-stream session watcher already detects exactly that, so it now lifts the mask on a confirmed switch to a desktop session — **ahead of** the `compositor_for_kind` arm, because a switch we cannot follow still has to unbar the return.
Masking itself is unchanged; the belt-and-braces window it defends is real.
> **Why not "on the transition into Gaming"?** That trigger cannot work: the watcher can never observe `Gaming` while the mask is on, because the mask is precisely what stops that session from starting — it would wait on the thing it is meant to enable. So the lift fires on the way **out**, which always precedes the return. `Gaming` and `None` deliberately do not lift: a takeover's own managed session reads as `Gaming`, and one momentarily down between relaunches reads as `None` — lifting on either would void the mask for the whole stream, in exactly the SDDM-storm window it exists for.
## A second, worse leak fixed on the way
`honor_session_select_switch` consumed `STOPPED_AUTOLOGIN` — the only record of what carries a mask — **without unmasking**. Under a DM-stop takeover the disconnect restore then found an empty list and lifted nothing, so that mask outlived not just the stream but the **boot**. It is also what silently broke that path's own step 1, since the DM's autologin heads back into game mode through precisely that unit.
The lift is idempotent, keeps the restart list intact (the disconnect restore still owes those units a `start`), and every hand-back path now routes through it.
## Verification
- `pf-vdisplay` clippy + **138/0** tests on Linux; `punktfunk-host` clippy `--all-targets` clean on Linux; rustfmt clean.
- New decision-table test for `switch_ends_mask_window`.
- New `#[ignore]`d test driving **real `systemctl --user`** against a throwaway unit: masked → survives `Gaming`/`None` → lifted by a desktop switch → restart list intact → idempotent.
`cargo test -p pf-vdisplay -- --ignored the_mask_comes_off`
- **Proven non-vacuous** by planting "Gaming also lifts", which fails it on the during-stream assert.
⏳ **On-glass repro still owed.** The full mid-stream Desktop→Game switch with the patched binary was not run: it needs a *packaged* build, because a scratch path voids the KWin per-exe grant and black-screens the Desktop leg the repro depends on. Strong indirect evidence the hook fires — .41's own journal carries `session watcher: mid-stream switch … from=Gaming to=DesktopKde` at 16:34 on 2026-08-10, the exact hook point, three lines above the new call.
A managed takeover runtime-masks the box's `gamescope-session-plus@*` unit so its
session supervisor cannot restart it underneath our Steam. The only unmask ran in
`do_restore_tv_session`, on client disconnect — so for the whole stream the mask
stayed on, and it silently barred the door the user was most likely to walk
through next.
`mask_unit`'s own doc said the mask "blocks nothing" on images whose sddm helper
execs the session script directly. That is half right, and the half it gets wrong
is this bug: on f43 bazzite-deck the script's last act is
systemctl --user --wait start gamescope-session-plus@${CLIENT}.service
(verified on the .41 VM). What the mask fails to stop is the RELOGIN LOOP — sddm
keeps trying regardless, which is why stopping the DM is the real defense. What it
very much does stop is the unit, and with it every entry into game mode, including
the user's own deliberate "Return to Gaming Mode" after a mid-stream switch to the
desktop. Steam then sits on its "Switch to Desktop…" modal forever. `--runtime`
lives in tmpfs, so a reboot cleared it — hence "it works right after a reboot" —
and a plain `unmask` does not (measured: still `masked-runtime`).
So the mask's sound lifetime is shorter than the takeover's: it ends the moment the
box stops being ours. The mid-stream session watcher already detects exactly that,
so it now lifts the mask on a confirmed switch to a desktop session — ahead of the
`compositor_for_kind` arm, because a switch we cannot follow still has to unbar the
return. `Gaming` and `None` deliberately do not lift: a takeover's own managed
session reads as `Gaming` and one momentarily down between relaunches reads as
`None`, and lifting on either would void the mask for the whole stream, in exactly
the SDDM-storm window it exists for.
Fixes a second, worse leak on the way: `honor_session_select_switch` consumed
`STOPPED_AUTOLOGIN` — the only record of what carries a mask — without unmasking,
so under a DM-stop takeover the disconnect restore found an empty list and lifted
nothing. That mask outlived not just the stream but the boot. It is also what let
that path's own step 1 work at all, since the DM's autologin heads back into game
mode through precisely this unit.
The lift is idempotent, keeps the restart list intact (the disconnect restore still
owes those units a `start`), and every hand-back path now routes through it.
Verified on Linux: `switch_ends_mask_window` decision table, plus an ignored
end-to-end test driving real `systemctl --user` (masked → survives Gaming/None →
lifted by a desktop switch → restart list intact → idempotent). Proven non-vacuous
by planting "Gaming also lifts", which fails it on the during-stream assert.
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.
Mid-stream "Switch to Desktop" worked; "Return to Gaming Mode" never came up — Steam sat on its "Switch to Desktop…" modal forever, and only a reboot fixed it. Field-hit on the .41 bazzite VM.
The mask outlived the window it defends
A managed takeover runtime-masks the box's
gamescope-session-plus@*unit so its session supervisor cannot restart it underneath our Steam. The onlyunmask_unit()call was indo_restore_tv_session, scheduled on client disconnect — so for the whole stream the box could not re-enter its own game mode.One load-bearing comment was wrong, which is why this was invisible
mask_unit's doc said that on images whose sddm helper execs the session script directly, "the relogin never touches the unit, so the mask blocks nothing". Reading the script on the box says otherwise./usr/bin/gamescope-session-plus— the one/etc/sddm/wayland-sessionexecs — ends with:So the mask stops that unit, and with it every entry into game mode — including the user's own deliberate return. What it fails to stop is the relogin loop: sddm keeps trying regardless, which is why stopping the DM is the real storm defense. Both are true at once, and conflating them hid this bug for ten days. The doc is corrected in place.
Two more measured facts that made this so hard to pin down:
systemctl --user unmaskdoes not clear a--runtimemask (verified on .41: stillmasked-runtimeafterwards). Onlyunmask --runtimedoes./run/user/1000/systemd/user/— tmpfs. Hence "it works right after a reboot", and hence no non-reboot remedy ever stuck.The fix
The mask's sound lifetime is shorter than the takeover's: it ends the moment the box stops being ours. The mid-stream session watcher already detects exactly that, so it now lifts the mask on a confirmed switch to a desktop session — ahead of the
compositor_for_kindarm, because a switch we cannot follow still has to unbar the return.Masking itself is unchanged; the belt-and-braces window it defends is real.
A second, worse leak fixed on the way
honor_session_select_switchconsumedSTOPPED_AUTOLOGIN— the only record of what carries a mask — without unmasking. Under a DM-stop takeover the disconnect restore then found an empty list and lifted nothing, so that mask outlived not just the stream but the boot. It is also what silently broke that path's own step 1, since the DM's autologin heads back into game mode through precisely that unit.The lift is idempotent, keeps the restart list intact (the disconnect restore still owes those units a
start), and every hand-back path now routes through it.Verification
pf-vdisplayclippy + 138/0 tests on Linux;punktfunk-hostclippy--all-targetsclean on Linux; rustfmt clean.switch_ends_mask_window.#[ignore]d test driving realsystemctl --useragainst a throwaway unit: masked → survivesGaming/None→ lifted by a desktop switch → restart list intact → idempotent.cargo test -p pf-vdisplay -- --ignored the_mask_comes_off⏳ On-glass repro still owed. The full mid-stream Desktop→Game switch with the patched binary was not run: it needs a packaged build, because a scratch path voids the KWin per-exe grant and black-screens the Desktop leg the repro depends on. Strong indirect evidence the hook fires — .41's own journal carries
session watcher: mid-stream switch … from=Gaming to=DesktopKdeat 16:34 on 2026-08-10, the exact hook point, three lines above the new call.A managed takeover runtime-masks the box's `gamescope-session-plus@*` unit so its session supervisor cannot restart it underneath our Steam. The only unmask ran in `do_restore_tv_session`, on client disconnect — so for the whole stream the mask stayed on, and it silently barred the door the user was most likely to walk through next. `mask_unit`'s own doc said the mask "blocks nothing" on images whose sddm helper execs the session script directly. That is half right, and the half it gets wrong is this bug: on f43 bazzite-deck the script's last act is systemctl --user --wait start gamescope-session-plus@${CLIENT}.service (verified on the .41 VM). What the mask fails to stop is the RELOGIN LOOP — sddm keeps trying regardless, which is why stopping the DM is the real defense. What it very much does stop is the unit, and with it every entry into game mode, including the user's own deliberate "Return to Gaming Mode" after a mid-stream switch to the desktop. Steam then sits on its "Switch to Desktop…" modal forever. `--runtime` lives in tmpfs, so a reboot cleared it — hence "it works right after a reboot" — and a plain `unmask` does not (measured: still `masked-runtime`). So the mask's sound lifetime is shorter than the takeover's: it ends the moment the box stops being ours. The mid-stream session watcher already detects exactly that, so it now lifts the mask on a confirmed switch to a desktop session — ahead of the `compositor_for_kind` arm, because a switch we cannot follow still has to unbar the return. `Gaming` and `None` deliberately do not lift: a takeover's own managed session reads as `Gaming` and one momentarily down between relaunches reads as `None`, and lifting on either would void the mask for the whole stream, in exactly the SDDM-storm window it exists for. Fixes a second, worse leak on the way: `honor_session_select_switch` consumed `STOPPED_AUTOLOGIN` — the only record of what carries a mask — without unmasking, so under a DM-stop takeover the disconnect restore found an empty list and lifted nothing. That mask outlived not just the stream but the boot. It is also what let that path's own step 1 work at all, since the DM's autologin heads back into game mode through precisely this unit. The lift is idempotent, keeps the restart list intact (the disconnect restore still owes those units a `start`), and every hand-back path now routes through it. Verified on Linux: `switch_ends_mask_window` decision table, plus an ignored end-to-end test driving real `systemctl --user` (masked → survives Gaming/None → lifted by a desktop switch → restart list intact → idempotent). Proven non-vacuous by planting "Gaming also lifts", which fails it on the during-stream assert.