feat(gamescope): end dedicated stream on Steam game exit + auto --steam
ci / web (push) Successful in 54s
ci / docs-site (push) Successful in 1m12s
apple / swift (push) Successful in 1m19s
decky / build-publish (push) Successful in 32s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 11s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
ci / bench (push) Successful in 6m57s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 4m51s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 14s
apple / screenshots (push) Successful in 6m13s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m1s
deb / build-publish (push) Successful in 9m37s
docker / deploy-docs (push) Successful in 29s
deb / build-publish-host (push) Successful in 9m29s
windows-host / package (push) Successful in 16m24s
arch / build-publish (push) Successful in 17m40s
android / android (push) Successful in 18m22s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m39s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 18m52s
ci / rust (push) Successful in 26m16s
ci / web (push) Successful in 54s
ci / docs-site (push) Successful in 1m12s
apple / swift (push) Successful in 1m19s
decky / build-publish (push) Successful in 32s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 9s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 11s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 9s
ci / bench (push) Successful in 6m57s
docker / build-push (ci, ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 4m51s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 14s
apple / screenshots (push) Successful in 6m13s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m1s
deb / build-publish (push) Successful in 9m37s
docker / deploy-docs (push) Successful in 29s
deb / build-publish-host (push) Successful in 9m29s
windows-host / package (push) Successful in 16m24s
arch / build-publish (push) Successful in 17m40s
android / android (push) Successful in 18m22s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m39s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 18m52s
ci / rust (push) Successful in 26m16s
Dedicated single-game sessions now end cleanly when the launched game quits, and Steam launches get gamescope's Steam integration without an operator env knob. End-on-exit: the existing APP_EXITED close only fired when gamescope's PipeWire node vanished, which never happens for Steam — the nested `steam` is the resident singleton client and stays up after a game quits, so gamescope (and its node) never die and the stream sat on a hidden Steam session forever. Add a Steam AppId reaper watcher (pf-vdisplay .../gamescope/discovery.rs): steam_appid_from_launch() parses steam://rungameid/<id>; wait_for_steam_game_exit() waits for the game to start (<=300s grace) then exit (3s confirm); steam_game_running() scans same-uid /proc for Steam's launch reaper matching both the `SteamLaunch` and `AppId=<id>` argv tokens (exact-match; reaper lifetime == game lifetime, so shader precompile can't false-trigger). The host spawns a pf1-gamewatch thread for nested Steam launches that closes the connection with APP_EXITED (launcher clients return to their library) and sets quit/stop; cancelled via stop if the session ends first. Non-Steam nested launches keep the node-death path (gamescope's child IS the game). Flags: auto-enable `--steam` whenever the launch is a Steam launch (was only the global PUNKTFUNK_GAMESCOPE_STEAM knob, default-off) — in-game overlay / Steam+X / gamepad-UI nav for Steam titles with no operator config. New opt-in PUNKTFUNK_GAMESCOPE_GRAB_CURSOR adds `--force-grab-cursor` for a real game launch (FPS mouselook); default OFF because it forces relative mode, which breaks absolute-pointer games/menus. Verified: fmt clean; clippy -D warnings clean on the three crates; pf-vdisplay 64/0 (incl. new steam-appid parse test); punktfunk-host builds + 186/0; reaper /proc detection smoke-tested (detect, exact non-match, gone-after-exit). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -76,10 +76,16 @@ pub struct HostConfig {
|
||||
/// backend (the legacy SudoVDA backend was removed), so this is currently informational — kept for the
|
||||
/// shipped `host.env` and as a forward seam if a second backend is ever added.
|
||||
pub vdisplay: Option<String>,
|
||||
/// `PUNKTFUNK_GAMESCOPE_STEAM` — opt the bare headless gamescope spawn into its Steam
|
||||
/// integration mode (`--steam`). Managed gamescope-session-plus/SteamOS sessions own their
|
||||
/// own flags and do not consult this.
|
||||
/// `PUNKTFUNK_GAMESCOPE_STEAM` — force the bare headless gamescope spawn into its Steam
|
||||
/// integration mode (`--steam`) for EVERY launch. A Steam title auto-enables `--steam` on its
|
||||
/// own regardless of this knob; it exists to force it on for non-Steam launches too. Managed
|
||||
/// gamescope-session-plus/SteamOS sessions own their own flags and do not consult this.
|
||||
pub gamescope_steam: bool,
|
||||
/// `PUNKTFUNK_GAMESCOPE_GRAB_CURSOR` — add `--force-grab-cursor` to the bare headless gamescope
|
||||
/// spawn for an actual game launch, forcing relative-mouse capture so FPS mouselook works over the
|
||||
/// injected pointer. Default OFF: it forces relative mode, which breaks absolute-pointer titles
|
||||
/// and menus, so it's opt-in per host until validated on-glass.
|
||||
pub gamescope_grab_cursor: bool,
|
||||
/// `PUNKTFUNK_RECOVER_SESSION_CMD` — operator hook fired (debounced) when a client connects while NO
|
||||
/// graphical session is live for this uid: the state a compositor crash leaves behind (gnome-shell
|
||||
/// SIGSEGV → GDM greeter, whose auto-login is once-per-boot, so the box would otherwise need a walk-up
|
||||
@@ -152,6 +158,12 @@ impl HostConfig {
|
||||
"1" | "true" | "yes" | "on"
|
||||
)
|
||||
}),
|
||||
gamescope_grab_cursor: val("PUNKTFUNK_GAMESCOPE_GRAB_CURSOR").is_some_and(|s| {
|
||||
matches!(
|
||||
s.trim().to_ascii_lowercase().as_str(),
|
||||
"1" | "true" | "yes" | "on"
|
||||
)
|
||||
}),
|
||||
recover_session_cmd: val("PUNKTFUNK_RECOVER_SESSION_CMD")
|
||||
.filter(|s| !s.trim().is_empty()),
|
||||
on_connect_cmd: val("PUNKTFUNK_ON_CONNECT_CMD").filter(|s| !s.trim().is_empty()),
|
||||
|
||||
@@ -78,7 +78,7 @@ pub use routing::{
|
||||
#[cfg(target_os = "linux")]
|
||||
pub use routing::{
|
||||
cancel_pending_tv_restore, dedicated_game_exited, launch_into_gamescope_session,
|
||||
launch_is_nested,
|
||||
launch_is_nested, steam_appid_from_launch, watch_steam_game_exit,
|
||||
};
|
||||
|
||||
/// Compositors punktfunk knows how to drive (plan §6).
|
||||
|
||||
@@ -25,7 +25,10 @@ use discovery::{
|
||||
check_gamescope_version, find_gamescope_eis_socket, find_gamescope_node,
|
||||
gamescope_node_present, poll_managed_node, wait_for_node,
|
||||
};
|
||||
pub(crate) use discovery::{game_session_exited, is_available};
|
||||
pub(crate) use discovery::{
|
||||
game_session_exited, is_available, steam_appid_from_launch, wait_for_steam_game_exit,
|
||||
SteamGameWatch,
|
||||
};
|
||||
|
||||
/// The gamescope virtual-display driver. Three modes by env, in precedence order:
|
||||
/// * `PUNKTFUNK_GAMESCOPE_SESSION=<client>` — host-MANAGE a `gamescope-session-plus` session
|
||||
@@ -1369,7 +1372,14 @@ fn shape_dedicated_command(app: &str) -> String {
|
||||
/// Add the compositor-side arguments shared by every bare gamescope spawn. `steam_mode` belongs
|
||||
/// before the `--` terminator; [`PUNKTFUNK_GAMESCOPE_APP`](spawn) configures the nested command
|
||||
/// after it and therefore cannot enable gamescope's Steam integration itself.
|
||||
fn add_bare_gamescope_args(command: &mut Command, w: u32, h: u32, hz: u32, steam_mode: bool) {
|
||||
fn add_bare_gamescope_args(
|
||||
command: &mut Command,
|
||||
w: u32,
|
||||
h: u32,
|
||||
hz: u32,
|
||||
steam_mode: bool,
|
||||
grab_cursor: bool,
|
||||
) {
|
||||
command
|
||||
.args(["--backend", "headless"])
|
||||
.args(["-W", &w.to_string()])
|
||||
@@ -1378,6 +1388,9 @@ fn add_bare_gamescope_args(command: &mut Command, w: u32, h: u32, hz: u32, steam
|
||||
if steam_mode {
|
||||
command.arg("--steam");
|
||||
}
|
||||
if grab_cursor {
|
||||
command.arg("--force-grab-cursor");
|
||||
}
|
||||
command.args(["--xwayland-count", "1", "--"]);
|
||||
}
|
||||
|
||||
@@ -1398,16 +1411,27 @@ fn spawn(w: u32, h: u32, hz: u32, cmd: Option<&str>, log: &std::path::Path) -> R
|
||||
// Read the env fallback under the shared env lock so it can't race a concurrent session's
|
||||
// `set_var` of the same key (security-review 2026-06-28 #7).
|
||||
.or_else(|| crate::with_env_lock(|| std::env::var("PUNKTFUNK_GAMESCOPE_APP").ok()))
|
||||
.filter(|s| !s.trim().is_empty())
|
||||
.unwrap_or_else(|| "sleep infinity".to_string());
|
||||
.filter(|s| !s.trim().is_empty());
|
||||
// A real app was requested (vs. the `sleep infinity` keep-alive) — used to scope the game-only
|
||||
// cursor-grab flag below.
|
||||
let game_launch = app.is_some();
|
||||
let app = app.unwrap_or_else(|| "sleep infinity".to_string());
|
||||
// Dedicated-launch command shaping (Part B): a Steam URI runs with `-silent` so the game is the
|
||||
// gamescope focus with no Steam client window to navigate.
|
||||
let app = shape_dedicated_command(&app);
|
||||
let relay = ei_socket_file();
|
||||
let _ = std::fs::remove_file(&relay); // stale socket path from a previous session
|
||||
let steam_mode = pf_host_config::config().gamescope_steam;
|
||||
// Enable gamescope's Steam integration (`--steam`: in-game overlay, Steam+X shortcuts, gamepad-UI
|
||||
// navigation) whenever we're launching Steam — the operator no longer has to set the global
|
||||
// PUNKTFUNK_GAMESCOPE_STEAM knob for a Steam title. The knob still forces it on for every spawn.
|
||||
let steam_mode = pf_host_config::config().gamescope_steam || is_steam_launch(&app);
|
||||
// Opt-in relative-mouse capture for a nested game (`PUNKTFUNK_GAMESCOPE_GRAB_CURSOR`): the client
|
||||
// already sends relative motion, but gamescope only enters relative mode when the app hides the
|
||||
// cursor, which some FPS titles never signal over the injected pointer — grabbing fixes mouselook.
|
||||
// Default OFF (it forces relative mode, which would break absolute-pointer games/menus).
|
||||
let grab_cursor = game_launch && pf_host_config::config().gamescope_grab_cursor;
|
||||
let mut cmd = Command::new("gamescope");
|
||||
add_bare_gamescope_args(&mut cmd, w, h, hz, steam_mode);
|
||||
add_bare_gamescope_args(&mut cmd, w, h, hz, steam_mode, grab_cursor);
|
||||
cmd.args([
|
||||
"sh",
|
||||
"-c",
|
||||
|
||||
@@ -29,6 +29,122 @@ pub(crate) fn game_session_exited(node_id: u32) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Outcome of watching a dedicated Steam launch's game lifetime ([`wait_for_steam_game_exit`]).
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) enum SteamGameWatch {
|
||||
/// The game was seen running and has now exited — the session should end (APP_EXITED).
|
||||
Exited,
|
||||
/// The watch was cancelled (the session ended for another reason) or the game never started
|
||||
/// within the startup grace — leave the session as-is.
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
/// Parse the Steam appid a dedicated launch targets from its resolved command
|
||||
/// (`steam [-silent] steam://rungameid/<appid>`). `None` unless the first token is `steam` and a
|
||||
/// `steam://rungameid/<digits>` URI is present — the trailing digits are the appid, which is exactly
|
||||
/// what Steam's launch reaper carries as `AppId=<appid>` (gameid == appid for a plain library title,
|
||||
/// the only kind the host ever resolves to this shape).
|
||||
pub(crate) fn steam_appid_from_launch(cmd: &str) -> Option<u32> {
|
||||
if cmd.split_whitespace().next() != Some("steam") {
|
||||
return None;
|
||||
}
|
||||
const MARKER: &str = "steam://rungameid/";
|
||||
let tail = &cmd[cmd.find(MARKER)? + MARKER.len()..];
|
||||
let digits: String = tail
|
||||
.chars()
|
||||
.take_while(|c: &char| c.is_ascii_digit())
|
||||
.collect();
|
||||
digits.parse().ok()
|
||||
}
|
||||
|
||||
/// Block until the dedicated Steam game `appid` has started and then exited, `cancel` is set, or the
|
||||
/// game never appears within the startup grace. Same-uid `/proc` scan keyed on Steam's launch reaper
|
||||
/// (`SteamLaunch AppId=<appid>`), whose lifetime is exactly the game's. Returns
|
||||
/// [`SteamGameWatch::Exited`] only after the game was actually seen running and then stayed gone
|
||||
/// across a short confirmation window — so a cold Steam boot / shader precompile (game not up yet) or
|
||||
/// a transient scan miss can't end the stream early. Runs on the host's per-session watch thread.
|
||||
pub(crate) fn wait_for_steam_game_exit(
|
||||
appid: u32,
|
||||
cancel: &std::sync::atomic::AtomicBool,
|
||||
) -> SteamGameWatch {
|
||||
use std::sync::atomic::Ordering;
|
||||
// Cold Steam boot + first-launch shader precompile can delay the game window by minutes; give it a
|
||||
// generous window to appear. A game that never starts leaves the session up (the Steam client is
|
||||
// still streamed, and the node-death path still covers the Steam client itself dying).
|
||||
const START_GRACE: Duration = Duration::from_secs(300);
|
||||
const POLL: Duration = Duration::from_secs(1);
|
||||
// Require the reaper gone across this window (a few polls) so a brief process swap can't fire early.
|
||||
const EXIT_CONFIRM: Duration = Duration::from_secs(3);
|
||||
|
||||
let start_deadline = Instant::now() + START_GRACE;
|
||||
// Phase 1: wait for the game's reaper to appear.
|
||||
while !steam_game_running(appid) {
|
||||
if cancel.load(Ordering::Relaxed) || Instant::now() >= start_deadline {
|
||||
return SteamGameWatch::Cancelled;
|
||||
}
|
||||
std::thread::sleep(POLL);
|
||||
}
|
||||
// Phase 2: the game is up — wait for its reaper to disappear (confirmed across the window).
|
||||
let mut gone_since: Option<Instant> = None;
|
||||
loop {
|
||||
if cancel.load(Ordering::Relaxed) {
|
||||
return SteamGameWatch::Cancelled;
|
||||
}
|
||||
if steam_game_running(appid) {
|
||||
gone_since = None;
|
||||
} else if gone_since.get_or_insert_with(Instant::now).elapsed() >= EXIT_CONFIRM {
|
||||
return SteamGameWatch::Exited;
|
||||
}
|
||||
std::thread::sleep(POLL);
|
||||
}
|
||||
}
|
||||
|
||||
/// Is Steam's launch reaper for appid `appid` alive right now (same uid as the host)? Steam wraps
|
||||
/// every game launch — native or Proton — in `…/reaper SteamLaunch AppId=<appid> -- <game>`, and the
|
||||
/// reaper lives for the game's whole lifetime, so its presence is a precise "the game is running"
|
||||
/// signal. Matched on the `SteamLaunch` + `AppId=<appid>` argv tokens together (exact-match, so
|
||||
/// `AppId=57` never matches appid 570) — specific to the game reaper, so Steam's own shader-precompile
|
||||
/// step (not reaper-wrapped) can't be mistaken for the game.
|
||||
fn steam_game_running(appid: u32) -> bool {
|
||||
// SAFETY: `getuid()` is a parameterless POSIX call that always succeeds and touches no memory.
|
||||
let uid = unsafe { libc::getuid() };
|
||||
let appid_tok = format!("AppId={appid}");
|
||||
let Ok(entries) = std::fs::read_dir("/proc") else {
|
||||
return false;
|
||||
};
|
||||
for e in entries.flatten() {
|
||||
let name = e.file_name();
|
||||
let Some(pid_str) = name.to_str() else {
|
||||
continue;
|
||||
};
|
||||
if !pid_str.bytes().all(|b| b.is_ascii_digit()) {
|
||||
continue;
|
||||
}
|
||||
let Ok(md) = std::fs::metadata(e.path()) else {
|
||||
continue;
|
||||
};
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
if md.uid() != uid {
|
||||
continue;
|
||||
}
|
||||
let Ok(cmdline) = std::fs::read(e.path().join("cmdline")) else {
|
||||
continue;
|
||||
};
|
||||
let (mut launch, mut appid_match) = (false, false);
|
||||
for arg in cmdline.split(|&b| b == 0) {
|
||||
if arg == b"SteamLaunch" {
|
||||
launch = true;
|
||||
} else if arg == appid_tok.as_bytes() {
|
||||
appid_match = true;
|
||||
}
|
||||
}
|
||||
if launch && appid_match {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Poll [`find_gamescope_node`] (unscoped) up to `timeout` — for the managed / SteamOS session, which
|
||||
/// logs to journald (no per-spawn file) and is single-session (no scoping needed).
|
||||
pub(super) fn poll_managed_node(timeout: Duration) -> Option<u32> {
|
||||
@@ -263,7 +379,29 @@ fn parse_version(text: &str) -> Option<(u32, u32, u32)> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{parse_version, MIN_GAMESCOPE};
|
||||
use super::{parse_version, steam_appid_from_launch, MIN_GAMESCOPE};
|
||||
|
||||
#[test]
|
||||
fn parses_steam_appid_from_launch() {
|
||||
// The resolved dedicated-launch command (pre- or post-`-silent` shaping) → the appid.
|
||||
assert_eq!(
|
||||
steam_appid_from_launch("steam steam://rungameid/570"),
|
||||
Some(570)
|
||||
);
|
||||
assert_eq!(
|
||||
steam_appid_from_launch("steam -silent steam://rungameid/1091500"),
|
||||
Some(1091500)
|
||||
);
|
||||
// Non-Steam launches / bare Steam with no rungameid URI → no appid (no game-exit watch).
|
||||
assert_eq!(steam_appid_from_launch("lutris lutris:rungameid/42"), None);
|
||||
assert_eq!(steam_appid_from_launch("steam -gamepadui"), None);
|
||||
assert_eq!(steam_appid_from_launch("vkcube"), None);
|
||||
// A steam:// URI that isn't the first `steam` token (a custom command) is not treated as one.
|
||||
assert_eq!(
|
||||
steam_appid_from_launch("firefox steam://rungameid/570"),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_version_banner() {
|
||||
|
||||
@@ -176,6 +176,28 @@ pub fn dedicated_game_exited(_node_id: u32) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// The Steam appid a dedicated launch targets (`steam … steam://rungameid/<appid>`), for the
|
||||
/// game-exit watcher. `None` for a non-Steam launch (those are covered by the node-death path
|
||||
/// [`dedicated_game_exited`] — gamescope's nested child IS the game). See
|
||||
/// [`gamescope::steam_appid_from_launch`].
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn steam_appid_from_launch(cmd: &str) -> Option<u32> {
|
||||
gamescope::steam_appid_from_launch(cmd)
|
||||
}
|
||||
|
||||
/// Block until the dedicated Steam game `appid` has started and then exited — returns `true` when the
|
||||
/// session should end cleanly (APP_EXITED). Returns `false` if `cancel` is set (the session ended for
|
||||
/// another reason) or the game never started within the startup grace (leave the session up). Runs on
|
||||
/// the host's per-session watch thread; `cancel` is the session's stop flag. See
|
||||
/// [`gamescope::wait_for_steam_game_exit`].
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn watch_steam_game_exit(appid: u32, cancel: &std::sync::atomic::AtomicBool) -> bool {
|
||||
matches!(
|
||||
gamescope::wait_for_steam_game_exit(appid, cancel),
|
||||
gamescope::SteamGameWatch::Exited
|
||||
)
|
||||
}
|
||||
|
||||
/// Cancel any pending TV-session restore because a client (re)connected (review #3). No-op off Linux.
|
||||
#[cfg(target_os = "linux")]
|
||||
pub fn cancel_pending_tv_restore() {
|
||||
|
||||
@@ -978,6 +978,45 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDispl
|
||||
#[cfg(not(any(target_os = "windows", target_os = "linux")))]
|
||||
let _ = &launch;
|
||||
|
||||
// Dedicated Steam launch: end the stream cleanly when the LAUNCHED GAME exits. The node-death
|
||||
// check in the capture-loss branch below can't see this for Steam — the nested `steam` is the
|
||||
// resident client and stays up after a game quits, so gamescope (and its node) never dies and the
|
||||
// stream would sit on a hidden Steam session forever. Watch the game process directly (Steam's
|
||||
// `SteamLaunch AppId=<id>` reaper, whose lifetime == the game's) and close with APP_EXITED when
|
||||
// it's gone, so a launcher client returns to its library. Non-Steam nested launches keep the
|
||||
// node-death path (gamescope's child IS the game). Cancelled via `stop` when the session ends for
|
||||
// another reason first; the thread self-terminates, so we don't join it.
|
||||
#[cfg(target_os = "linux")]
|
||||
let _game_watch = launch
|
||||
.as_deref()
|
||||
.filter(|_| crate::vdisplay::launch_is_nested(compositor))
|
||||
.and_then(crate::vdisplay::steam_appid_from_launch)
|
||||
.map(|appid| {
|
||||
let conn = conn.clone();
|
||||
let stop = stop.clone();
|
||||
let quit = quit.clone();
|
||||
std::thread::Builder::new()
|
||||
.name("pf1-gamewatch".into())
|
||||
.spawn(move || {
|
||||
if crate::vdisplay::watch_steam_game_exit(appid, &stop) {
|
||||
tracing::info!(
|
||||
appid,
|
||||
"dedicated Steam game exited — ending the session cleanly (APP_EXITED)"
|
||||
);
|
||||
// Close FIRST so APP_EXITED is the winning close code (quinn keeps the first
|
||||
// application close), then set the flags: `quit` skips the display lease's
|
||||
// keep-alive linger and `stop` wakes the encode/send loops out.
|
||||
conn.close(
|
||||
punktfunk_core::quic::APP_EXITED_CLOSE_CODE.into(),
|
||||
b"game exited",
|
||||
);
|
||||
quit.store(true, Ordering::SeqCst);
|
||||
stop.store(true, Ordering::SeqCst);
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
});
|
||||
|
||||
let perf = pf_host_config::config().perf;
|
||||
// Microburst cap (applied in send_loop/paced_submit): a frame ≤ the cap bursts out
|
||||
// immediately; only a bigger frame's overflow is spread. `None` = auto — max(128 KB, the
|
||||
|
||||
Reference in New Issue
Block a user