From 71c1970b933a9257eaee5b29d557d9e4ddc14706 Mon Sep 17 00:00:00 2001 From: enricobuehler Date: Wed, 12 Aug 2026 15:19:42 +0200 Subject: [PATCH] fix(client/console): a stats tier picked between streams reached nothing until a restart MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- clients/session/src/console.rs | 26 ++++++----- clients/session/src/main.rs | 66 +++++++++++++++++++++++++--- crates/pf-client-core/src/session.rs | 13 ++++++ crates/pf-presenter/src/run.rs | 13 ++++++ 4 files changed, 101 insertions(+), 17 deletions(-) diff --git a/clients/session/src/console.rs b/clients/session/src/console.rs index 1ba24b6b..8e60d50b 100644 --- a/clients/session/src/console.rs +++ b/clients/session/src/console.rs @@ -13,7 +13,7 @@ //! (portrait paths starting with `/` load from disk), the GPU-only dev path. use crate::session_main::{ - arg_flag, arg_value, fullscreen_mode, parse_host_port, session_params, window_pos, + arg_flag, arg_value, fullscreen_mode, parse_host_port, session_params, stats_tier, window_pos, }; use pf_client_core::gamepad::is_steam_deck; use pf_client_core::{discovery, library, trust, wol}; @@ -141,11 +141,18 @@ pub fn run(target: Option<&str>) -> u8 { let json_status = arg_flag("--json-status"); let settings_at_start = trust::Settings::load(); // The console's window and its input models are built ONCE, from the global defaults, and - // live across every launch — so the presentation-tier fields below (stats tier, touch and - // mouse model, shortcut inhibit, match-window, render scale) are latched here and a per-host - // profile cannot move them in this mode. Everything the HOST is told (mode, bitrate, codec, - // audio, pad) is re-resolved per launch and does honor the binding. Closing that gap means - // rebuilding the presenter's models per launch — profiles P4 territory, not P0. + // live across every launch — so the presentation-tier fields below (touch and mouse model, + // shortcut inhibit, match-window, render scale) are latched here and a per-host profile + // cannot move them in this mode. Everything the HOST is told (mode, bitrate, codec, audio, + // pad) is re-resolved per launch and does honor the binding. Closing the rest of that gap + // means rebuilding the presenter's models per launch — profiles P4 territory, not P0. + // + // ⚠ The STATS TIER used to be latched here too, and that was a bug people hit: the console's + // own settings screen writes the tier to the file and redraws its row, so the choice looked + // taken while every stream kept the tier the process started on — "no matter what I select + // the overlay is stuck on Detailed", cured only by restarting the app. It now rides + // `SessionParams` per launch (`stats_verbosity`), so the value below only seeds the loop + // until the first stream. Anything else moved off this snapshot has to travel the same way. let latched_mouse = settings_at_start.mouse_mode(); // Request-access hand-off: the launch handler stamps this when it starts a delegated-approval @@ -162,11 +169,8 @@ pub fn run(target: Option<&str>) -> u8 { ), fullscreen: fullscreen_mode(), window_pos: window_pos(), - // `--stats` forces the overlay visible without demoting a richer chosen tier. - stats_verbosity: match settings_at_start.stats_verbosity() { - trust::StatsVerbosity::Off if arg_flag("--stats") => trust::StatsVerbosity::Normal, - v => v, - }, + // Seeds the loop only — every launch carries its own freshly resolved tier. + stats_verbosity: stats_tier(&settings_at_start), touch_mode: settings_at_start.touch_mode(), mouse_mode: settings_at_start.mouse_mode(), invert_scroll: settings_at_start.invert_scroll, diff --git a/clients/session/src/main.rs b/clients/session/src/main.rs index 0c3e804f..b82e55d3 100644 --- a/clients/session/src/main.rs +++ b/clients/session/src/main.rs @@ -116,6 +116,28 @@ mod session_main { std::env::args().any(|a| a == flag) } + /// The stats-overlay tier a session starts on: the resolved setting, except that + /// `--stats` (tooling/debug runs) forces the overlay VISIBLE without demoting an + /// explicitly chosen richer tier. + /// + /// One helper because three callers need the identical rule — both run modes' presenter + /// options and the per-launch [`session_params`] — and a fourth reading of it would be + /// the bug this is here to prevent. + pub(crate) fn stats_tier(settings: &trust::Settings) -> trust::StatsVerbosity { + stats_tier_with(settings.stats_verbosity(), arg_flag("--stats")) + } + + /// [`stats_tier`]'s rule, with argv lifted out so it is testable. + pub(crate) fn stats_tier_with( + chosen: trust::StatsVerbosity, + stats_flag: bool, + ) -> trust::StatsVerbosity { + match chosen { + trust::StatsVerbosity::Off if stats_flag => trust::StatsVerbosity::Normal, + v => v, + } + } + /// Running under Gaming Mode (a Deck, or any gamescope session): the environment /// where the local Steam UI owns the physical Steam/QAM buttons — the system-button /// "auto" policy keys off this. @@ -420,6 +442,12 @@ mod session_main { connect_timeout: connect_timeout(), force_software, profile, + // Presentation-tier, carried per launch rather than read once by the run loop: + // the console streams many sessions through ONE loop, so this is the only way a + // tier the user picked between streams (or one a host's profile carries) reaches + // the overlay before the app is restarted. Single mode passes the same value its + // presenter options already hold, so it changes nothing there. + stats_verbosity: stats_tier(settings), // Phase-locked capture (design/phase-locked-capture.md, Apple/Android parity): // advertised only when the presenter has real on-glass latch stamps // (VK_KHR_present_wait) — without them there is no latch grid to report. The @@ -926,12 +954,7 @@ mod session_main { window_title: format!("Punktfunk · {title}"), fullscreen, window_pos: window_pos(), - // `--stats` forces the overlay visible (tooling/debug runs) without - // demoting an explicitly chosen richer tier. - stats_verbosity: match settings.stats_verbosity() { - trust::StatsVerbosity::Off if arg_flag("--stats") => trust::StatsVerbosity::Normal, - v => v, - }, + stats_verbosity: stats_tier(&settings), touch_mode: settings.touch_mode(), mouse_mode: settings.mouse_mode(), invert_scroll: settings.invert_scroll, @@ -1000,6 +1023,37 @@ mod session_main { } } } + + #[cfg(test)] + mod tests { + use super::*; + use trust::StatsVerbosity as V; + + /// `--stats` is a floor, never a ceiling: it lifts Off to Normal and leaves every + /// richer chosen tier alone. Both run modes' presenter options AND the per-launch + /// params read this one rule, which is the point of having it. + #[test] + fn the_stats_flag_lifts_off_and_demotes_nothing() { + assert_eq!(stats_tier_with(V::Off, true), V::Normal); + assert_eq!(stats_tier_with(V::Off, false), V::Off); + for chosen in [V::Compact, V::Normal, V::Detailed] { + assert_eq!(stats_tier_with(chosen, true), chosen); + assert_eq!(stats_tier_with(chosen, false), chosen); + } + } + + /// The console reads the file ONCE for its window, so a tier changed between streams + /// can only reach the overlay by riding the launch. Guards the wiring the field exists + /// for: whatever settings a launch resolved is what the params carry. + #[test] + fn a_launch_carries_the_tier_its_settings_resolved() { + let mut s = trust::Settings::default(); + for chosen in [V::Off, V::Compact, V::Normal, V::Detailed] { + s.set_stats_verbosity(chosen); + assert_eq!(stats_tier_with(s.stats_verbosity(), false), chosen); + } + } + } } #[cfg(any(target_os = "linux", windows))] diff --git a/crates/pf-client-core/src/session.rs b/crates/pf-client-core/src/session.rs index f881ea9d..bcffb7fb 100644 --- a/crates/pf-client-core/src/session.rs +++ b/crates/pf-client-core/src/session.rs @@ -104,6 +104,19 @@ pub struct SessionParams { /// above; it rides along so the stats overlay can answer "which profile am I on?" without /// re-reading any store (design/client-settings-profiles.md §5.2). pub profile: Option, + /// The stats-overlay tier THIS launch resolved to — the globals, or the profile bound to + /// this host. Presentation-tier, like [`profile`](Self::profile): the session controller + /// never reads it, it rides along so the presenter can adopt it when a browse-mode launch + /// starts. + /// + /// That adoption is the whole point. The console (Gaming Mode / Decky) builds its window + /// and its run loop ONCE and streams many sessions through them, so a tier taken only from + /// the loop's start-of-process options could never change again — a user picking a tier in + /// the console's settings screen saw the row move, the file updated, and every stream keep + /// the old overlay until the app was restarted. Carrying it per launch is what lets the + /// choice land on the next stream, and it makes a profile's `stats_verbosity` reach the + /// console too. The in-stream cycle chord still wins for the rest of the stream it moved. + pub stats_verbosity: crate::trust::StatsVerbosity, /// Advertise `quic::CLIENT_CAP_PHASE_LOCK`: this embedder's presenter has REAL on-glass /// latch stamps (`VK_KHR_present_wait`) and will feed [`latch_grid`](Self::latch_grid), /// so the pump sends the ~1 Hz `PhaseReport`s the host phase-locks its capture tick to diff --git a/crates/pf-presenter/src/run.rs b/crates/pf-presenter/src/run.rs index fb8bbb80..f15809ea 100644 --- a/crates/pf-presenter/src/run.rs +++ b/crates/pf-presenter/src/run.rs @@ -1280,6 +1280,19 @@ fn run_inner(mut opts: SessionOpts, mut mode: ModeCtl) -> Result opts.render_scale_max_dim, ); } + // Adopt the tier this launch RESOLVED (globals or the host's + // profile) instead of keeping the one the process started on. + // `opts.stats_verbosity` only ever seeds the loop: the console + // outlives every stream, so without this a settings change + // reached the file and the settings row and nothing else until + // the app was restarted. + // + // Deliberately HERE and not in `StreamState::new`: the + // codec-fallback retry rebuilds the state from a clone of these + // params mid-stream, and doing it there 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. + stats_verbosity = params.stats_verbosity; // A live pump here would be DETACHED by the assignment // below — `StreamState` has no `Drop`, so its thread // would keep decoding onto the shared Vulkan device that -- 2.54.0