diff --git a/docs/plans/2026-07-22-gamewindow-slice-8-checkpoint-j-lifetime-shutdown.md b/docs/plans/2026-07-22-gamewindow-slice-8-checkpoint-j-lifetime-shutdown.md new file mode 100644 index 00000000..0a79a8bd --- /dev/null +++ b/docs/plans/2026-07-22-gamewindow-slice-8-checkpoint-j-lifetime-shutdown.md @@ -0,0 +1,169 @@ +# GameWindow Slice 8 Checkpoint J — lifetime and shutdown + +**Status:** Active 2026-07-22. +**Parent:** +[`2026-07-22-gamewindow-slice-8-composition-lifecycle.md`](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. + +## 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: + +```csharp +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. diff --git a/docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md b/docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md index 061f4c55..0ba200db 100644 --- a/docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md +++ b/docs/plans/2026-07-22-gamewindow-slice-8-composition-lifecycle.md @@ -33,7 +33,8 @@ audit. invoke the same pipeline and carry exact typed prior results. Detailed plan: [`2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md`](2026-07-22-gamewindow-slice-8-checkpoint-i-ordered-composition.md). - [ ] J — move the exact retryable shutdown manifest to a focused lifetime - owner and prove all partial-load/reentrant/retry paths. + owner and prove all partial-load/reentrant/retry paths. Detailed plan: + [`2026-07-22-gamewindow-slice-8-checkpoint-j-lifetime-shutdown.md`](2026-07-22-gamewindow-slice-8-checkpoint-j-lifetime-shutdown.md). - [ ] K — in a separate #232 commit, add canonical owner snapshots to every soak checkpoint without weakening the process-memory guard. - [ ] L — corrected-diff reviews, focused and full Release gates, connected