docs(architecture): plan focused window lifetime

This commit is contained in:
Erik 2026-07-22 19:23:43 +02:00
parent 530b4bd8f5
commit 5a55d08106
2 changed files with 171 additions and 1 deletions

View file

@ -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.

View file

@ -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