test(resize): extract + unit-test the live-reconfigure gate (H1 gamescope / H5 per-client-mode)
The Linux §6 on-glass matrix can validate the gamescope must-REJECT behavior only on native-gamescope hardware (the NVIDIA dev box fails headless GBM allocation — a nested Hyprland/sway/gamescope output comes up 0×0), so pin the gate down deterministically instead: extract the inline `live_reconfig_ok` decision into a pure `reconfig_allowed(compositor, per_client_mode)` and test it — gamescope rejects in every identity mode, a per-client-mode policy rejects on every backend, and all other compositors (plus the synthetic protocol-test source) with the default identity accept. Also fmt-normalizes the re_add block from the prior commit (whitespace only). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1101,7 +1101,7 @@ async fn serve_session(
|
|||||||
let per_client_mode_identity = crate::vdisplay::policy::prefs()
|
let per_client_mode_identity = crate::vdisplay::policy::prefs()
|
||||||
.configured_effective()
|
.configured_effective()
|
||||||
.is_some_and(|e| e.identity == crate::vdisplay::policy::Identity::PerClientMode);
|
.is_some_and(|e| e.identity == crate::vdisplay::policy::Identity::PerClientMode);
|
||||||
compositor != Some(crate::vdisplay::Compositor::Gamescope) && !per_client_mode_identity
|
reconfig_allowed(compositor, per_client_mode_identity)
|
||||||
};
|
};
|
||||||
// Negotiated codec (HEVC / H.264 / AV1), derived from the Welcome. `Copy`, so the control task's
|
// Negotiated codec (HEVC / H.264 / AV1), derived from the Welcome. `Copy`, so the control task's
|
||||||
// `async move` captures a copy and it stays usable for the data-plane SessionContext below.
|
// `async move` captures a copy and it stays usable for the data-plane SessionContext below.
|
||||||
@@ -3045,6 +3045,22 @@ fn delivered_mode(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether a session on `compositor` (`None` = the synthetic source) with a `per_client_mode`
|
||||||
|
/// identity policy may LIVE-reconfigure — accept a mid-stream `Reconfigure`
|
||||||
|
/// (design/midstream-resolution-resize.md H1/H5). Gated OFF for:
|
||||||
|
/// * **gamescope** (every sub-mode): a resize would respawn the nested game / restart the box's
|
||||||
|
/// game-mode session — it must never relaunch the title, so the client keeps scaling client-side.
|
||||||
|
/// * a **per-client-mode identity** policy: the mode is part of the display-identity slot key, so a
|
||||||
|
/// resize resolves a DIFFERENT slot (a fresh Windows monitor / a differently-named KWin output),
|
||||||
|
/// defeating the policy — honest downgrade is to reject and let the client scale.
|
||||||
|
/// Every other compositor (and the synthetic protocol-test source) with the default identity accepts.
|
||||||
|
fn reconfig_allowed(
|
||||||
|
compositor: Option<crate::vdisplay::Compositor>,
|
||||||
|
per_client_mode: bool,
|
||||||
|
) -> bool {
|
||||||
|
compositor != Some(crate::vdisplay::Compositor::Gamescope) && !per_client_mode
|
||||||
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn send_loop(
|
fn send_loop(
|
||||||
mut session: Session,
|
mut session: Session,
|
||||||
@@ -4615,6 +4631,29 @@ mod tests {
|
|||||||
assert_eq!(capped.refresh_hz, 30);
|
assert_eq!(capped.refresh_hz, 30);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reconfig_allowed_gates_gamescope_and_per_client_mode() {
|
||||||
|
use crate::vdisplay::Compositor::{Gamescope, Hyprland, Kwin, Mutter, Wlroots};
|
||||||
|
// gamescope ALWAYS rejects — a resize would respawn the nested game (H1/D3), regardless of
|
||||||
|
// the identity policy.
|
||||||
|
assert!(!reconfig_allowed(Some(Gamescope), false));
|
||||||
|
assert!(!reconfig_allowed(Some(Gamescope), true));
|
||||||
|
// A per-client-mode identity policy rejects on every backend — the resize resolves a
|
||||||
|
// different display-identity slot (H5).
|
||||||
|
assert!(!reconfig_allowed(Some(Kwin), true));
|
||||||
|
assert!(!reconfig_allowed(Some(Mutter), true));
|
||||||
|
assert!(!reconfig_allowed(None, true));
|
||||||
|
// Every other compositor with the default identity ACCEPTS (recreate / re-arrival / in-place).
|
||||||
|
for c in [Kwin, Mutter, Wlroots, Hyprland] {
|
||||||
|
assert!(
|
||||||
|
reconfig_allowed(Some(c), false),
|
||||||
|
"{c:?} should allow live reconfigure"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// The synthetic source (no compositor) is the protocol-test path — always reconfigurable.
|
||||||
|
assert!(reconfig_allowed(None, false));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn pad_snapshot_replaces_state_and_seq_gates() {
|
fn pad_snapshot_replaces_state_and_seq_gates() {
|
||||||
use punktfunk_core::input::{gamepad, GamepadSnapshot};
|
use punktfunk_core::input::{gamepad, GamepadSnapshot};
|
||||||
|
|||||||
@@ -573,8 +573,8 @@ impl VirtualDisplayManager {
|
|||||||
};
|
};
|
||||||
// SAFETY: `dev` is the handle `ensure_device()` returned above; `re_add` touches the
|
// SAFETY: `dev` is the handle `ensure_device()` returned above; `re_add` touches the
|
||||||
// live topology under the held `state` lock. `mon` is owned here (removed from the map).
|
// live topology under the held `state` lock. `mon` is owned here (removed from the map).
|
||||||
let new_mon = match unsafe { self.re_add(dev, &mut inner, slot, &mon, mode, client_hdr) }
|
let new_mon =
|
||||||
{
|
match unsafe { self.re_add(dev, &mut inner, slot, &mon, mode, client_hdr) } {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// The re-arrival failed — put the OLD monitor back so the session keeps
|
// The re-arrival failed — put the OLD monitor back so the session keeps
|
||||||
@@ -588,9 +588,13 @@ impl VirtualDisplayManager {
|
|||||||
// `re_add` preserved `gen`, so both the old session's lease and this new one match on
|
// `re_add` preserved `gen`, so both the old session's lease and this new one match on
|
||||||
// release. +1 ref for the new (build-then-drop overlap) lease.
|
// release. +1 ref for the new (build-then-drop overlap) lease.
|
||||||
let out = self.output_for(slot, &new_mon, quit);
|
let out = self.output_for(slot, &new_mon, quit);
|
||||||
inner
|
inner.slots.insert(
|
||||||
.slots
|
slot,
|
||||||
.insert(slot, SlotState::Active { mon: new_mon, refs: refs + 1 });
|
SlotState::Active {
|
||||||
|
mon: new_mon,
|
||||||
|
refs: refs + 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
// The width changed — re-arrange the group so auto-row siblings don't overlap the
|
// The width changed — re-arrange the group so auto-row siblings don't overlap the
|
||||||
// resized display (no-op for a single member).
|
// resized display (no-op for a single member).
|
||||||
self.apply_group_layout(&mut inner);
|
self.apply_group_layout(&mut inner);
|
||||||
@@ -1053,7 +1057,10 @@ impl VirtualDisplayManager {
|
|||||||
// SAFETY: `dev` is the live control handle (this fn's contract); `&old.key` borrows the
|
// SAFETY: `dev` is the live control handle (this fn's contract); `&old.key` borrows the
|
||||||
// still-owned `MonitorKey`, alive across the synchronous IOCTL.
|
// still-owned `MonitorKey`, alive across the synchronous IOCTL.
|
||||||
if let Err(e) = unsafe { self.driver.remove_monitor(dev, &old.key) } {
|
if let Err(e) = unsafe { self.driver.remove_monitor(dev, &old.key) } {
|
||||||
tracing::warn!(old_target = old.target_id, "re-arrival REMOVE failed (continuing to ADD): {e:#}");
|
tracing::warn!(
|
||||||
|
old_target = old.target_id,
|
||||||
|
"re-arrival REMOVE failed (continuing to ADD): {e:#}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Let the OS finish the ASYNC monitor departure before the ADD — a back-to-back REMOVE→ADD
|
// Let the OS finish the ASYNC monitor departure before the ADD — a back-to-back REMOVE→ADD
|
||||||
// races the teardown and the ADD is rejected under churn (same 400 ms settle as the reconnect
|
// races the teardown and the ADD is rejected under churn (same 400 ms settle as the reconnect
|
||||||
|
|||||||
Reference in New Issue
Block a user