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:
@@ -176,7 +176,9 @@ fn row_spec(id: RowId, ctx: &Ctx) -> RowSpec {
|
||||
RowId::Resolution => (
|
||||
Some("Stream"),
|
||||
"Resolution",
|
||||
if s.width == 0 {
|
||||
if s.match_window {
|
||||
"Match window".into()
|
||||
} else if s.width == 0 {
|
||||
"Native".into()
|
||||
} else {
|
||||
format!("{} × {}", s.width, s.height)
|
||||
@@ -259,7 +261,8 @@ fn row_spec(id: RowId, ctx: &Ctx) -> RowSpec {
|
||||
fn detail(id: RowId) -> &'static str {
|
||||
match id {
|
||||
RowId::Resolution => {
|
||||
"The host creates a virtual display at exactly this size — no scaling."
|
||||
"The host creates a virtual display at exactly this size — no scaling. \
|
||||
Match window follows this window, including mid-stream resizes."
|
||||
}
|
||||
RowId::Refresh => "Native follows the display this window is on.",
|
||||
RowId::Bitrate => "Automatic uses the host's default (20 Mbps).",
|
||||
@@ -303,11 +306,20 @@ fn adjust(id: RowId, delta: i32, wrap: bool, ctx: &mut Ctx) -> bool {
|
||||
let s = &mut *ctx.settings;
|
||||
match id {
|
||||
RowId::Resolution => {
|
||||
let cur = RESOLUTIONS
|
||||
.iter()
|
||||
.position(|(w, h)| (*w, *h) == (s.width, s.height));
|
||||
step_option(cur, RESOLUTIONS.len(), delta, wrap).map(|i| {
|
||||
(s.width, s.height) = RESOLUTIONS[i];
|
||||
// The D1 tri-state as one picker: Native, Match window, then the explicit
|
||||
// sizes (RESOLUTIONS keeps its (0,0) = Native head; Match window is the
|
||||
// virtual index 1, stored as the `match_window` flag with w/h cleared).
|
||||
let cur = if s.match_window {
|
||||
Some(1)
|
||||
} else {
|
||||
RESOLUTIONS
|
||||
.iter()
|
||||
.position(|(w, h)| (*w, *h) == (s.width, s.height))
|
||||
.map(|i| if i == 0 { 0 } else { i + 1 })
|
||||
};
|
||||
step_option(cur, RESOLUTIONS.len() + 1, delta, wrap).map(|i| {
|
||||
s.match_window = i == 1;
|
||||
(s.width, s.height) = if i <= 1 { (0, 0) } else { RESOLUTIONS[i - 1] };
|
||||
})
|
||||
}
|
||||
RowId::Refresh => {
|
||||
@@ -401,14 +413,26 @@ mod tests {
|
||||
device_name: "t",
|
||||
t: 0.0,
|
||||
};
|
||||
// Resolution starts at Native (index 0): left refuses, right steps.
|
||||
// Resolution starts at Native (index 0): left refuses, right steps — first onto
|
||||
// Match window (the D1 tri-state's middle option), then the explicit sizes.
|
||||
assert!(!adjust(RowId::Resolution, -1, false, &mut ctx));
|
||||
assert!(adjust(RowId::Resolution, 1, false, &mut ctx));
|
||||
assert!(ctx.settings.match_window, "Native → Match window");
|
||||
assert_eq!((ctx.settings.width, ctx.settings.height), (0, 0));
|
||||
assert!(adjust(RowId::Resolution, 1, false, &mut ctx));
|
||||
assert!(!ctx.settings.match_window, "explicit size clears the policy");
|
||||
assert_eq!((ctx.settings.width, ctx.settings.height), (1280, 720));
|
||||
// Stepping back from an explicit size returns to Match window, then Native.
|
||||
assert!(adjust(RowId::Resolution, -1, false, &mut ctx));
|
||||
assert!(ctx.settings.match_window);
|
||||
assert!(adjust(RowId::Resolution, -1, false, &mut ctx));
|
||||
assert!(!ctx.settings.match_window);
|
||||
assert_eq!(ctx.settings.width, 0, "back to Native");
|
||||
// Cycle from the last option wraps to the first.
|
||||
(ctx.settings.width, ctx.settings.height) = (3840, 2160);
|
||||
assert!(adjust(RowId::Resolution, 1, true, &mut ctx));
|
||||
assert_eq!(ctx.settings.width, 0, "wrapped to Native");
|
||||
assert!(!ctx.settings.match_window);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user