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:
2026-07-04 20:32:03 +00:00
parent bbd98241e4
commit 87f0ce7997
9 changed files with 889 additions and 1 deletions
+171
View File
@@ -138,6 +138,48 @@
}
}
},
"/api/v1/display/release": {
"post": {
"tags": [
"display"
],
"summary": "Release kept virtual displays",
"description": "Tear down lingering/pinned displays now — so a physical-screen user gets their screen back\nwithout waiting out the linger. `slot` releases one; omit it to release all kept displays.\nActive (streaming) displays are never torn down here (that is session control).",
"operationId": "releaseDisplay",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ReleaseDisplayRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "The number of kept displays released",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ReleaseDisplayResult"
}
}
}
},
"401": {
"description": "Missing or invalid bearer token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
}
}
}
},
"/api/v1/display/settings": {
"get": {
"tags": [
@@ -230,6 +272,38 @@
}
}
},
"/api/v1/display/state": {
"get": {
"tags": [
"display"
],
"summary": "Live virtual displays",
"description": "The host's managed virtual displays right now — active (streaming), lingering (kept after\ndisconnect, counting down to teardown), or pinned (kept indefinitely). See\n`design/display-management.md`.",
"operationId": "getDisplayState",
"responses": {
"200": {
"description": "The live/kept virtual displays",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/DisplayStateResponse"
}
}
}
},
"401": {
"description": "Missing or invalid bearer token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
}
}
}
},
"/api/v1/gpus": {
"get": {
"tags": [
@@ -1693,6 +1767,59 @@
"av1"
]
},
"ApiDisplayInfo": {
"type": "object",
"description": "One live or kept virtual display.",
"required": [
"slot",
"backend",
"mode",
"state",
"sessions"
],
"properties": {
"backend": {
"type": "string",
"description": "Backend name (`pf-vdisplay`, `kwin`, …)."
},
"client": {
"type": [
"string",
"null"
],
"description": "Short client label, when the owner tracks it."
},
"expires_in_ms": {
"type": [
"integer",
"null"
],
"format": "int64",
"description": "Milliseconds until a lingering display is torn down (absent when active/pinned).",
"minimum": 0
},
"mode": {
"type": "string",
"description": "`WIDTHxHEIGHT@HZ`."
},
"sessions": {
"type": "integer",
"format": "int32",
"description": "Live sessions holding the display.",
"minimum": 0
},
"slot": {
"type": "integer",
"format": "int64",
"description": "Stable-enough id for the `/display/release` `slot` argument.",
"minimum": 0
},
"state": {
"type": "string",
"description": "`active` | `lingering` | `pinned`."
}
}
},
"ApiError": {
"type": "object",
"description": "Error envelope for every non-2xx response.",
@@ -2076,6 +2203,21 @@
}
}
},
"DisplayStateResponse": {
"type": "object",
"description": "The host's managed virtual displays right now.",
"required": [
"displays"
],
"properties": {
"displays": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ApiDisplayInfo"
}
}
}
},
"EffectivePolicy": {
"type": "object",
"description": "The six resolved fields after preset expansion — what the lifecycle/registry and the Stage-0 call\nsites read, and what the mgmt API echoes as the \"currently in force\" policy. Pure output of\n[`DisplayPolicy::effective`].",
@@ -2795,6 +2937,35 @@
}
}
},
"ReleaseDisplayRequest": {
"type": "object",
"description": "Request body for `releaseDisplay`.",
"properties": {
"slot": {
"type": [
"integer",
"null"
],
"format": "int64",
"description": "Slot to release (see `state`); omit to release **all** kept displays.",
"minimum": 0
}
}
},
"ReleaseDisplayResult": {
"type": "object",
"description": "Result of a `/display/release`.",
"required": [
"released"
],
"properties": {
"released": {
"type": "integer",
"description": "Number of kept displays torn down.",
"minimum": 0
}
}
},
"RuntimeStatus": {
"type": "object",
"description": "Live host status (changes as clients launch/end sessions).",