Field report from a Deck user: "no matter what I select the stats overlay is stuck showing as detailed" — cured by restarting the client app.
What was happening
The console (Gaming Mode, and therefore Decky) builds its window and its run loop once and streams every session through them. The loop took its stats tier from the settings snapshot read at process start, and there was no channel back into it — OverlayAction has no settings-changed variant.
So the console's own settings screen wrote the chosen tier to the file and redrew its row, the choice looked taken, and every stream kept the tier the process happened to start on. "Detailed" wasn't special: it was whatever was on disk when the app launched, so anyone who had been on Detailed stayed there. Only a restart re-read it.
crates/pf-presenter/src/run.rs — let mut stats_verbosity = opts.stats_verbosity in run_inner, which browse mode enters once for the process's life
The desktop shells were never affected. GTK/WinUI spawn a session process per stream, so each stream resolves settings for itself. Only the persistent console latches — worth knowing before anyone tries to reproduce this in desktop mode.
The fix
The tier now rides SessionParams per launch, so browse mode adopts what this launch resolved and the start-of-process value only seeds the loop until the first stream.
Two choices worth calling out in review:
Adoption sits in the ActionOutcome::Start arm, not StreamState::new. The codec-fallback retry rebuilds stream state from a clone of those params mid-stream; doing it in the constructor would snap the overlay back every time a session fell down the codec ladder, undoing a cycle the user had just made with the chord.
Resolving per launch rather than per process means a profile bound to a host can now move the tier in console mode, which closes part of the documented P4 gap for free.
The --stats rule (a floor: lifts Off to Normal, demotes nothing) was written out three times and is now one tested helper, session_main::stats_tier.
Deliberately not in scope
Touch mode, mouse mode, invert scroll, shortcut inhibit, present priority, vsync, VRR, match-window, render scale and window size are still latched at console start and still have this same bug. Each needs the presenter's input and swapchain models rebuilt per launch — the P4 work the comment in console.rs already flags. Expect more "setting X does nothing until I restart" reports from the Deck until that lands; the comment there now says so explicitly.
params.cursor_forward stays deliberately pinned to the latched mouse mode and must remain that way — a profile flipping it mid-console produces a stream with no visible cursor at all.
Verification
pf-client-core doesn't build on macOS, so this was gated in the pf-lxcheck2 container on linux/amd64, re-run after rebasing onto 28b66330:
cargo clippy --all-targets -D warnings for pf-client-core, pf-presenter, punktfunk-client-session
plain cargo build as well (--all-targets compiles cfg(test) and hides "dead outside tests")
cargo test: 188 pass in pf-client-core, 2 new in punktfunk-client-session
cargo fmt --all --check clean
clippy proven non-vacuous — the run's log shows both crates actually re-Checking, not a cached Finished
⚠ The new tests cover the --stats rule and the resolve, not the run-loop adoption itself: run_inner needs a live SDL window, so that hop has no unit test. The field being non-Option is what forces a future construction site to think about it.
Not yet exercised on a Deck in Gaming Mode — worth one on-glass check that picking a tier between two streams takes effect on the second.
Field report from a Deck user: *"no matter what I select the stats overlay is stuck showing as detailed"* — cured by restarting the client app.
## What was happening
The console (Gaming Mode, and therefore Decky) builds its window and its run loop **once** and streams every session through them. The loop took its stats tier from the settings snapshot read at process start, and there was no channel back into it — `OverlayAction` has no settings-changed variant.
So the console's own settings screen wrote the chosen tier to the file and redrew its row, the choice looked taken, and every stream kept the tier the process happened to start on. "Detailed" wasn't special: it was whatever was on disk when the app launched, so anyone who had been on Detailed stayed there. Only a restart re-read it.
- `clients/session/src/console.rs` — `settings_at_start = trust::Settings::load()` → `SessionOpts`
- `crates/pf-presenter/src/run.rs` — `let mut stats_verbosity = opts.stats_verbosity` in `run_inner`, which browse mode enters once for the process's life
**The desktop shells were never affected.** GTK/WinUI spawn a session *process* per stream, so each stream resolves settings for itself. Only the persistent console latches — worth knowing before anyone tries to reproduce this in desktop mode.
## The fix
The tier now rides `SessionParams` per launch, so browse mode adopts what *this* launch resolved and the start-of-process value only seeds the loop until the first stream.
Two choices worth calling out in review:
- **Adoption sits in the `ActionOutcome::Start` arm, not `StreamState::new`.** The codec-fallback retry rebuilds stream state from a clone of those params mid-stream; doing it in the constructor would snap the overlay back every time a session fell down the codec ladder, undoing a cycle the user had just made with the chord.
- **Resolving per launch rather than per process means a profile bound to a host can now move the tier in console mode**, which closes part of the documented P4 gap for free.
The `--stats` rule (a floor: lifts Off to Normal, demotes nothing) was written out three times and is now one tested helper, `session_main::stats_tier`.
## Deliberately not in scope
Touch mode, mouse mode, invert scroll, shortcut inhibit, present priority, vsync, VRR, match-window, render scale and window size are **still latched at console start and still have this same bug**. Each needs the presenter's input and swapchain models rebuilt per launch — the P4 work the comment in `console.rs` already flags. Expect more "setting X does nothing until I restart" reports from the Deck until that lands; the comment there now says so explicitly.
`params.cursor_forward` stays deliberately pinned to the *latched* mouse mode and must remain that way — a profile flipping it mid-console produces a stream with no visible cursor at all.
## Verification
`pf-client-core` doesn't build on macOS, so this was gated in the `pf-lxcheck2` container on `linux/amd64`, re-run after rebasing onto `28b66330`:
- `cargo clippy --all-targets -D warnings` for `pf-client-core`, `pf-presenter`, `punktfunk-client-session`
- plain `cargo build` as well (`--all-targets` compiles `cfg(test)` and hides "dead outside tests")
- `cargo test`: 188 pass in `pf-client-core`, 2 new in `punktfunk-client-session`
- `cargo fmt --all --check` clean
- clippy proven non-vacuous — the run's log shows both crates actually re-`Checking`, not a cached `Finished`
⚠ The new tests cover the `--stats` rule and the resolve, **not** the run-loop adoption itself: `run_inner` needs a live SDL window, so that hop has no unit test. The field being non-`Option` is what forces a future construction site to think about it.
Not yet exercised on a Deck in Gaming Mode — worth one on-glass check that picking a tier between two streams takes effect on the second.
Field report: "no matter what I select the stats overlay is stuck showing as
detailed" on the Deck, cured by restarting the client app.
The console (Gaming Mode, and therefore Decky) builds its window and its run loop
ONCE and streams every session through them, and the loop took its stats tier from
the settings snapshot read at process start. Its own settings screen writes the
chosen tier to the file and redraws its row, so the choice looked taken while every
stream kept the tier the process happened to start on — Detailed for anyone who had
been on Detailed. Only a restart re-read it. The desktop shells were never affected:
they spawn a session process per stream, which resolves settings for itself.
The tier now rides `SessionParams` per launch, so browse mode adopts what THIS launch
resolved and the start-of-process value only seeds the loop until the first stream.
Two things fall out of resolving per launch rather than per process: a profile bound
to a host can finally move the tier in console mode (part of the documented P4 gap),
and the adoption sits in the `Start` arm rather than `StreamState::new`, so the
codec-fallback retry can't snap the overlay back and undo an in-stream cycle.
The `--stats` rule (a floor that lifts Off to Normal and demotes nothing) was written
out three times and is now one tested helper. The rest of the console's latched
presentation tier — touch and mouse model, shortcut inhibit, match-window, render
scale — is unchanged and still needs the models rebuilt per launch.
Gate: clippy --all-targets -D warnings, plain build, and tests for pf-client-core,
pf-presenter and punktfunk-client-session, all green in pf-lxcheck2 (linux/amd64);
clippy proven non-vacuous by touching the four edited files. cargo fmt --all --check
clean.
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 report from a Deck user: "no matter what I select the stats overlay is stuck showing as detailed" — cured by restarting the client app.
What was happening
The console (Gaming Mode, and therefore Decky) builds its window and its run loop once and streams every session through them. The loop took its stats tier from the settings snapshot read at process start, and there was no channel back into it —
OverlayActionhas no settings-changed variant.So the console's own settings screen wrote the chosen tier to the file and redrew its row, the choice looked taken, and every stream kept the tier the process happened to start on. "Detailed" wasn't special: it was whatever was on disk when the app launched, so anyone who had been on Detailed stayed there. Only a restart re-read it.
clients/session/src/console.rs—settings_at_start = trust::Settings::load()→SessionOptscrates/pf-presenter/src/run.rs—let mut stats_verbosity = opts.stats_verbosityinrun_inner, which browse mode enters once for the process's lifeThe desktop shells were never affected. GTK/WinUI spawn a session process per stream, so each stream resolves settings for itself. Only the persistent console latches — worth knowing before anyone tries to reproduce this in desktop mode.
The fix
The tier now rides
SessionParamsper launch, so browse mode adopts what this launch resolved and the start-of-process value only seeds the loop until the first stream.Two choices worth calling out in review:
ActionOutcome::Startarm, notStreamState::new. The codec-fallback retry rebuilds stream state from a clone of those params mid-stream; doing it in the constructor would snap the overlay back every time a session fell down the codec ladder, undoing a cycle the user had just made with the chord.The
--statsrule (a floor: lifts Off to Normal, demotes nothing) was written out three times and is now one tested helper,session_main::stats_tier.Deliberately not in scope
Touch mode, mouse mode, invert scroll, shortcut inhibit, present priority, vsync, VRR, match-window, render scale and window size are still latched at console start and still have this same bug. Each needs the presenter's input and swapchain models rebuilt per launch — the P4 work the comment in
console.rsalready flags. Expect more "setting X does nothing until I restart" reports from the Deck until that lands; the comment there now says so explicitly.params.cursor_forwardstays deliberately pinned to the latched mouse mode and must remain that way — a profile flipping it mid-console produces a stream with no visible cursor at all.Verification
pf-client-coredoesn't build on macOS, so this was gated in thepf-lxcheck2container onlinux/amd64, re-run after rebasing onto28b66330:cargo clippy --all-targets -D warningsforpf-client-core,pf-presenter,punktfunk-client-sessioncargo buildas well (--all-targetscompilescfg(test)and hides "dead outside tests")cargo test: 188 pass inpf-client-core, 2 new inpunktfunk-client-sessioncargo fmt --all --checkcleanChecking, not a cachedFinished⚠ The new tests cover the
--statsrule and the resolve, not the run-loop adoption itself:run_innerneeds a live SDL window, so that hop has no unit test. The field being non-Optionis what forces a future construction site to think about it.Not yet exercised on a Deck in Gaming Mode — worth one on-glass check that picking a tier between two streams takes effect on the second.