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:
2026-07-11 12:53:47 +02:00
parent ca61477c3c
commit d0d9bd5bfb
13 changed files with 644 additions and 70 deletions
+23 -15
View File
@@ -136,29 +136,37 @@ pub(crate) fn settings_page(
let s = ctx.settings.lock().unwrap().clone();
// --- Display ---------------------------------------------------------------------------
// The D1 tri-state: Native, Match window (a virtual index 1, stored as the
// `match_window` flag), then the explicit sizes.
let (res_names, res_i) = {
let names: Vec<String> = RESOLUTIONS
.iter()
.map(|&(w, h)| {
if w == 0 {
"Native display".into()
} else {
format!("{w} \u{00D7} {h}")
}
})
let names: Vec<String> = std::iter::once("Native display".to_string())
.chain(std::iter::once("Match window".to_string()))
.chain(
RESOLUTIONS
.iter()
.skip(1)
.map(|&(w, h)| format!("{w} \u{00D7} {h}")),
)
.collect();
let i = RESOLUTIONS
.iter()
.position(|&(w, h)| w == s.width && h == s.height)
.unwrap_or(0);
let i = if s.match_window {
1
} else {
RESOLUTIONS
.iter()
.position(|&(w, h)| w == s.width && h == s.height)
.map(|i| if i == 0 { 0 } else { i + 1 })
.unwrap_or(0)
};
(names, i)
};
let res_combo = setting_combo(ctx, "Resolution", res_names, res_i, |s, i| {
(s.width, s.height) = RESOLUTIONS[i];
s.match_window = i == 1;
(s.width, s.height) = if i <= 1 { (0, 0) } else { RESOLUTIONS[i - 1] };
})
.tooltip(
"The host creates a virtual display at exactly this size. \u{201C}Native display\u{201D} \
resolves to the monitor this window is on at connect.",
resolves to the monitor this window is on at connect; \u{201C}Match window\u{201D} \
follows the stream window, including mid-stream resizes.",
);
let (hz_names, hz_i) = {
let names: Vec<String> = REFRESH