acdream/docs/plans/2026-07-22-gamewindow-slice-8-checkpoint-j-lifetime-shutdown.md
Erik 31e6e192b3 refactor(app): extract focused window lifetime
Move the exact retryable shutdown manifest, typed root snapshot, terminal reporting, and native-window-last release out of GameWindow. Keep session and GPU convergence as hard barriers while reporting persistent physical callback cleanup without stranding dependent owners.

Co-authored-by: Codex <codex@openai.com>
2026-07-22 19:43:52 +02:00

9.2 KiB

GameWindow Slice 8 Checkpoint J — lifetime and shutdown

Status: Complete 2026-07-22. Parent: 2026-07-22-gamewindow-slice-8-composition-lifecycle.md, Checkpoint J. Integrated baseline: 530b4bd8; Checkpoint I is complete, the production startup path uses the tested nine-phase pipeline, GameWindow.cs is 1,945 raw lines, App Release passes 3,431 tests / 3 skips, and the complete Release suite passes 7,803 tests / 5 skips. Behavior rule: preserve the accepted disconnect, reset, callback cutoff, frame withdrawal, GL dependency, and native-window-last order. This checkpoint changes lifetime ownership and failure reporting, not gameplay behavior.

Result: GameWindowLifetime now owns the immutable typed root snapshot, retryable staged transaction, structured terminal report, and native window. Persistent physical-detach failures are retried and reported without stranding later owners; session convergence and GPU drain remain hard barriers. Clean native release drops the completed transaction/root graph, while terminal abandonment retains it. GameWindow.cs is 1,625 raw lines, down 14,098 lines (89.7%) from the campaign baseline. The App Release gate passes 3,441 tests / 3 skips; the complete Release gate passes 7,813 / 5 skips; the solution build has only #228's 17 existing test warnings. Three corrected-diff review passes are clean and no retail-divergence row changed.

1. Outcome

Replace GameWindow.CreateShutdownTransaction and the window-owned transaction field with one pre-window GameWindowLifetime. The lifetime owns:

  • the single staged ResourceShutdownTransaction;
  • the first immutable typed shutdown-root snapshot;
  • the native window root once Window.Create succeeds;
  • explicit Active, RetryableIncomplete, Complete, CompleteWithCleanupFailures, and AbandonedIncomplete state;
  • one immutable report returned by every terminal repeated call.

GameWindow.OnClosing performs a synchronous non-terminal attempt while the GL context is current. GameWindow.Dispose retries the retained transaction and then releases the native window. A persistent hard barrier forces the named, logged last-resort native fallback and terminal abandonment; it never reports a clean shutdown.

2. Typed ownership boundary

GameWindowShutdownRoots is a teardown-only aggregate of cohesive records, not a runtime service locator. It exposes no gameplay operations and is captured exactly once when shutdown first starts:

  1. IngressShutdownRoots — quiescence, command/input logical cutoff, physical callback owners, session controller, and native callback binding.
  2. FrameShutdownRoots — atomic frame publication, frame/session bindings, and UI/session late bindings.
  3. LiveShutdownRoots — retained UI, interaction/streaming owners, canonical live runtime, hook/effect/audio owners, and effect state.
  4. RenderShutdownRoots — GPU-flight barrier, private/render frontends, texture/mesh/render resources, dedicated atlas/sky roots, and construction cleanup.
  5. PlatformShutdownRoots — DAT mapping, input context, and GL.

The records retain exact object identities. Operations call those identities directly; no stage stores GameWindow, a callback facade into it, a dictionary, or an untyped service provider. The window may retain borrowed fields until the host itself dies, but only GameWindowLifetime drives release after capture.

3. Transaction policy

Extend ResourceShutdownOperation with a defaulted policy:

ResourceShutdownOperationPolicy.HardBarrier
ResourceShutdownOperationPolicy.ReportAndContinue

Existing call sites remain hard by default. All operations in a stage are attempted independently. Successful operations never replay. A hard failure remains pending and protects later stages. A report-and-continue operation is retried once; a second failure becomes a structured cleanup failure, marks that operation settled, and allows the stage to converge. The transaction exposes its current stage and immutable cleanup failures without swallowing exceptions.

Only physical ingress removal is soft. Logical quiescence, live-session convergence, frame withdrawal, owner disposal, GPU drain, DAT/input/GL release, and all dependency barriers remain hard.

4. Frozen stage order

The manifest outside GameWindow uses this exact order:

  1. host and session barriers
    • stop accepting callbacks;
    • deactivate combat/diagnostic/retained/gameplay/pointer/dispatcher/device paths;
    • quiesce retained/devtools input;
    • dispose the live-session controller and require converged disposal.
  2. physical ingress cleanup — settings VM, retained/gameplay/pointer/ dispatcher/device bindings, retained/devtools input, and native window callbacks; every operation is report-and-continue.
  3. frame borrowers — withdraw the exact atomic frame pair, then frame-root and session/player bindings.
  4. session dependents — late bindings, mouse presentation, retained UI, combat/item/container owners, streamer, and equipped children.
  5. live entities — clear canonical runtime while teardown callbacks live.
  6. effect dispatch edges — live bindings, effect advance, hook registrations.
  7. live entity dependents — lights, presentation, remote teleport, effect state, and audio.
  8. submitted GPU work — hard wait barrier.
  9. render frontends.
  10. shared texture owners.
  11. mesh adapter.
  12. remaining render owners.
  13. dedicated render resources — sky shader then terrain atlas.
  14. failed render construction cleanup.
  15. frame flight owner.
  16. content mappings.
  17. input context.
  18. OpenGL context.
  19. Native window release/fallback outside the transaction and last.

The former late native-callback operation moves to stage 2. Logical cutoff makes copied callbacks inert before the potentially long session close, so a broken event remove cannot prevent F653/transport teardown or strand unrelated owners.

5. State and reporting

GameWindowLifetimeReport contains status, blocked stage, structured soft cleanup failures, and the retained hard/native error when present.

  • TryComplete(roots) captures roots once and never performs native fallback. It is used by OnClosing.
  • CompleteAndReleaseNativeWindow(roots) retries hard work, then disposes the native window. If hard convergence still fails, it retains the blocked-stage report, marks eligible partial owners abandoned, releases the native fallback, and becomes AbandonedIncomplete.
  • Clean transaction + no soft failures becomes Complete.
  • Clean transaction + soft failures becomes CompleteWithCleanupFailures.
  • Reentrant completion is inert while the outer call owns progress.
  • Every terminal repeated call returns the same report and touches no owner.
  • Native window publication is single-owner, pre-Run-safe, and released once.

6. Implementation sequence

  1. Add operation policy, structured cleanup-failure exposure, and focused transaction tests for transient/persistent soft cleanup, mixed hard/soft stages, retry/no-replay, and reentrancy.
  2. Add GameWindowLifetime, report/state types, typed root records, and the manifest factory. Test every stage trace using fakes, optional-null roots, persistent session/GPU barriers, persistent soft detach, clean/native-failure finalization, terminal abandonment, and repeated/reentrant calls.
  3. Publish the native window immediately after creation; replace the window transaction/manifest with one typed root capture and the two narrow lifetime calls. Remove all shutdown feature bodies and update source-shape tests to inspect the focused owner.
  4. Exercise constructed-never-run, Load-never-fired, partial composition prefixes, normal close then Dispose, direct Dispose, transient retry, and optional audio/devtools/retained-UI absence.
  5. Run behavior-order, architecture/ownership, and adversarial failure review passes; correct every finding. Run focused tests, App Release, solution Release build, and complete Release tests.
  6. Reconcile architecture, roadmap, milestones, issues, AGENTS/CLAUDE, and durable memory; commit Checkpoint J as one bisectable ownership unit.

7. Acceptance

  • No shutdown stage or substantial release body remains in GameWindow.
  • No lifetime/manifest type stores GameWindow or a general callback facade.
  • Session and GPU hard failures never run protected dependent stages.
  • Persistent physical-detach failure does not strand frame/live/render/content/ input/GL owners and produces CompleteWithCleanupFailures.
  • Both frame roots become unreachable before any borrower retires.
  • Every acquired owner releases once; absent owners are no-ops; failed owners retry without replaying completed work.
  • Direct, repeated, concurrent-safe/reentrant, close-then-dispose, and every partial-load path are deterministic.
  • Native window release is last. Hard non-convergence becomes named terminal abandonment; repeated calls are inert and preserve the same report.
  • App production builds without warnings; only #228's 17 test warnings remain; focused, App, and complete Release suites pass.
  • No retail-divergence row is added unless review discovers an actual behavior change; this ownership correction itself is behavior-preserving.