feat(vdisplay): ship keep_alive=forever (gaming-rig) — Windows MgrState::Pinned

Completes the last §6A-era preset. The Linux registry already resolved forever→Pinned (pure
lifecycle machine); the blockers were the Windows manager, the mgmt reject, and the console tag:

- Windows manager: new `MgrState::Pinned { mon }` — the last-released monitor under keep_alive=forever
  is kept indefinitely (like Lingering but the linger timer never fires). A reconnect preempts +
  recreates it (same as Lingering — a reused IddCx swap-chain is dead), snapshot reports "pinned",
  and `force_release` (POST /display/release, the §8 escape hatch) frees a pinned monitor. release()
  branches on the new `keep_alive_forever()`; all MgrState matches made exhaustive over Pinned.
- mgmt PUT /display/settings: stop rejecting keep_alive=forever (now honored on both platforms with a
  release path). OpenAPI regenerated.
- web: un-disable the gaming-rig preset (DISABLED_PRESETS now empty) — one-click applies.

Linux paths + web/tsc/openapi green; 47 vdisplay tests pass. The Windows manager.rs is #[cfg(windows)]
(not compilable on the Linux dev box) — build-verified + on-glass validation on .173 to follow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-05 17:26:43 +00:00
parent a0546b36b6
commit ccbd7e8880
4 changed files with 85 additions and 55 deletions
+2 -2
View File
@@ -268,7 +268,7 @@
"display" "display"
], ],
"summary": "Set the display-management policy", "summary": "Set the display-management policy",
"description": "Persists a new policy (validated + clamped) and applies it from the next connect/teardown — a\nrunning session keeps the display it opened on. `keep_alive: forever` is rejected until the\ndisplay-lifecycle stage ships (it would keep physical monitors dark indefinitely with no release\npath yet).", "description": "Persists a new policy (validated + clamped) and applies it from the next connect/teardown — a\nrunning session keeps the display it opened on. `keep_alive: forever` (the gaming-rig preset) is\nhonored (the display is Pinned; free it via `POST /display/release`).",
"operationId": "setDisplaySettings", "operationId": "setDisplaySettings",
"requestBody": { "requestBody": {
"content": { "content": {
@@ -292,7 +292,7 @@
} }
}, },
"400": { "400": {
"description": "An option value is not yet supported (e.g. keep_alive forever)", "description": "Malformed policy body",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
+5 -15
View File
@@ -1065,9 +1065,8 @@ async fn get_display_settings() -> Json<DisplaySettingsState> {
/// Set the display-management policy /// Set the display-management policy
/// ///
/// Persists a new policy (validated + clamped) and applies it from the next connect/teardown — a /// Persists a new policy (validated + clamped) and applies it from the next connect/teardown — a
/// running session keeps the display it opened on. `keep_alive: forever` is rejected until the /// running session keeps the display it opened on. `keep_alive: forever` (the gaming-rig preset) is
/// display-lifecycle stage ships (it would keep physical monitors dark indefinitely with no release /// honored (the display is Pinned; free it via `POST /display/release`).
/// path yet).
#[utoipa::path( #[utoipa::path(
put, put,
path = "/display/settings", path = "/display/settings",
@@ -1076,7 +1075,7 @@ async fn get_display_settings() -> Json<DisplaySettingsState> {
request_body = crate::vdisplay::policy::DisplayPolicy, request_body = crate::vdisplay::policy::DisplayPolicy,
responses( responses(
(status = OK, description = "Policy stored; the new state", body = DisplaySettingsState), (status = OK, description = "Policy stored; the new state", body = DisplaySettingsState),
(status = BAD_REQUEST, description = "An option value is not yet supported (e.g. keep_alive forever)", body = ApiError), (status = BAD_REQUEST, description = "Malformed policy body", body = ApiError),
(status = INTERNAL_SERVER_ERROR, description = "Policy could not be persisted", body = ApiError), (status = INTERNAL_SERVER_ERROR, description = "Policy could not be persisted", body = ApiError),
(status = UNAUTHORIZED, description = "Missing or invalid bearer token", body = ApiError), (status = UNAUTHORIZED, description = "Missing or invalid bearer token", body = ApiError),
) )
@@ -1084,17 +1083,8 @@ async fn get_display_settings() -> Json<DisplaySettingsState> {
async fn set_display_settings( async fn set_display_settings(
ApiJson(policy): ApiJson<crate::vdisplay::policy::DisplayPolicy>, ApiJson(policy): ApiJson<crate::vdisplay::policy::DisplayPolicy>,
) -> Response { ) -> Response {
use crate::vdisplay::policy::KeepAlive; // `keep_alive: forever` (the gaming-rig preset) is now honored: the display is Pinned (Linux
// Reject options this build can't honor yet, so the console can't promise a behavior that won't // registry + Windows `MgrState::Pinned`) and freed via `POST /display/release` (the escape hatch).
// happen. `keep_alive: forever` (directly or via the `gaming-rig` preset) needs the Pinned
// lifecycle + a release path; until then it would strand physical monitors dark.
if policy.effective().keep_alive == KeepAlive::Forever {
return api_error(
StatusCode::BAD_REQUEST,
"keep_alive `forever` (and the `gaming-rig` preset) is not available yet — it arrives \
with the display-lifecycle stage. Use a fixed duration for now.",
);
}
if let Err(e) = crate::vdisplay::policy::prefs().set(policy) { if let Err(e) = crate::vdisplay::policy::prefs().set(policy) {
return api_error( return api_error(
StatusCode::INTERNAL_SERVER_ERROR, StatusCode::INTERNAL_SERVER_ERROR,
@@ -131,6 +131,12 @@ enum MgrState {
Idle, Idle,
Active { mon: Monitor, refs: u32 }, Active { mon: Monitor, refs: u32 },
Lingering { mon: Monitor, until: Instant }, Lingering { mon: Monitor, until: Instant },
/// `keep_alive = forever` (gaming-rig): the monitor is kept indefinitely after the last session
/// leaves — like `Lingering` but the linger timer never tears it down. A reconnect preempts +
/// recreates it (same as `Lingering`, since a reused IddCx swap-chain is dead); only the mgmt
/// `/display/release` (or host shutdown) frees it. The physical screens stay off (exclusive) for
/// the box's life — the §8 release-now escape hatch (`force_release`) is the way back.
Pinned { mon: Monitor },
} }
/// The manager's control-device cache. Reopenable: a driver upgrade / WUDFHost restart kills the /// The manager's control-device cache. Reopenable: a driver upgrade / WUDFHost restart kills the
@@ -386,22 +392,28 @@ impl VirtualDisplayManager {
let mut state = self.state.lock().unwrap(); let mut state = self.state.lock().unwrap();
let dev = self.ensure_device()?; let dev = self.ensure_device()?;
// IDD-push: a new connection while a monitor is LINGERING is a single-client RECONNECT (the // IDD-push: a new connection while a monitor is kept (LINGERING or PINNED) is a single-client
// prior session fully released). A REUSED IddCx swap-chain is DEAD, so reusing it hands a black // RECONNECT (the prior session fully released). A REUSED IddCx swap-chain is DEAD, so reusing it
// screen — PREEMPT: tear the lingering monitor down (its key/topology are restored) and create a // hands a black screen — PREEMPT: tear the kept monitor down (its key/topology are restored) and
// fresh one. The old session's lease is gen-stamped, so its later drop is a no-op. // create a fresh one. The old session's lease is gen-stamped, so its later drop is a no-op.
// //
// ONLY Lingering, NOT Active: an Active monitor still has a lease held — that's the build-retry // ONLY the kept states, NOT Active: an Active monitor still has a lease held — that's the
// path (`build_pipeline_with_retry` holds one lease across all attempts) or a concurrent session, // build-retry path (`build_pipeline_with_retry` holds one lease across all attempts) or a
// NOT a reconnect. Preempting Active would tear a live session down AND churn REMOVE→ADD on every // concurrent session, NOT a reconnect. Preempting Active would tear a live session down AND churn
// retry — the per-cold-start monitor churn that exhausts the IddCx slot pool and wedges ADD at // REMOVE→ADD on every retry — the per-cold-start monitor churn that exhausts the IddCx slot pool
// 0x80070490. Active falls through to the JOIN path below (refcount++, no ADD). // and wedges ADD at 0x80070490. Active falls through to the JOIN path below (refcount++, no ADD).
if matches!(*state, MgrState::Lingering { .. }) { if matches!(*state, MgrState::Lingering { .. } | MgrState::Pinned { .. }) {
if let MgrState::Lingering { mon, .. } = std::mem::replace(&mut *state, MgrState::Idle) let taken = match std::mem::replace(&mut *state, MgrState::Idle) {
{ MgrState::Lingering { mon, .. } | MgrState::Pinned { mon } => Some(mon),
other => {
*state = other;
None
}
};
if let Some(mon) = taken {
tracing::info!( tracing::info!(
old_target = mon.target_id, old_target = mon.target_id,
"IDD-push reconnect — preempting the lingering monitor, recreating a fresh one" "IDD-push reconnect — preempting the kept (lingering/pinned) monitor, recreating a fresh one"
); );
// SAFETY: `teardown` requires `dev` to be a valid control handle; `dev` is the value // SAFETY: `teardown` requires `dev` to be a valid control handle; `dev` is the value
// `ensure_device()` returned above (cached handles are never closed — a dead one is // `ensure_device()` returned above (cached handles are never closed — a dead one is
@@ -457,12 +469,14 @@ impl VirtualDisplayManager {
return Ok(self.output_for(mon)); return Ok(self.output_for(mon));
} }
// Idle or Lingering: repurpose a lingering monitor / create a fresh one → Active{refs:1}. // Idle or kept: repurpose a kept monitor / create a fresh one → Active{refs:1}. (In practice a
// kept Lingering/Pinned monitor was already preempted → Idle above; this arm is the defensive
// reuse path if a race left one here — it must stay exhaustive over `Pinned` regardless.)
let mon = match std::mem::replace(&mut *state, MgrState::Idle) { let mon = match std::mem::replace(&mut *state, MgrState::Idle) {
MgrState::Lingering { mut mon, .. } => { MgrState::Lingering { mut mon, .. } | MgrState::Pinned { mut mon } => {
tracing::info!( tracing::info!(
backend = self.driver.name(), backend = self.driver.name(),
"virtual monitor reused (reconnect within the linger window)" "virtual monitor reused (reconnect to a kept monitor)"
); );
if mon.mode != mode { if mon.mode != mode {
// SAFETY: `reconfigure` needs an exclusive `&mut Monitor` and only touches the live // SAFETY: `reconfigure` needs an exclusive `&mut Monitor` and only touches the live
@@ -747,7 +761,9 @@ impl VirtualDisplayManager {
fn release(&self, gen: u64) { fn release(&self, gen: u64) {
let mut state = self.state.lock().unwrap(); let mut state = self.state.lock().unwrap();
let stale = match &*state { let stale = match &*state {
MgrState::Active { mon, .. } | MgrState::Lingering { mon, .. } => mon.gen != gen, MgrState::Active { mon, .. }
| MgrState::Lingering { mon, .. }
| MgrState::Pinned { mon } => mon.gen != gen,
MgrState::Idle => true, MgrState::Idle => true,
}; };
if stale { if stale {
@@ -758,6 +774,14 @@ impl VirtualDisplayManager {
mon, mon,
refs: refs - 1, refs: refs - 1,
}, },
// Last session left: keep the monitor forever (Pinned) under `keep_alive = forever`,
// else linger for the policy window before the timer tears it down.
MgrState::Active { mon, .. } if keep_alive_forever() => {
tracing::info!(
"virtual-display: last session left — PINNED (keep_alive=forever); free via /display/release"
);
MgrState::Pinned { mon }
}
MgrState::Active { mon, .. } => { MgrState::Active { mon, .. } => {
let ms = linger_ms(); let ms = linger_ms();
tracing::info!( tracing::info!(
@@ -918,7 +942,7 @@ fn resolve_render_pin() -> Option<LUID> {
pub(crate) struct ManagedInfo { pub(crate) struct ManagedInfo {
pub backend: &'static str, pub backend: &'static str,
pub mode: (u32, u32, u32), pub mode: (u32, u32, u32),
/// `"active"` | `"lingering"`. /// `"active"` | `"lingering"` | `"pinned"`.
pub state: &'static str, pub state: &'static str,
/// Milliseconds until a lingering monitor is torn down (`None` when active). /// Milliseconds until a lingering monitor is torn down (`None` when active).
pub expires_in_ms: Option<u64>, pub expires_in_ms: Option<u64>,
@@ -939,6 +963,8 @@ impl VirtualDisplayManager {
let ms = until.saturating_duration_since(Instant::now()).as_millis() as u64; let ms = until.saturating_duration_since(Instant::now()).as_millis() as u64;
(mon, "lingering", 0u32, Some(ms)) (mon, "lingering", 0u32, Some(ms))
} }
// Pinned (keep_alive=forever): kept indefinitely, no expiry — the console shows "Pinned".
MgrState::Pinned { mon } => (mon, "pinned", 0u32, None),
}; };
Some(ManagedInfo { Some(ManagedInfo {
backend: self.driver.name(), backend: self.driver.name(),
@@ -950,20 +976,28 @@ impl VirtualDisplayManager {
}) })
} }
/// Force-tear-down a LINGERING monitor now (the `/display/release` endpoint) — so a /// Force-tear-down a kept (LINGERING **or** PINNED) monitor now (the `/display/release` endpoint) —
/// physical-screen user gets their screen back without waiting out the linger. An Active monitor /// so a physical-screen user gets their screen back without waiting out the linger, and it is the §8
/// is refused (stopping a live session is session management, not display management). Returns /// escape hatch that frees a `keep_alive=forever` (Pinned) monitor. An Active monitor is refused
/// `true` if a lingering monitor was released. /// (stopping a live session is session management, not display management). Returns `true` if a kept
/// monitor was released.
pub(crate) fn force_release(&self) -> bool { pub(crate) fn force_release(&self) -> bool {
let Some(dev) = self.device_handle() else { let Some(dev) = self.device_handle() else {
return false; return false;
}; };
let mut st = self.state.lock().unwrap(); let mut st = self.state.lock().unwrap();
if matches!(&*st, MgrState::Lingering { .. }) { if matches!(&*st, MgrState::Lingering { .. } | MgrState::Pinned { .. }) {
if let MgrState::Lingering { mon, .. } = std::mem::replace(&mut *st, MgrState::Idle) { let mon = match std::mem::replace(&mut *st, MgrState::Idle) {
MgrState::Lingering { mon, .. } | MgrState::Pinned { mon } => Some(mon),
other => {
*st = other;
None
}
};
if let Some(mon) = mon {
// SAFETY: `teardown` needs a live control handle; `dev` is from `device_handle()` // 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 // (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, // `DeviceSlot`). `mon` was moved out of the kept state under the `state` lock,
// so it is exclusively owned here — no aliasing. // so it is exclusively owned here — no aliasing.
unsafe { self.teardown(dev, mon) }; unsafe { self.teardown(dev, mon) };
return true; return true;
@@ -996,16 +1030,10 @@ fn linger_ms() -> u64 {
return match eff.keep_alive.linger() { return match eff.keep_alive.linger() {
Linger::Immediate => 0, Linger::Immediate => 0,
Linger::For(d) => d.as_millis() as u64, Linger::For(d) => d.as_millis() as u64,
// Pinned (keep forever) is built in the display-lifecycle stage; until then fall back to // `forever` is handled BEFORE this by `keep_alive_forever()` in `release` (→ `Pinned`), so
// the default rather than silently keeping the monitor — and thus the physical screens — // this arm is only reached defensively (e.g. a caller that resolves ms without the pin
// dark indefinitely. (The mgmt PUT also rejects `forever` at Stage 0, so this is defensive.) // check) — fall back to the default rather than a huge linger.
Linger::Forever => { Linger::Forever => 10_000,
tracing::warn!(
"display policy: keep_alive=forever not yet honored — lingering 10 s \
(Pinned lands in the display-lifecycle stage)"
);
10_000
}
}; };
} }
std::env::var("PUNKTFUNK_MONITOR_LINGER_MS") std::env::var("PUNKTFUNK_MONITOR_LINGER_MS")
@@ -1014,6 +1042,17 @@ fn linger_ms() -> u64 {
.unwrap_or(10_000) .unwrap_or(10_000)
} }
/// Whether the configured console policy's `keep_alive` resolves to **forever** (`Pinned`) — the
/// gaming-rig preset. `release` uses this to keep the last-released monitor indefinitely instead of
/// lingering. Unconfigured hosts are never forever (default is a short linger).
fn keep_alive_forever() -> bool {
use crate::vdisplay::policy::{prefs, Linger};
prefs()
.configured_effective()
.map(|eff| matches!(eff.keep_alive.linger(), Linger::Forever))
.unwrap_or(false)
}
/// The effective display topology for a freshly-created monitor (never `Auto`): the console policy's /// The effective display topology for a freshly-created monitor (never `Auto`): the console policy's
/// [`effective_topology`](crate::vdisplay::effective_topology) when configured, else the legacy /// [`effective_topology`](crate::vdisplay::effective_topology) when configured, else the legacy
/// `PUNKTFUNK_NO_ISOLATE` env knob (`Extend`) / `Exclusive` (today's default). `Extend` leaves the IDD /// `PUNKTFUNK_NO_ISOLATE` env knob (`Extend`) / `Exclusive` (today's default). `Extend` leaves the IDD
+4 -3
View File
@@ -572,9 +572,10 @@ const apiErrorMessage = (err: unknown): string | undefined => {
return err ? String(err) : undefined; return err ? String(err) : undefined;
}; };
/** `gaming-rig` expands to `keep_alive: forever`, which the host still rejects (Windows has no /** Presets the host can't honor yet (one-click apply would 400) are surfaced but disabled. Empty
* Pinned state yet) — surface it, but disabled, rather than let the one-click apply 400. */ * now that `gaming-rig` (`keep_alive: forever`) ships: the display is Pinned (Linux + Windows) and
const DISABLED_PRESETS: ReadonlySet<string> = new Set(["gaming-rig"]); * freed via Release. */
const DISABLED_PRESETS: ReadonlySet<string> = new Set<string>();
const PRESET_LABEL: Record<string, () => string> = { const PRESET_LABEL: Record<string, () => string> = {
custom: m.display_preset_custom, custom: m.display_preset_custom,