docs(architecture): plan GameWindow render-frame extraction

This commit is contained in:
Erik 2026-07-22 04:11:58 +02:00
parent 9512404e25
commit f316bbd817
3 changed files with 545 additions and 73 deletions

View file

@ -605,89 +605,76 @@ visual-distance reduction.
## Per-Frame Update Order (current runtime) ## Per-Frame Update Order (current runtime)
``` ```
1. Network tick UpdateFrameOrchestrator
└── Drain inbound queue → process CreateObject, UpdateMotion, 1. retry retained live-entity teardown tombstones
UpdatePosition, PlayerTeleport → create/update GameEntities 2. normalize/publish the host and PhysicsScript clocks
3. converge streaming origin, readiness, residence, and rescued projections
2. Streaming tick 4. sample semantic input, raw mouse, and combat intent
└── Compute observer position → load/unload landblocks → 5. advance local/ordinary/static live objects and deferred effect tails
create terrain + scenery GameEntities 6. drain inbound session traffic in one GpuWorldState mutation batch
7. run the local post-network CommandInterpreter position tail
3. Input tick (player mode only) 8. reconcile roots, children, emitters, and lights without advancing time
└── InputDispatcher scopes → PlayerMovementController → 9. expire liveness
MotionInterpreter/body prediction → ResolveWithTransition → 10. advance local teleport/reveal/tunnel presentation
send MoveToState/AutonomousPosition to server 11. evaluate one-shot player-mode entry
12. publish fly/chase/player camera presentation
4. Entity / animation tick
└── Current code still has scattered world/entity state. L.1 owns
animation parity; L.2 owns movement/collision conformance.
5. Render tick
└── Read current entity mesh refs, draw
TerrainModernRenderer + WbDrawDispatcher + EnvCellRenderer
(frustum cull, translucency pass, portal visibility, etc.)
World Wb translucents + Scene particles submit to RetailAlphaQueue;
RetailPViewRenderer flushes the landscape scope before the conditional
depth clear and GameWindow flushes the final scope before private
viewports/UI. Sky/private viewport particles remain immediate.
Projectiles remain ordinary live-entity draws; there is no global or
projectile-specific render pass.
6. Plugin tick
└── Fire IEvents, drain IActions queue
6a. UI tick
IPanelHost.Draw → iterate registered IPanel instances, build
ViewModels from IGameState, dispatch user Commands via ICommandBus.
Backend-agnostic — ImGui or custom retail-look draws here depending
on which is compiled in. See docs/plans/2026-04-24-ui-framework.md.
``` ```
`GameWindow.OnUpdate` owns only the profiler scope and one orchestrator handoff.
The accepted host order above is preserved as TS-53; it is not overclaimed as
the exact retail `Client::UseTime` order. See the completed Slice 6 ledger and
`memory/project_gamewindow_decomposition.md`.
The render callback currently owns the GPU frame boundary and draw graph. Slice
7 extracts that graph without changing its order: per-resource frame begins and
render-thread uploads; world/PView and its two shared-alpha scopes; portal and
paperdoll private viewports; retained gameplay UI; ImGui devtools; screenshot;
then the GPU-flight close in `finally`. Both UI stacks coexist in the same frame.
--- ---
## Render Pipeline (SSOT — current state + unified-PView target) ## Render Pipeline (SSOT — current accepted state)
> The modern path (Phase N.5, mandatory) is > The modern path (Phase N.5, mandatory) is
> `WbDrawDispatcher` (entities) + `EnvCellRenderer` (indoor cell shells) + > `WbDrawDispatcher` (entities) + `EnvCellRenderer` (indoor cell shells) +
> `TerrainModernRenderer` (terrain), fed by the portal-visibility stack. This section is > `TerrainModernRenderer` (terrain), fed by the unified PView stack. This is the
> the authoritative description of how indoor/outdoor rendering is *supposed* to work and > authoritative current draw model; the 2026-05-31 reset handoff is historical.
> where the code currently diverges. Canonical reset handoff:
> `docs/research/2026-05-31-render-architecture-reset-handoff.md`.
**The principle (retail PView).** acdream must render the world the way retail does — **One visibility owner.** `RetailPViewRenderer.DrawInside` is the production
through **one** portal-visibility traversal whose output **gates every geometry type world gate. Its root is the collided camera/viewer cell, or the synthetic
uniformly**. From the player's cell, walk the portal graph; each visible cell carries a outdoor cell adaptation; the player's current cell separately owns sunlight
screen-space clip region (its portal opening, recursively intersected); the **outside** and indoor lighting. A null root exists only for login/debug/streaming-gap
(terrain + outdoor scenery) is reached only through **exit portals** and is clipped to fallback frames. `RetailPViewRenderer` is the one authoritative PView owner and
those openings. Interior cell shells, interior statics, and the outside are **all** product family: it builds the main frame, deliberate per-building exterior
clipped to their PView region. This is why retail is **seamless by construction**. Decomp floods for the synthetic outdoor root, and separate interior-root look-in
anchors: `PView::ConstructView` (`:433750`), `InitCell` (`:432896`), `DrawCells` frames. No second per-frame ACME visibility BFS competes with that family.
(`:432715`), `CEnvCell::find_visible_child_cell` (`:311397`), `SmartBox::update_viewer`
(`:92761`). Reference port acdream owns but never invokes: WB `RenderInsideOut` /
`VisibilityManager`.
**The one rule:** *compute visibility once; enforce it once, for all geometry.* Indoors, **Current draw discipline.** Outside-view slices draw sky, terrain, and outdoor
you see the outside **only** through portal openings (clipped); an empty outside-view statics first. Interior-root building look-ins then punch all entry apertures
(windowless interior) draws **no** outdoor geometry. Outdoors, the gate is "everything." before drawing their shell and contents. The landscape shared-alpha scope
flushes before the root-specific depth boundary. Interior roots perform the
conditional depth clear and then write true-depth exit seals; the synthetic
outdoor root retains world depth and writes far-Z building-entry punches.
Opaque EnvCell shells, immediate far-to-near transparent EnvCell shells, cell
statics and their particles, then the surviving main-stage dynamics and their
particles follow. Look-in and outside-stage dynamics intentionally draw in
their earlier landscape phases. The final world shared-alpha scope flushes
before private portal/paperdoll viewports and UI.
**Current divergence (the patchwork — what the reset must fix).** acdream computes the The modern renderer intentionally does not hard-clip every shell or entity to
visibility correctly (`CellVisibility.ComputeVisibility``PortalVisibilityBuilder.Build`, the accumulated polygon. It combines PView admission and viewcone checks with
a `ConstructView` port → `ClipFrameAssembler`) but then **enforces it three different, retail's punch/seal depth discipline; terrain/outside slices use the bounded
inconsistent ways**: clip-plane/scissor adaptation. World Wb translucents and Scene particles share
1. `TerrainModernRenderer` — gated by `TerrainClipMode {Skip|Scissor|Planes}` (the Scissor one stable far-to-near `RetailAlphaQueue`; EnvCell transparent shells and
fallback over-includes). private viewports remain immediate. Projectiles are ordinary live-entity draws,
2. `EnvCellRenderer` — gated by the per-cell clip slot (≈correct; the shells DO render — never a separate global pass.
proven by the `[shell]` probe, `ACDREAM_PROBE_SHELL`).
3. `WbDrawDispatcher` — gated by `ParentCellId ∈ visibleCellIds`, **but outdoor stabs
(`ParentCellId==null`) bypass the gate** → outdoor scenery/terrain shows from inside
(issue #78).
Three gates that must agree but don't → structural seams (transparent walls, Retail anchors are `SmartBox::RenderNormalMode @ 0x00453AA0`,
terrain-through-floor, grey enclosure). **The reset consolidates them into the single `PView::DrawInside @ 0x005A5860`, `PView::DrawCells @ 0x005A4840`,
PView gate** (outside content clipped to the `OutsideView` region; no `ParentCellId==null` `LScape::draw @ 0x00506330`, `D3DPolyRender::FlushAlphaList @ 0x0059D2E0`,
bypass; no Scissor over-include). This is a **consolidation of existing machinery** and `SceneTool::EndFrame @ 0x0043FB30`. The deliberate modern/PView
(`PortalVisibilityBuilder` + `ClipFrame`), not a rewrite. Do NOT add a fourth special-case adaptations are audited in the retail divergence register; Slice 7 changes
gate to mask a seam — that anti-pattern produced the patchwork. their ownership only, not their behavior.
### Streaming publication ownership ### Streaming publication ownership

View file

@ -179,7 +179,7 @@ src/AcDream.App/
├── RuntimeOptions.cs # typed startup options (Rule 4) ├── RuntimeOptions.cs # typed startup options (Rule 4)
├── Rendering/ ├── Rendering/
│ ├── GameWindow.cs # thin: GL/window lifecycle + delegates per-frame to RenderFrameOrchestrator │ ├── GameWindow.cs # thin: GL/window lifecycle + delegates per-frame to RenderFrameOrchestrator
│ ├── RenderFrameOrchestrator.cs # per-frame draw order (sky → terrain → opaque → trans → particles → debug → UI) │ ├── RenderFrameOrchestrator.cs # GPU-flight boundary + typed world/private/UI render phases
│ ├── LiveEntityAnimationScheduler.cs # shipped: ordinary live-object update workset │ ├── LiveEntityAnimationScheduler.cs # shipped: ordinary live-object update workset
│ ├── LiveEntityAnimationPresenter.cs # final part-pose/mesh/effect composition after scheduler output │ ├── LiveEntityAnimationPresenter.cs # final part-pose/mesh/effect composition after scheduler output
│ ├── RetailStaticAnimatingObjectScheduler.cs # shipped: separate static-animation workset │ ├── RetailStaticAnimatingObjectScheduler.cs # shipped: separate static-animation workset
@ -606,6 +606,10 @@ registered; this behavior-preserving extraction introduced no new divergence.
#### Slice 7 — extract `RenderFrameOrchestrator` #### Slice 7 — extract `RenderFrameOrchestrator`
Detailed execution ledger:
[`docs/plans/2026-07-22-gamewindow-slice-7-render-frame-orchestration.md`](../plans/2026-07-22-gamewindow-slice-7-render-frame-orchestration.md)
(active).
Move the complete draw graph and its reusable frame-local scratch state into a Move the complete draw graph and its reusable frame-local scratch state into a
GL-owning App collaborator. Preserve the exact modern pipeline order, clip GL-owning App collaborator. Preserve the exact modern pipeline order, clip
routing, PView flood, landscape/opaque/shared-alpha flush boundaries, routing, PView flood, landscape/opaque/shared-alpha flush boundaries,

View file

@ -0,0 +1,481 @@
# GameWindow Slice 7 — render-frame orchestration
**Status:** Active 2026-07-22.
**Parent program:** [`docs/architecture/code-structure.md`](../architecture/code-structure.md), Slice 7.
**Baseline:** `9512404e`; `GameWindow.cs` is 7,026 raw lines, 241 fields, and
108 methods. The Release suite passes 7,182 tests / 5 fixture or environment
skips. The 314-second lifecycle/reconnect gate and 394-second synchronized
nine-stop resource soak pass.
**Behavior rule:** This is an ownership extraction. Preserve every accepted
world/PView/alpha/private-viewport/UI draw edge, render-thread publication
edge, GL-state boundary, and GPU-flight failure contract. Do not combine the
extraction with a render-quality change, visibility correction, frame-order
port, or resource-lifetime redesign.
## Progress ledger
- [ ] A — freeze the complete render graph, correct the architecture SSOT, and
introduce data-only frame contracts plus deterministic order/failure tests.
- [ ] B — extract paperdoll, panel-layout/devtools, and render-diagnostics leaf
owners while `GameWindow` still orders the frame.
- [ ] C — extract frame-resource begin/upload/publication and live display,
and weather/display foundations that do not depend on camera/root
classification.
- [ ] D — extract frame-data classification and reusable world scratch without
duplicating live identity, spatial, visibility, or resource ownership; then
run the dependent audio, sky, lighting, and listener preparation.
- [ ] E — replace `RetailPViewDrawContext`'s callback bag with a typed
`RetailPViewPassExecutor` and data-only inputs.
- [ ] F — extract `WorldSceneRenderer`, including both shared-alpha scopes,
PView/fallback world branches, particles, debug world draw, and completion.
- [ ] G — compose `RenderFrameOrchestrator`, cut `GameWindow.OnRender` to one
handoff, and delete obsolete frame bodies, fields, helpers, and callbacks.
- [ ] H — corrected-diff reviews, full Release suite, connected lifecycle and
soak gates, framebuffer comparison, documentation, memory, and metrics.
Every checked checkpoint is one bisectable architectural commit. A checkpoint
does not complete while a new owner merely delegates a substantial body back to
`GameWindow`.
## 1. Outcome and non-goals
At slice exit, `GameWindow.OnRender` supplies one immutable delta/viewport input
to `RenderFrameOrchestrator`. The orchestrator owns the GPU-flight transaction
and composes focused frame-resource, world-scene, private-presentation, and
diagnostic phases. `GameWindow` remains the construction and shutdown shell; it
does not keep the draw graph, PView pass callbacks, frame scratch, paperdoll
assembly, or performance-title body.
The target ownership graph is:
```text
RenderFrameOrchestrator
├── RenderFrameResourceController GPU slot + resource BeginFrame/upload
├── WorldRenderFrameBuilder camera/root/light/classification facts
├── WorldSceneRenderer accepted world draw graph
│ ├── RetailPViewRenderer visibility/order owner
│ ├── RetailPViewPassExecutor concrete GL pass implementation
│ └── WorldRenderDiagnostics probes/debug counters/signatures
├── PrivatePresentationRenderer portal + paperdoll + retained UI + ImGui
│ ├── PaperdollFramePresenter
│ └── DevToolsFramePresenter
└── RenderFrameDiagnosticsController title/resource/frame snapshots
```
Names may narrow as code lands, but the ownership boundaries do not collapse
into one replacement god object. No new owner may receive `GameWindow`, a broad
service locator, or an anonymous list of callbacks into the window.
This slice does **not** change:
- the viewer-cell render root or player-cell lighting root;
- synthetic outdoor-cell behavior or the null-root safety path;
- portal flood, building pre-gates, clip-plane/scissor limits, punch/seal
depth behavior, shell lift, viewcone culling, or draw distances;
- shared-alpha sort keys, scope boundaries, blend/cull behavior, or immediate
EnvCell/private alpha;
- weather, fog, point-light selection, particle visibility/range, terrain
quality, FOV, frame pacing, or audio-listener formulas;
- portal-space, paperdoll, retained gameplay UI, ImGui, or screenshot visuals;
- resource ownership, shutdown/disposal order, or three-frame GPU retirement;
- TS-53's retained-UI time placement on the render seam.
Issue #225's translucent lifestone/particle comparison remains a separate user
visual gate. Passing this structural slice must not mark it resolved.
## 2. Retail oracle and accepted adaptations
This extraction reuses the existing named-retail research rather than
reinterpreting the draw algorithms:
- `SceneTool::BeginScene @ 0x0043DAD0` establishes the black scene-clear
baseline used by portal CreatureMode replacement;
- `SmartBox::DrawNoBlit @ 0x00454C20` updates the viewer and invokes the normal
world render before post-world selection work;
- `SmartBox::RenderNormalMode @ 0x00453AA0` owns landscape/inside dispatch and
the final retail alpha flush;
- `RenderDeviceD3D::DrawInside @ 0x0059F0D0`,
`PView::DrawInside @ 0x005A5860`, and
`PView::ConstructView @ 0x005A57B0` own the portal traversal;
- `PView::DrawCells @ 0x005A4840` fixes the outside-landscape → alpha flush →
conditional depth clear → exit masks → shells → object-list order;
- `LScape::draw @ 0x00506330`, `GameSky::Draw @ 0x00506FF0`,
`DrawBuilding @ 0x0059F2A0`, `DrawBlock @ 0x005A17C0`, and
`DrawObjCellForDummies @ 0x005A0760` constrain the landscape/building/cell
phases;
- `AddMeshToAlphaList @ 0x0059C230` and
`FlushAlphaList @ 0x0059D2E0` constrain alpha submission/flush boundaries;
- `CPhysicsPart::UpdateViewerDistance @ 0x0050E030` and
`CShadowPart::insertion_sort @ 0x006B5130` constrain transformed DAT
SortCenter/CYpt and stable far-to-near ordering;
- `CreatureMode::Render @ 0x004529D0` constrains portal and paperdoll private
viewports;
- `SceneTool::EndFrame @ 0x0043FB30` proves retail gameplay UI precedes retail
profiler/debug overlays; placing ImGui above gameplay UI is acdream's IA-12
modern coexistence adaptation, not literal Keystone parity.
The readable pseudocode oracle is
[`docs/research/2026-06-05-retail-pview-indoor-render-pseudocode.md`](../research/2026-06-05-retail-pview-indoor-render-pseudocode.md),
with the current full reference in
[`docs/research/2026-06-02-retail-render-pipeline-full-reference.md`](../research/2026-06-02-retail-render-pipeline-full-reference.md).
Shared-alpha ordering is pinned separately by
[`docs/research/2026-07-18-retail-shared-alpha-list-pseudocode.md`](../research/2026-07-18-retail-shared-alpha-list-pseudocode.md).
This slice preserves, rather than retires, the registered modern adaptations:
IA-8/9/10/12/14/15; AD-16/17/19/24;
AP-21/32/33/34/35/85/92/115/116/117;
TS-6/7; and TS-53. Source pointers move with ownership in the same commit, but
mechanism/risk language changes only if behavior actually changes.
## 3. Frozen production render order
The complete accepted order is:
1. call `GpuFrameFlightController.BeginFrame` outside the render `try`;
2. begin the composite-texture frame and cache tick;
3. begin, in order, dispatcher, EnvCell, portal depth, world text, retained-UI
text, clip, terrain, lighting UBO, then profiler frame state for the current
GPU slot;
4. select the portal replacement viewport; reset particle visibility when it
is active;
5. choose portal black or normal fog clear color, force depth writes, clear
color/depth/stencil, and establish CW/back-face frame-global state;
6. run GL-state/surface diagnostics;
7. advance render-thread publication in order: `WbMeshAdapter.Tick`, destination
reveal preparation/evaluation, then `ParticleRenderer.BeginFrame`;
8. begin ImGui, tick weather, and begin retail selection-scene accumulation;
9. when a normal camera/world viewport is active:
- begin the one frame-scoped `RetailAlphaQueue` collection; its landscape
`Flush` drains the first sub-scope and `EndFrame` later drains the final
world sub-scope without a second begin;
- resolve camera/projection/frustum and begin particle/terrain visibility;
- publish world-reveal visibility/completion;
- preview audio/display settings and publish the listener;
- resolve player lighting cell and collided viewer render cell;
- build prior-visible-cell point-light candidacy, sun/fog/lighting UBO, and
animated/equipped classification;
- construct the synthetic outdoor root/building candidates when required;
- run the null-root safety world or the one `RetailPViewRenderer.DrawInside`
graph;
- draw the post-world Scene particles/weather fallback and debug geometry;
- sample current-frame DebugVM visible-landblock and nearest-collision facts
before the developer UI can consume them;
- close the final shared-alpha scope and publish terrain/cell particle
visibility;
10. complete retail selection-scene accumulation;
11. draw the portal CreatureMode replacement viewport;
12. render the private paperdoll FBO before its 2-D widget blit;
13. tick/update-cursor/draw retained gameplay UI;
14. render ImGui menus and developer panels above gameplay UI;
15. capture a requested default-framebuffer screenshot;
16. publish window-title, resource-dump, and frame-profile diagnostics;
17. close the exact GPU frame once in `finally`.
Within `RetailPViewRenderer.DrawInside`:
1. construct the main PView; merge per-building exterior floods only for the
synthetic outdoor root and build separate look-in frames for interior roots;
2. assemble/upload the one clip arena, prepare EnvCell batches, partition
statics/dynamics, and classify outside-stage dynamics;
3. draw each outside slice early: sky, terrain, and outdoor statics;
4. draw building look-ins: all far-Z punches, opaque shells, then transparent
shells plus statics/dynamics/emitters;
5. draw outside-stage dynamic meshes, Scene particles, and weather late;
6. draw one interior-root unattached Scene-particle pass;
7. flush the **landscape** shared-alpha scope;
8. for an interior root, perform the full depth clear, then write true-depth
exit seals; for an outdoor root, retain world depth and use far-Z entry
punches;
9. draw opaque EnvCell shells once, then transparent shell cells immediate
far-to-near;
10. draw cell statics as one cross-cell batch, then their particles;
11. draw the surviving main-flood dynamics last, then their particles; look-in
and outside-stage dynamics remain in their earlier landscape phases.
The landscape alpha flush must never move across the depth clear. The final
world alpha close must never move after portal/paperdoll private viewports.
## 4. Architecture and interfaces
### 4.1 Frame transaction
Use immutable values equivalent to:
```csharp
internal readonly record struct RenderFrameInput(
double DeltaSeconds,
int ViewportWidth,
int ViewportHeight);
internal readonly record struct WorldRenderFrameOutcome(
int VisibleLandblocks,
int TotalLandblocks,
bool NormalWorldDrawn);
```
Portal-viewport selection and drawing belong to `PrivatePresentationRenderer`,
which runs after the world phase. If a combined public outcome becomes useful,
the top-level orchestrator composes it only after private presentation; the
world owner never claims that a later viewport was drawn.
`RenderFrameOrchestrator` receives a small fixed set of typed phase owners. It
does not receive every renderer separately, a mutable frame-services bag, or
`GameWindow`.
`BeginFrame` remains outside the `try`: if it fails, no `EndFrame` is attempted.
After begin succeeds, every failure closes exactly once. Render failure plus
close failure becomes the current explicit `AggregateException`; close-only
failure propagates directly. Alpha, particle-visibility, and selection-scene
completion retain their current non-finally behavior; changing that recovery
contract requires separate approval and evidence.
### 4.2 Borrowed resources and shutdown
Slice 7 collaborators borrow the already-created GL/render owners. They do not
dispose them. The existing retryable shutdown transaction keeps its proven
dependency order: wait for submitted work; withdraw UI/render publications;
release textures and mesh owners; then dispose GL backing stores, with
`GpuFrameFlightController` last among render resources. Slice 8 may group
construction/disposal after the render cutover is stable.
`DisplayFramePacingController` is the focused shared owner for requested VSync,
live display preview, monitor/window callbacks, and the separate
`OnFrameRendered` pacing event. Render preparation and window lifecycle wiring
invoke that typed owner directly; neither calls back into a substantial
`GameWindow` method. Its existing disposal remains in the Slice 7 shutdown
transaction.
### 4.3 World frame data
`WorldRenderFrameBuilder` owns reusable classification scratch:
- animated/equipped entity IDs;
- synthetic outdoor node and candidate building cells;
- previous-frame drawable cells used by point-light candidacy;
- camera/root/classification facts with one-frame borrowed lifetime.
The builder owns only facts available before PView execution. Slice-local
visible/outdoor/unattached particle-owner IDs belong to
`RetailPViewPassExecutor`. The current interior partition is produced by
`RetailPViewRenderer.DrawInside` and remains in its borrowed result/diagnostic
snapshot; it is never stored as stale pre-draw builder state.
It queries canonical `LiveEntityRuntime`, `GpuWorldState`, player-mode/camera,
physics cell graph, cell visibility, and landblock presentation sources. It
does not copy their dictionaries or create a second visibility computation.
Returned collections are borrowed until the next build; tests pin this
lifetime and consumers cannot retain them.
### 4.4 PView pass execution
Replace the current `RetailPViewDrawContext` callback bag with a data-only
`RetailPViewFrameInput`, a typed cell-visibility source, and one explicit
`IRetailPViewPassExecutor`/`RetailPViewPassExecutor`. The executor owns the GL
resources needed for these named operations:
```text
SetTerrainClip
DrawLandscapeSlice
DrawLandscapeSliceLate
ClearInteriorDepth
DrawExitPortalMask
DrawLookInPortalPunch
DrawUnattachedSceneParticles
FlushLandscapeAlpha
DrawCellParticles
DrawDynamicsParticles
EmitDiagnostics
```
`RetailPViewRenderer` remains the retail ordering owner and calls these typed
operations. The executor may not call `GameWindow` or make independent
visibility/order decisions. Existing borrowed `RetailPViewFrameResult` scratch
semantics remain explicit.
### 4.5 Presentation and diagnostics
`PaperdollFramePresenter` owns dirty state, DAT-backed clone/re-dress, pose
selection/application, and FBO rendering. Inventory/session paths call
`MarkDirty`; they do not mutate its internals.
`PrivatePresentationRenderer` owns portal viewport, paperdoll, retained UI,
ImGui, and screenshot order. A focused panel-layout/devtools owner handles menu
actions and is also reused by framebuffer resize; it cannot call a substantial
window method.
`WorldRenderDiagnostics` owns pass-local PView/GL/scissor state, render
signatures, and world-pass timings. `RenderFrameDiagnosticsController` owns
FPS/frame-time accumulation, final visible-landblock counters, performance
title/resource publication, and the public render-frame snapshot. Only
render-derived DebugVM providers move in Slice 7. Update/session/streaming
providers and the cross-domain world-lifecycle resource sampler stay at the
composition boundary until Slice 8 rather than turning either diagnostics
owner into a replacement service locator. Diagnostics observe the frame; they
never choose a draw branch or mutate canonical world ownership.
## 5. Implementation checkpoints
### A — contracts, architecture, and oracles
- correct the stale render-pipeline and per-frame architecture sections;
- add `RenderFrameInput`, phase interfaces, and a recording orchestrator;
- pin normal order, portal replacement, login wait, indoor/outdoor/null-root
branches, and screenshot placement; camera is an initialized runtime
invariant, so the slice does not invent a new graceful missing-camera path;
- pin the exact GPU begin/end exception matrix;
- add source guards against `GameWindow` back-references, mega contexts, and a
new PView delegate bag;
- capture the current connected framebuffer/resource baseline.
### B — leaf presentation and diagnostics
- extract paperdoll refresh/pose/dirty/FBO ownership;
- extract panel-layout reuse and devtools menu/panel rendering;
- extract pass-local render signature, PView/GL/scissor probes, and timing state
into `WorldRenderDiagnostics`; extract FPS, final counts, title/resource
publication, and the public render snapshot into
`RenderFrameDiagnosticsController`;
- preserve the pre-presentation render-derived DebugVM sampling site separately
from the post-screenshot title/resource publication site; leave non-render
DebugVM providers and cross-domain world-lifecycle sampling at composition;
- keep call sites in their current `GameWindow.OnRender` positions.
### C — frame resources and live preparation
- extract ordered per-resource `BeginFrame` calls, clear/state establishment,
histogram probe, mesh upload, reveal evaluation, and particle begin;
- extract weather accumulation and display foundations that do not consume the
camera/root classification, preserving their current render-time positions;
- consolidate requested-VSync/live-preview, monitor callbacks, and
`OnFrameRendered` pacing through `DisplayFramePacingController`, while
leaving its disposal in the existing shutdown transaction;
- prove `WbMeshAdapter.Tick` remains before reveal evaluation and drawing;
- prove dispatcher begin occurs once for the whole world/paperdoll frame.
### D — world frame builder
- move animated/equipped classification, camera/root selection, outdoor-node
building gather, and prior-visible-cell light scratch out of the window;
- after those facts exist, run focused audio/listener, live preview, sky PES,
sun/fog/point-light, and lighting-UBO preparation; do not build an interim
`GameWindow` mega-context;
- keep PView-generated particle-owner sets in `RetailPViewPassExecutor` and the
post-draw partition in the borrowed PView result/diagnostic snapshot;
- pin viewer-cell versus player-cell roles, portal replacement, login wait,
fallback/null-root, sealed/SeenOutside, and borrowed scratch reset;
- preserve allocation-free warmed frames and hysteretic scratch retention.
### E — typed PView pass executor
- replace `Action`/`Func` draw callbacks in `RetailPViewDrawContext`;
- move landscape early/late, scissor/clip, depth seal/punch, particle passes,
landscape alpha flush, and PView diagnostics to the executor;
- add a recording executor test for the complete `DrawInside` call sequence;
- verify no executor operation rebuilds portal visibility or retains a borrowed
frame result across calls.
### F — world scene owner
- extract alpha/selection/particle-visibility begin/end and the normal-world
conditional;
- extract sky/terrain/null-root fallback and unified PView branches;
- extract post-world particles/weather and debug collision drawing;
- return a small `WorldRenderFrameOutcome` for diagnostics/presentation;
- delete the world draw body and helper closure from `GameWindow`.
### G — final orchestration cutover
- compose concrete resource, world, private-presentation, and diagnostic owners;
- replace `OnRender` with one `RenderFrameOrchestrator.Render` handoff;
- remove obsolete fields/helpers/callbacks and prove the transitive render graph
has no substantial path back to `GameWindow`;
- preserve the existing shutdown resource list without transferring disposal;
- measure line/field/method counts. Ownership decides completion; the expected
signal is roughly 5,0005,500 lines before Slice 8 composition cleanup.
### H — release and connected gates
After three clean corrected-diff reviews:
1. run focused App/Core render, PView, alpha, portal, screenshot, and resource
tests;
2. run `dotnet build AcDream.slnx -c Release`;
3. run the complete Release suite;
4. run `tools/run-connected-world-lifecycle-gate.ps1 -SkipBuild`;
5. run `tools/run-connected-r6-soak.ps1 -SkipBuild`;
6. require code-zero graceful exits, exact lifecycle checkpoints/PNGs, no
fatal/invariant/reveal/pending-resource failures, and unchanged same-location
resource tolerances;
7. compare the six existing lifecycle/reconnect PNGs (`capped_login`,
`aerlinthe_first`, `facility_hub`, `holtburg_after_dungeon`,
`aerlinthe_revisit`, and `uncapped_reconnect`) with the Slice 6 baseline;
8. audit architecture, divergence pointers, roadmap, milestones, issues,
`AGENTS.md`, `CLAUDE.md`, and durable memory.
The accepted baseline at docs-only commit `9512404e` is behavior-equivalent to
production cutover `e91f3102`: 7,182 passed / 5 skipped, 314.195-second
lifecycle/reconnect, and 393.581-second soak. Exact counters are recorded in
`memory/project_gamewindow_decomposition.md` and the Slice 6 closeout ledger.
Building/doorway poses, an open paperdoll, transient portal-exit visuals, and
#225 remain user visual gates; the stable post-materialization PNGs do not
overclaim them.
## 6. Mandatory review gate
After every implementation checkpoint, three read-only reviews run:
1. **Retail conformance:** named anchors, PView/alpha/depth/private-viewport/UI
order, and registered adaptation boundaries.
2. **Architecture/integration:** focused owner scope, no replacement god object,
no service locator/back-callback, render-thread mutation, borrowed lifetime,
GL-state symmetry, and shutdown dependency direction.
3. **Adversarial:** begin/end failures, missing resources, portal/login
branches, stale borrowed scratch, callback reentrancy, frame spikes,
mid-frame resource churn, and GUID/session replacement.
The primary agent fixes confirmed findings at their root, then requests
corrected-diff re-review until clean. Each checkpoint then runs focused tests,
Release build/full suite, divergence/doc audit, and lands as one commit.
## 7. Final acceptance matrix
### Automated
- exact orchestrator order for normal, login, portal, indoor, outdoor, and
fallback frames, plus construction-time rejection of missing required phase
owners;
- exact GPU begin/end/aggregate failure behavior;
- PView landscape/look-in/flush/clear/seal/shell/static/dynamics ordering;
- screenshot after retained UI/ImGui, before title/resource/frame-profile
diagnostics, and before GPU `EndFrame`;
- dispatcher frame begin once across world and paperdoll scopes;
- frame scratch reset/reuse after skipped, portal, normal, and failed frames;
- no `GameWindow` reference or anonymous draw delegate bag in extracted owners;
- existing PView replay, shared-alpha, particle visibility, portal, paperdoll,
GPU-fence, screenshot, resource, and structural suites remain green;
- Release build and all tests green;
- connected lifecycle/reconnect and synchronized soak reports pass.
### In-client campaign gate
- outdoor dense scenes, terrain, sky, weather, particles, and selection match;
- buildings, doorway look-in/out, depth seals/punches, and dynamics stay stable;
- sealed dungeons keep correct visibility and lighting;
- portal replacement and destination reveal preserve the accepted flow;
- paperdoll, retained gameplay UI, cursor, and ImGui layering are unchanged;
- no resource growth, GPU reset, render exception, or shutdown regression.
## 8. Fixed assumptions
- all render/GL mutation remains on the existing render thread;
- `DatCollection` remains the only DAT reader;
- `LiveEntityRuntime`, `GpuWorldState`, cell visibility, session, and resource
owners remain canonical and are not copied;
- existing render resources are borrowed through Slice 7 and disposed by the
current shutdown transaction;
- frame-local results and scratch collections are borrowed until the next
frame/call unless explicitly copied;
- no quality/range/performance tradeoff is introduced to make the extraction
easier;
- missing optional diagnostic/UI owners skip only their own presentation and
never suppress world drawing or GPU frame closure.