acdream/docs/plans/2026-07-22-gamewindow-slice-7-render-frame-orchestration.md

25 KiB
Raw Blame History

GameWindow Slice 7 — render-frame orchestration

Status: Active 2026-07-22. Parent program: docs/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:

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, with the current full reference in docs/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.

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:

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:

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 the outer resource/world/private/diagnostic order across normal, portal-replacement, login-wait, and screenshot outcome shapes; defer the concrete indoor/outdoor/null-root branch tests to the world builder and scene owner in D/F; 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.

Completed 2026-07-22. RenderFrameOrchestrator now owns the inert five-owner outer transaction contract; GpuFrameFlightController implements its narrow lifetime seam. Twenty-six focused render/GPU tests pin value-only contracts, required-owner construction, exact phase prefixes, per-phase input propagation, and every begin/render/close failure combination. Release build and the full suite pass (7,199 passed / 5 skipped after adding 17 tests). All three corrected-diff reviews are clean; production GameWindow.OnRender is unchanged.

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.