feat(resize): mid-stream resolution resize — host hardening (H1–H5) + session-binary Match window (C1)
design/midstream-resolution-resize.md Phase 0 + Phase 1. Host (Phase 0): - H1/H5: per-backend Reconfigure acceptance gate — reject for gamescope (all sub-modes; a resize must never relaunch the title) and under the per-client-mode identity policy (a resize would resolve a different display slot). Synthetic stays reconfigurable on purpose (the protocol test source; the C-ABI roundtrip test rides it). Plus a 500 ms host-side min-interval backstop against Reconfigure spam. - H2: rollback/corrective ack — the data plane reports the mode actually live after a failed rebuild (or a refresh the backend capped) through a reconfig_result channel; the control task forwards it as a second accepted Reconfigured so the client's mode slot self-corrects. - H3: live stats mode — SendStats reads a packed AtomicU64 (w|h|hz) updated on every switch instead of latching the session-start mode. - H4: registry::retire(gen) — a mode-switch rebuild force-releases the superseded Linux display, so linger/forever keep-alive policies don't accumulate kept monitors at stale modes. VirtualOutput carries pool_gen (fresh AND reused) and the Pipeline tuple threads it to the switch arm. Client (Phase 1, default off): - Settings: match_window policy + persisted last window size; exposed as the Resolution tri-state (Native / Match window / explicit) in the Skia console, GTK and WinUI settings pages. - pf-presenter: window opens at the persisted size; Hello mode follows the window's pixel size; D2 trigger discipline (400 ms debounce to resize-end, ≥1 s spacing, even-floor + ≥320×200 clamp, each distinct size requested at most once — covers rejects and host rollbacks) as a pure, unit-tested decision; HUD line + title refresh on a switch. - Session binary wires both --connect and --browse paths; the WinUI shell is session-always, so this covers Windows too. Verified: workspace tests + clippy green; synthetic --remode end-to-end; live session-binary run (window at persisted 1000×600 → Hello 1000×600@60). On-glass per-backend matrix (Mutter/KWin/gamescope-reject, keep-alive accumulation) still pending before any default flip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -241,6 +241,7 @@ impl VirtualDisplay for GamescopeDisplay {
|
||||
keepalive: Box::new(()),
|
||||
ownership: DisplayOwnership::External,
|
||||
reused_gen: None,
|
||||
pool_gen: None,
|
||||
});
|
||||
}
|
||||
check_gamescope_version(); // diagnostic only — warns on known-deadlock-prone versions
|
||||
@@ -366,6 +367,7 @@ fn managed_output(node_id: u32, mode: Mode) -> VirtualOutput {
|
||||
keepalive: Box::new(()),
|
||||
ownership: DisplayOwnership::SessionManaged,
|
||||
reused_gen: None,
|
||||
pool_gen: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -198,6 +198,7 @@ impl VirtualDisplay for HyprlandDisplay {
|
||||
// `remote_fd.is_some()` — same as wlroots.
|
||||
ownership: DisplayOwnership::Owned,
|
||||
reused_gen: None,
|
||||
pool_gen: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +135,7 @@ impl VirtualDisplay for WlrootsDisplay {
|
||||
// `remote_fd.is_some()` (keep-alive stays off for wlroots until fresh-portal re-attach).
|
||||
ownership: DisplayOwnership::Owned,
|
||||
reused_gen: None,
|
||||
pool_gen: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,6 +176,21 @@ pub fn mark_failed(gen: u64) {
|
||||
let _ = gen;
|
||||
}
|
||||
|
||||
/// Force-release a **superseded** kept display by its generation stamp
|
||||
/// (`design/midstream-resolution-resize.md` H4): after a mid-stream mode-switch rebuild, the old
|
||||
/// display's lease drop is indistinguishable from a disconnect, so under a `linger`/`forever`
|
||||
/// keep-alive policy every resize would accumulate kept monitors at stale modes. The mode-switch
|
||||
/// arm calls this once the new pipeline is up and the old capturer is dropped. Only a KEPT
|
||||
/// (lingering/pinned) entry is released — an Active one is refused, like `/display/release` — and
|
||||
/// a gen that's already gone (immediate teardown) is a no-op. No-op off Linux (Windows
|
||||
/// reconfigures the same monitor in place — nothing is superseded).
|
||||
pub fn retire(gen: u64) {
|
||||
#[cfg(target_os = "linux")]
|
||||
linux::retire(gen);
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let _ = gen;
|
||||
}
|
||||
|
||||
/// Invalidate every kept display of `backend` — its compositor instance is gone (a Game↔Desktop switch
|
||||
/// tore it down), so `/display/state` must stop listing it and its keepalive must be reaped
|
||||
/// (`design/gamemode-and-dedicated-sessions.md` A4). Called from the session-switch watcher / a
|
||||
@@ -386,6 +401,9 @@ mod linux {
|
||||
// A2: tell the pipeline builder this was a REUSED kept display, so a first-frame failure can
|
||||
// `mark_failed(gen)` (tear the corpse down) rather than re-wedge the retry loop on the same node.
|
||||
out.reused_gen = reused.then_some(gen);
|
||||
// H4: every pooled display carries its gen, so a mode-switch rebuild can `retire` the entry
|
||||
// this output's successor supersedes.
|
||||
out.pool_gen = Some(gen);
|
||||
out
|
||||
}
|
||||
|
||||
@@ -819,6 +837,20 @@ mod linux {
|
||||
}
|
||||
|
||||
pub(super) fn force_release(slot: Option<u64>) -> usize {
|
||||
release_kept(slot, "released (mgmt /display/release)")
|
||||
}
|
||||
|
||||
/// H4 — force-release a display superseded by a mid-stream mode switch. Same machinery as
|
||||
/// [`force_release`] (kept entries only — an Active entry is refused, and a gen already torn
|
||||
/// down under `immediate` is a no-op), distinct log line.
|
||||
pub(super) fn retire(gen: u64) {
|
||||
release_kept(Some(gen), "retired (superseded by a mode switch)");
|
||||
}
|
||||
|
||||
/// Remove + tear down KEPT (lingering/pinned) entries — all of them, or one by gen — running /
|
||||
/// handing off group topology restores, with keepalive drops outside the lock. The shared core
|
||||
/// of [`force_release`] (mgmt) and [`retire`] (mode-switch supersede).
|
||||
fn release_kept(slot: Option<u64>, why: &'static str) -> usize {
|
||||
let Some(r) = REG.get() else { return 0 };
|
||||
let (released, restores) = {
|
||||
let mut es = r.entries.lock().unwrap();
|
||||
@@ -847,10 +879,7 @@ mod linux {
|
||||
restore();
|
||||
}
|
||||
for e in released {
|
||||
tracing::info!(
|
||||
backend = e.backend,
|
||||
"virtual display released (mgmt /display/release)"
|
||||
);
|
||||
tracing::info!(backend = e.backend, "virtual display {why}");
|
||||
drop(e);
|
||||
}
|
||||
n
|
||||
|
||||
Reference in New Issue
Block a user