GUI ownership and retained instance persistence

September 9, 2026 ยท View on GitHub

This is a foundation checkpoint inside M1 of the product completion plan. It establishes manager ownership independent of legacy windows and durable state for the currently implemented canonical document subset. It does not complete M1, replace a shipped GUI, or establish game save/demo compatibility.

Manager ownership

The engine-private idUserInterfaceManaged contract supplies source metadata, reference counts, menu lifetime, memory/transition reporting and background thinking. idUserInterfaceLocal implements that contract using its existing window tree. Game modules retain the public idUserInterface contract; Game API 48 and renderer API 13 are unchanged.

One allocation registry owns all managed objects, including an Alloc() result that has not loaded a resource. Loaded, background-thinking and demo lists are non-owning subsets. Destruction unregisters from every list, including direct editor deletion. Shutdown drains the ownership registry before shared retained resources are destroyed. Level purging respects menu lifetime, references and material GlobalGui() references. Reload copies the resource path before initialization can replace its storage.

These changes repair stale registrations left by direct editor deletion and unowned, uninitialized debug/demo allocations. They also remove manager dereferences of legacy desktops. The existing editor continues to receive its concrete legacy implementation for .guied sources. Pathless allocation and normal loading still construct the legacy implementation at this checkpoint; the separate retained adapter and deferred resource selection were subsequent M1 work. The newer application integration checkpoint adds those routes and coordinates all registered engine views; this document retains the original checkpoint's evidence and limits.

Snapshot contract

Runtime::SaveSnapshot writes version 1 of openq4-ui-instance to a bounded UTF-8 JSON string. RestoreSnapshot validates a candidate before changing any live state. Failure preserves the existing instance; save failure preserves the caller's previous output. The envelope is limited to 128 MiB, rejects duplicate keys and unsupported fields/schema, and validates the document identity and every restored state and presentation value.

Identity includes the canonical format version, document ID, VFS source path and exact source bytes. The source must already be loaded: a snapshot cannot load code or substitute a different document. Embedding source bytes avoids an ambiguous identity in this initial format but increases snapshot size. Shared compiled resources should supply a compact, versioned resource identity before the full corpus is qualified. Raw RML previews have no snapshot contract.

The implemented persistent state includes:

  • All declared application values, with their declared types and ownership.
  • Timeline owners, retarget values, elapsed phase, pause/repeat progress and reduced-motion presentation, reanchored to the receiving monotonic clock.
  • Focus, nested modal scopes and their return focus, and unbound enabled overrides for the implemented button controls.

CVars remain authoritative host data. Restore reads them afresh and evaluates their values together with saved application state in one transaction. A CVar cannot be restored as application-owned state. Changed availability can clear an invalid focus and retarget its feedback while preserving unrelated motion.

Pending actions, hover and armed presses are transient. Saving sanitizes copies of interaction/motion state, preserving continuous feedback toward the persistent state without changing the live instance. Restore clears action queues and press ownership, so a later unmatched release cannot activate a control. Input routing also quarantines held physical sources when the engine restores a checkpoint. After quarantine, the adapter clears logical held latches without changing restored presentation. Releases queued under an earlier ownership generation can retire only quarantined sources, so the first fresh press is not swallowed.

The reserved widget-state object must currently be empty. Text edits, lists, sliders, script clocks and other future widget state are not silently accepted as supported persistence. Game saves still use their existing positional legacy payload, and render-demo GUI payload writing remains disabled. Both require explicit backend framing/migration work before retained source routing changes.

Engine resource lifecycle and diagnostics

The canonical preview uses the complete snapshot across renderer and language generation changes, replacing its previous application-values-only restore. Generation checks run before document loading as well as drawing, including when a view was closed while the renderer changed. Closing a view releases its runtime without resetting host-owned font/target caches; shutdown and generation invalidation own those bounded resources. The shared dictionary explicitly notifies this boundary after loading. A same-language reload or a switch between languages using the same code page therefore invalidates retained text/fonts as well.

The current engine host has one preview context. Before manager-owned retained views are connected, invalidation must capture every affected instance, shut down all affected contexts, reset the host once, and restore every view. World GUIs also require an explicit output-surface contract: their geometry collection cannot use the fullscreen preview submission path.

ui_retainedCheckpoint save|restore provides one bounded in-memory diagnostic slot. It never writes game saves or dispatches actions. Closing the preview clears the slot. The capture harness accepts a separate --retained-resume-script for checks after vid_restart; unlike the default fixture replay, this can prove that state survives a restart.

Qualification

All four native suites pass: retained runtime, canonical documents, state and vectors. Snapshot cases include exact source identity, malformed/duplicate JSON, wrong ownership/types, failed binding evaluation, current host values, active/paused/retargeted/repeating/reduced motion, nested modal return focus, held input and large-clock rejection. Combined Runtime/Input checks verify that an old press/release cannot activate after restore and the first fresh action works once. The production transport test additionally covers stale keyboard and pointer releases across ownership generations.

The extracted production manager tests cover uninitialized and failed loads, direct editor deletion/reopen, loaded/demo membership, shared/unique instances, menu references, material retention, self/peer deletion, address reuse during thinking/reload, and shutdown. Production cursor, presentation, window-state, CVar-source, layer-pool, input and startup-language checks pass. The manager test is included in both script-validation workflows.

The first SP/OpenGL gameplay capture passed but adding full video restart to the MP/Vulkan capture exposed an existing direct call to OpenGL initialization from the full restart helper. The failed evidence remains under .tmp/ui/persistence-review/mp-vulkan-200/; it is not accepted. The restart helper now uses the active renderer's initialization path and reloads images once, through the same backend-aware initialization used at startup. renderer_vid_restart_route.py compiles the production restart and startup methods for both GL and Vulkan, checking repeated restarts, window preferences, resource ordering and exactly one image reload. The existing font-lifecycle contract and mutation checks also pass. Both validation workflows run the new route test.

Final captures on 9 September 2026 used the staged Windows debug build, hidden windowed mode, disabled host mouse/controller input, and the registered engine screenshot command after actual map gameplay. Both passed trace/provenance verification and visual review. Each run restored one 94,317-byte in-memory checkpoint, reloaded the same language, restarted the renderer, retained modal focus/availability and restored the prior focus when the modal was popped. No pending actions survived. SP stayed paused and resumed on close; MP continued simulation while the interface owned input.

Evidence under .tmp/ui/persistence-review/ConfigurationResult
sp-gl-125-r2SP airdefense1, OpenGL, 1280x720, 125% densityExit 0; zero warnings/errors; snapshot/ownership checks pass
mp-vulkan-200-r2MP q4dm1, Vulkan, 1920x1080, 200% density, explicit auto-joinExit 0; zero errors/retained diagnostics; snapshot/ownership checks pass

MP reports 94 warnings with the same 86 unique messages as the preceding checkpoint. Compared with its 93-warning run, the full renderer restart repeats the existing vertex array range in virtual memory (SLOW) warning once. There are no new unique warnings. These content/backend diagnostics remain open.

Client SHA-256 is 6d37792aa468b494c289ee67f18bd6a048ab6918a8e2e31b2cc3cd2853a41d31; the renderer module hashes are 18812bde2a30a8bebee9d0a373821c9cbef69f1fc5aa4f027054b33f931da383 (GL) and 369598d94f1e4ea226e7e0cdbcceb028202361ff029e52fbdcd0a8aa10296214 (Vulkan). The unchanged companion source revision is 1cd33980f072ac3d78978a07b388b4b6fe6b5eb2. Screenshot hashes, respectively:

93306463008e27d1bfa7782b9b6164b3c475d7f73ee614afb3f23b9060171890
f29f785719ffb2e3899f9186b05c4e90a0f2eaf531b5393580ef9a71be46b527

The local summary.json binds source and binary hashes, capture metadata, oracle results and warning comparisons. These are existing preview fixtures, not a finished settings screen, game save round trip, world surface or native editor. Shared GUI was requested but reported zero owned views: 20 SP and 18 MP views used its fallback path. No shipping performance claim is made.

Reproduce with capture_legacy_baseline.py, the interaction-smoke.q4ui document, snapshot-smoke.cfg setup and snapshot-resume.cfg resume script, --retained-open --language-reload --video-restart --shared-gui, the table's mode/backend/dimensions/density, installed assets, and a fresh output folder. Run verify_snapshot_capture.py <capture-folder> and review the engine TGA. The verifier checks exact source/script and artifact hashes, generated command order, fresh engine-log traces and ownership timing; setup is never replayed after restart to manufacture the expected state.

The Windows Meson wrapper also requires numeric arguments as strings when called from PowerShell (for example -j '8'); an unquoted integer reaches a string-only argument scanner. This existing tooling issue was worked around, without changing the wrapper.

The full GUI corpus, native visual editor, platform matrix and shipping performance gates remain required by the product plan. No external implementation code is incorporated.