fix(gamescope): offer the refresh-rate set on the SteamOS path #466
@@ -1416,11 +1416,23 @@ fn write_steamos_dropin(shim_dir: &std::path::Path, mode: Mode, hdr: bool) -> Re
|
||||
w = mode.width,
|
||||
h = mode.height,
|
||||
hz = game_hz(mode.refresh_hz),
|
||||
// Read (unquoted) by the PATH shim — empty for an SDR session. Quoted HERE because a
|
||||
// systemd `Environment=` value with spaces must be, or only the first flag survives.
|
||||
// Read (unquoted) by the PATH shim — empty for an SDR session on a stock gamescope. Quoted
|
||||
// HERE because a systemd `Environment=` value with spaces must be, or only the first flag
|
||||
// survives.
|
||||
//
|
||||
// `refresh_rate_args` rides along because this path has no other way to deliver it. The two
|
||||
// session-plus paths hand the set to the SCRIPT as `CUSTOM_REFRESH_RATES` and let it build
|
||||
// the flag; SteamOS's `gamescope-session` is Valve's, has never read that variable, and the
|
||||
// shim only forwards `PF_HDR_ARGS` — so without this the flag reaches no SteamOS session at
|
||||
// all, and Steam's in-session display menu shows one refresh entry and no resolutions.
|
||||
// Empty below `+pfhdr3`, exactly like the other two arg builders.
|
||||
hdr_args = hdr_args(hdr)
|
||||
.into_iter()
|
||||
.chain(cursor_args())
|
||||
// The advertised SET, not the rate we run at: `-r` is `PF_HZ` (frame-limited) above,
|
||||
// while the set is keyed on the session's own mode — the same split `launch_session`
|
||||
// makes between its `game` and `offered`.
|
||||
.chain(refresh_rate_args(mode.refresh_hz.max(1)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" "),
|
||||
);
|
||||
@@ -5529,20 +5541,31 @@ fn refresh_rate_args(session_hz: u32) -> Vec<String> {
|
||||
if !gamescope_can_offer_refresh_rates() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut rates = pf_host_config::config().gamescope_refresh_rates.clone();
|
||||
vec![
|
||||
"--custom-refresh-rates".to_string(),
|
||||
refresh_rate_list(
|
||||
session_hz,
|
||||
&pf_host_config::config().gamescope_refresh_rates,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
/// The comma-joined rate set [`refresh_rate_args`] hands gamescope. Split out pure because both
|
||||
/// managed paths interpolate it into an UNQUOTED `${PF_HDR_ARGS}` that the shell word-splits, so
|
||||
/// "contains no whitespace" is a correctness property of this string and not a formatting detail —
|
||||
/// a space here would split one flag into two argv entries and gamescope would reject the launch.
|
||||
fn refresh_rate_list(session_hz: u32, configured: &[u32]) -> String {
|
||||
let mut rates = configured.to_vec();
|
||||
if !rates.contains(&session_hz) {
|
||||
rates.push(session_hz);
|
||||
}
|
||||
rates.sort_unstable();
|
||||
rates.dedup();
|
||||
vec![
|
||||
"--custom-refresh-rates".to_string(),
|
||||
rates
|
||||
.iter()
|
||||
.map(u32::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join(","),
|
||||
]
|
||||
rates
|
||||
.iter()
|
||||
.map(u32::to_string)
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
}
|
||||
|
||||
/// What a bare SPAWN will actually run, resolved ONCE per `create`: the per-session launch command
|
||||
@@ -5699,7 +5722,7 @@ mod tests {
|
||||
free_box_session_for_exclusive, game_hz, gamescope_output_size, hdr_args, idle_dropin_body,
|
||||
idle_dropin_path, install_idle_dropin, is_steam_launch, managed_darken_acquire_edge,
|
||||
managed_darken_release_edge, mask_unit, missing_flags, mode_mismatch,
|
||||
nested_wrapper_script, our_wsi_layer_dir, parse_listed_units, plan_bind,
|
||||
nested_wrapper_script, our_wsi_layer_dir, parse_listed_units, plan_bind, refresh_rate_list,
|
||||
release_autologin_mask, remove_idle_dropin, script_hardcodes_gamescope, sentinel_advanced,
|
||||
shape_dedicated_command, switch_ends_mask_window, takeover_state_is_live, unmask_unit,
|
||||
xwayland_refusal_marker, BindOff, BindPlan, BoxOutputSize, DmHelperError, SessionBind,
|
||||
@@ -5879,6 +5902,30 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// The rate set rides into both managed sessions inside an UNQUOTED `${PF_HDR_ARGS}`, which the
|
||||
/// shim's shell word-splits. So whitespace in this string is not cosmetic: it would split one
|
||||
/// flag into two argv entries and gamescope would reject the launch. The session rate must also
|
||||
/// survive, since it is the rate the session actually runs at.
|
||||
#[test]
|
||||
fn the_refresh_rate_list_is_word_split_safe_and_keeps_the_session_rate() {
|
||||
let list = refresh_rate_list(240, &[60, 120]);
|
||||
assert_eq!(list, "60,120,240", "sorted, deduped, session rate appended");
|
||||
assert!(
|
||||
!list.contains(char::is_whitespace),
|
||||
"an unquoted ${{PF_HDR_ARGS}} word-splits on whitespace"
|
||||
);
|
||||
assert_eq!(
|
||||
refresh_rate_list(60, &[60]),
|
||||
"60",
|
||||
"a configured set that already holds the session rate gains no duplicate"
|
||||
);
|
||||
assert_eq!(
|
||||
refresh_rate_list(90, &[]),
|
||||
"90",
|
||||
"unset, we advertise exactly the rate the client asked for"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_select_sentinel_needs_a_baseline() {
|
||||
let t0 = std::time::SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_000);
|
||||
|
||||
@@ -236,6 +236,13 @@ own name and does **not** replace your system gamescope — your Gaming Mode kee
|
||||
- **NixOS** — `services.punktfunk.host.gamescopeHdr` (default `true`).
|
||||
- **Anything else** — `bash packaging/gamescope/build-punktfunk-gamescope.sh` from the source tree.
|
||||
|
||||
**Restart the host after installing it** — `systemctl --user restart punktfunk-host`, or your
|
||||
platform's equivalent. The host asks the gamescope binary what it can do **once per process** and
|
||||
keeps the answer, because a session's bit depth has to be settled before the display exists. So a
|
||||
build installed under a running host is not seen: streaming keeps working, in SDR, and the only
|
||||
sign is the startup line saying `gamescope has no +pfhdr marker`. After the restart that line reads
|
||||
`using the punktfunk build (10-bit HDR capture available)` instead.
|
||||
|
||||
HDR is attempted by default once the build is present (`PUNKTFUNK_GAMESCOPE_HDR=0` forces SDR).
|
||||
|
||||
**The build only reaches sessions the host starts itself** — managed, `PUNKTFUNK_GAMESCOPE_SESSION`,
|
||||
|
||||
Reference in New Issue
Block a user