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:
@@ -264,21 +264,23 @@ pub fn show(
|
||||
let page = adw::PreferencesPage::new();
|
||||
|
||||
let stream = adw::PreferencesGroup::builder().title("Stream").build();
|
||||
let res_names: Vec<String> = RESOLUTIONS
|
||||
.iter()
|
||||
.map(|&(w, h)| {
|
||||
if w == 0 {
|
||||
"Native display".to_string()
|
||||
} else {
|
||||
format!("{w} × {h}")
|
||||
}
|
||||
})
|
||||
// The D1 tri-state: Native, Match window (a virtual index 1, stored as the
|
||||
// `match_window` flag), then the explicit sizes.
|
||||
let res_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} × {h}")),
|
||||
)
|
||||
.collect();
|
||||
let res_row = ChoiceRow::new(
|
||||
&dialog,
|
||||
inline,
|
||||
"Resolution",
|
||||
"The host creates a virtual output at exactly this size",
|
||||
"The host creates a virtual output at exactly this size — Match window follows \
|
||||
the stream window, including mid-stream resizes",
|
||||
&res_names.iter().map(String::as_str).collect::<Vec<_>>(),
|
||||
);
|
||||
let hz_names: Vec<String> = REFRESH
|
||||
@@ -470,10 +472,15 @@ pub fn show(
|
||||
// Seed from the current settings.
|
||||
{
|
||||
let s = settings.borrow();
|
||||
let res_i = RESOLUTIONS
|
||||
.iter()
|
||||
.position(|&(w, h)| w == s.width && h == s.height)
|
||||
.unwrap_or(0);
|
||||
let res_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)
|
||||
};
|
||||
res_row.set_selected(res_i as u32);
|
||||
let hz_i = REFRESH.iter().position(|&r| r == s.refresh_hz).unwrap_or(0);
|
||||
hz_row.set_selected(hz_i as u32);
|
||||
@@ -508,8 +515,14 @@ pub fn show(
|
||||
dialog.add(&page);
|
||||
dialog.connect_closed(move |_| {
|
||||
let mut s = settings.borrow_mut();
|
||||
let (w, h) = RESOLUTIONS[(res_row.selected() as usize).min(RESOLUTIONS.len() - 1)];
|
||||
(s.width, s.height) = (w, h);
|
||||
// Index 1 is the virtual "Match window" option; 0 = Native, 2.. = explicit.
|
||||
let res_i = (res_row.selected() as usize).min(RESOLUTIONS.len());
|
||||
s.match_window = res_i == 1;
|
||||
(s.width, s.height) = if res_i <= 1 {
|
||||
(0, 0)
|
||||
} else {
|
||||
RESOLUTIONS[res_i - 1]
|
||||
};
|
||||
s.refresh_hz = REFRESH[(hz_row.selected() as usize).min(REFRESH.len() - 1)];
|
||||
s.bitrate_kbps = (bitrate_row.value() * 1000.0) as u32;
|
||||
s.gamepad = GAMEPADS[(pad_row.selected() as usize).min(GAMEPADS.len() - 1)].to_string();
|
||||
|
||||
Reference in New Issue
Block a user