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>
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.Createsucceeds; - explicit
Active,RetryableIncomplete,Complete,CompleteWithCleanupFailures, andAbandonedIncompletestate; - 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:
IngressShutdownRoots— quiescence, command/input logical cutoff, physical callback owners, session controller, and native callback binding.FrameShutdownRoots— atomic frame publication, frame/session bindings, and UI/session late bindings.LiveShutdownRoots— retained UI, interaction/streaming owners, canonical live runtime, hook/effect/audio owners, and effect state.RenderShutdownRoots— GPU-flight barrier, private/render frontends, texture/mesh/render resources, dedicated atlas/sky roots, and construction cleanup.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:
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.
physical ingress cleanup— settings VM, retained/gameplay/pointer/ dispatcher/device bindings, retained/devtools input, and native window callbacks; every operation is report-and-continue.frame borrowers— withdraw the exact atomic frame pair, then frame-root and session/player bindings.session dependents— late bindings, mouse presentation, retained UI, combat/item/container owners, streamer, and equipped children.live entities— clear canonical runtime while teardown callbacks live.effect dispatch edges— live bindings, effect advance, hook registrations.live entity dependents— lights, presentation, remote teleport, effect state, and audio.submitted GPU work— hard wait barrier.render frontends.shared texture owners.mesh adapter.remaining render owners.dedicated render resources— sky shader then terrain atlas.failed render construction cleanup.frame flight owner.content mappings.input context.OpenGL context.- 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 byOnClosing.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 becomesAbandonedIncomplete.- 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
- 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.
- 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. - 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.
- 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.
- 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.
- 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
GameWindowor 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.