feat(vdisplay): lifecycle state machine + display state/release API (Stage 1)
Stage 1 of design/display-management.md — the lifecycle core + the display
management surface:
- vdisplay/lifecycle.rs: pure per-slot state machine (Idle/Active{refs}/
Lingering{until}/Pinned) with acquire/release/expiry/force-release
transitions. No I/O, no OS types — the platform-neutral distillation of the
Windows manager's model. Unit + a 200k-iteration seeded property walk
(no leaks / double-frees / refcount underflow across arbitrary interleavings).
- vdisplay/registry.rs: neutral snapshot/release facade over the per-OS
lifecycle owners. Windows reads/controls the VirtualDisplayManager; Linux
keep-alive (a per-session pool) lands in a following increment (needs GPU-box
validation).
- windows/manager.rs: additive snapshot() + force_release() (no behavior change
to the on-glass-validated path).
- mgmt: GET /api/v1/display/state (live/kept displays) + POST /api/v1/display/release
(tear down lingering/pinned now; refuses active). OpenAPI regenerated.
- web console: Virtual displays card gains a live-display list (polled) with
per-row + release-all buttons and a linger countdown.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -892,6 +892,81 @@ fn resolve_render_pin() -> Option<LUID> {
|
||||
crate::win_adapter::resolve_render_adapter_luid()
|
||||
}
|
||||
|
||||
/// A read-only view of the managed monitor for the mgmt `/display/state` endpoint (Goal:
|
||||
/// display-management registry facade). Backend-neutral; the [`crate::vdisplay::registry`] facade
|
||||
/// maps it into the wire shape.
|
||||
pub(crate) struct ManagedInfo {
|
||||
pub backend: &'static str,
|
||||
pub mode: (u32, u32, u32),
|
||||
/// `"active"` | `"lingering"`.
|
||||
pub state: &'static str,
|
||||
/// Milliseconds until a lingering monitor is torn down (`None` when active).
|
||||
pub expires_in_ms: Option<u64>,
|
||||
/// Live sessions holding the monitor.
|
||||
pub sessions: u32,
|
||||
/// The monitor's generation stamp — a stable-enough id for the `/display/release` slot arg.
|
||||
pub gen: u64,
|
||||
}
|
||||
|
||||
impl VirtualDisplayManager {
|
||||
/// Snapshot the current monitor for the mgmt `/display/state` endpoint. `None` when Idle.
|
||||
pub(crate) fn snapshot(&self) -> Option<ManagedInfo> {
|
||||
let st = self.state.lock().unwrap();
|
||||
let (mon, state, sessions, expires_in_ms) = match &*st {
|
||||
MgrState::Idle => return None,
|
||||
MgrState::Active { mon, refs } => (mon, "active", *refs, None),
|
||||
MgrState::Lingering { mon, until } => {
|
||||
let ms = until.saturating_duration_since(Instant::now()).as_millis() as u64;
|
||||
(mon, "lingering", 0u32, Some(ms))
|
||||
}
|
||||
};
|
||||
Some(ManagedInfo {
|
||||
backend: self.driver.name(),
|
||||
mode: (mon.mode.width, mon.mode.height, mon.mode.refresh_hz),
|
||||
state,
|
||||
expires_in_ms,
|
||||
sessions,
|
||||
gen: mon.gen,
|
||||
})
|
||||
}
|
||||
|
||||
/// Force-tear-down a LINGERING monitor now (the `/display/release` endpoint) — so a
|
||||
/// physical-screen user gets their screen back without waiting out the linger. An Active monitor
|
||||
/// is refused (stopping a live session is session management, not display management). Returns
|
||||
/// `true` if a lingering monitor was released.
|
||||
pub(crate) fn force_release(&self) -> bool {
|
||||
let Some(dev) = self.device_handle() else {
|
||||
return false;
|
||||
};
|
||||
let mut st = self.state.lock().unwrap();
|
||||
if matches!(&*st, MgrState::Lingering { .. }) {
|
||||
if let MgrState::Lingering { mon, .. } = std::mem::replace(&mut *st, MgrState::Idle) {
|
||||
// SAFETY: `teardown` needs a live control handle; `dev` is from `device_handle()`
|
||||
// (cached handles are never closed — a dead one is retired, kept alive; see
|
||||
// `DeviceSlot`). `mon` was moved out of the `Lingering` state under the `state` lock,
|
||||
// so it is exclusively owned here — no aliasing.
|
||||
unsafe { self.teardown(dev, mon) };
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Snapshot the managed monitor, or `None` when no backend has initialised the manager yet (no
|
||||
/// session has ever run) or it is Idle. Safe to call per management request.
|
||||
pub(crate) fn snapshot() -> Option<ManagedInfo> {
|
||||
VDM.get().and_then(VirtualDisplayManager::snapshot)
|
||||
}
|
||||
|
||||
/// Force-release a lingering monitor now; `false` if nothing was lingering (or the manager is
|
||||
/// uninitialised).
|
||||
pub(crate) fn force_release() -> bool {
|
||||
VDM.get()
|
||||
.map(VirtualDisplayManager::force_release)
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Linger window before a session-less monitor is torn down. The console display-management policy
|
||||
/// wins when configured (`keep_alive`); otherwise the legacy `PUNKTFUNK_MONITOR_LINGER_MS` env knob,
|
||||
/// else the 10 s default.
|
||||
|
||||
Reference in New Issue
Block a user