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:
2026-07-11 14:56:59 +02:00
parent 4c18bb80ca
commit d294b3923c
2 changed files with 63 additions and 17 deletions
@@ -573,24 +573,28 @@ impl VirtualDisplayManager {
};
// 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).
let new_mon = match unsafe { self.re_add(dev, &mut inner, slot, &mon, mode, client_hdr) }
{
Ok(m) => m,
Err(e) => {
// The re-arrival failed — put the OLD monitor back so the session keeps
// streaming its current mode (the control task already acked the switch; the
// rebuild reuses the old target and Fix 2's corrective ack tells the client the
// resolution didn't change). Its `gen`/`refs` are intact, so leases stay valid.
inner.slots.insert(slot, SlotState::Active { mon, refs });
return Err(e).context("mid-stream resize re-arrival");
}
};
let new_mon =
match unsafe { self.re_add(dev, &mut inner, slot, &mon, mode, client_hdr) } {
Ok(m) => m,
Err(e) => {
// The re-arrival failed — put the OLD monitor back so the session keeps
// streaming its current mode (the control task already acked the switch; the
// rebuild reuses the old target and Fix 2's corrective ack tells the client the
// resolution didn't change). Its `gen`/`refs` are intact, so leases stay valid.
inner.slots.insert(slot, SlotState::Active { mon, refs });
return Err(e).context("mid-stream resize re-arrival");
}
};
// `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.
let out = self.output_for(slot, &new_mon, quit);
inner
.slots
.insert(slot, SlotState::Active { mon: new_mon, refs: refs + 1 });
inner.slots.insert(
slot,
SlotState::Active {
mon: new_mon,
refs: refs + 1,
},
);
// The width changed — re-arrange the group so auto-row siblings don't overlap the
// resized display (no-op for a single member).
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
// still-owned `MonitorKey`, alive across the synchronous IOCTL.
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
// races the teardown and the ADD is rejected under churn (same 400 ms settle as the reconnect