acdream/docs/plans/2026-07-21-gamewindow-slice-6-update-frame-orchestration.md

442 lines
23 KiB
Markdown

# GameWindow Slice 6 — update-frame orchestration
**Status:** Active 2026-07-21.
**Parent program:** [`docs/architecture/code-structure.md`](../architecture/code-structure.md), Slice 6.
**Baseline:** `d4ecac1d`; `GameWindow.cs` is 8,811 raw lines, 247 fields, and
153 methods. Release baseline: 7,054 passed / 5 skipped. The connected
lifecycle gate and synchronized nine-stop resource soak both pass.
**Behavior rule:** This is an ownership extraction. Preserve the accepted
retail object/network order, asynchronous streaming-readiness adaptation,
portal transit, input semantics, and camera presentation. A defect exposed by
the boundary receives its own deterministic oracle and bisectable fix; it is
never hidden behind a retry, delay, suppression flag, or reordered callback.
## Progress ledger
- [ ] A — freeze the complete production phase graph and introduce the typed
orchestration contract plus structural/order guards.
- [ ] B — extract the pre-network live-object presentation phase and the
non-advancing post-network spatial reconciler; remove callbacks into
`GameWindow` from `RetailLiveFrameCoordinator`.
- [ ] C — extract streaming-origin convergence, observer selection, dungeon
collapse, streaming tick, and rescued-entity rebucketing.
- [ ] D — extract dispatcher/raw-mouse/combat input sampling without changing
movement-command or UI-capture semantics.
- [ ] E — extract the complete local-player teleport owner and wire it between
the existing post-network liveness and player-mode auto-entry phases.
- [ ] F — extract the shared player-mode lifecycle plus fly/chase/player
camera presentation and cut production over to one
`UpdateFrameOrchestrator`.
- [ ] G — delete the old `OnUpdate` bodies, callback facades, and obsolete
frame state; run focused corrected-diff reviews after each ownership edge.
- [ ] H — full Release suite, connected lifecycle/reconnect gate, synchronized
resource soak, documentation, durable memory, and line/field/method closeout.
Every checked checkpoint is committed as one bisectable architectural unit.
Documentation records the exact commit and accepted test count as work lands.
## 1. Outcome and non-goals
At slice exit, `GameWindow.OnUpdate` starts the profiler scope and delegates one
host delta to `UpdateFrameOrchestrator`. `GameWindow` composes concrete owners;
it no longer selects a streaming observer, advances animations/effects,
samples raw mouse input, sequences inbound network traffic, advances portal
transit, or updates gameplay cameras.
The owner graph is explicit rather than a list of anonymous callbacks:
```text
UpdateFrameOrchestrator
├── live teardown convergence retry retained tombstones
├── UpdateFrameClock normalized host time / PES clock
├── StreamingFrameController residence/readiness pre-pass
├── GameplayInputFrameController semantic input + mouse/combat intent
├── RetailLiveFrameCoordinator retail SmartBox object barrier
│ ├── LiveObjectFrameController local/ordinary/static/effect tails
│ ├── LiveSessionController inbound network drain
│ ├── RetailLocalPlayerFrame CommandInterpreter position tail
│ └── LiveSpatialReconciler non-advancing authoritative refresh
├── LiveEntityLivenessController expiry after accepted inbound state
├── LocalPlayerTeleportController local transit/reveal/tunnel state
├── PlayerModeAutoEntry one-shot ready-world entry
├── PlayerModeController enter/exit/current-player lifetime
└── CameraFrameController fly/chase/player presentation
```
Concrete names may be narrowed during checkpoint A's source audit, but the
ownership boundaries and order do not change. The orchestrator receives a
small immutable per-frame input and concrete phase owners. It does not receive
a hundred delegates, one mega `IUpdateFrameServices` context, or a service
locator, and no transitive update collaborator calls a substantial method back
on `GameWindow`.
This slice does not change simulation rate, object quantum rules, movement
speed, interpolation, streaming radii, reveal readiness, portal visuals,
particle range, combat cadence, key bindings, camera formulas, render order,
or shutdown order. It adds no GL work and does not move retained-UI timers out
of their render seam. Slice 7 owns the draw graph; Slice 8 owns final
composition and Silk callback cleanup.
## 2. Retail oracle and accepted adaptation
The object/network barrier is pinned by
[`2026-07-19-r6-update-object-order-pseudocode.md`](../research/2026-07-19-r6-update-object-order-pseudocode.md):
- `CPhysicsObj::update_object @ 0x00515D10` applies the strict object quantum;
- `CPhysicsObj::UpdatePositionInternal @ 0x00512C30` advances the PartArray,
position/physics candidate, then processes semantic hooks;
- `CPhysicsObj::UpdateObjectInternal @ 0x005156B0` runs Detection → Target →
Movement → `CPartArray::HandleMovement` → Position → Particle → Script;
- `SmartBox::UseTime @ 0x00455410` advances object/physics time before the
inbound queue and then calls `CommandInterpreter::UseTime`;
- `gmSmartBoxUI::UseTime @ 0x004D6E30` owns UI/viewport time outside that
object barrier.
At the retail host level, `Client::UseTime @ 0x00411C40` calls
`UIElementManager::UseTime` before `SmartBox::UseTime`; the UI time broadcast
reaches `gmSmartBoxUI::UseTime`. Retail also publishes player-camera work from
the physics/player callback rather than from one post-network camera tail.
acdream currently advances teleport/UI-camera presentation after its
object/network/command barrier. Slice 6 preserves that existing order as
registered TS-53; it does not cite `gmSmartBoxUI` as proof that the current
post-network placement is retail-exact.
acdream preserves two documented presentation adaptations:
1. only semantic `AnimationDone` executes at retail's in-object hook slot;
every other captured animation hook retains the current deferred path after
final root, part, and equipped-child poses are published, including
particles, audio, lights, translucency, `CallPES`, and default-script
chaining (TS-50);
2. shared particle simulation and PhysicsScript queues tick once per host
update after both worksets rather than once per object's admitted quantum.
This also means the current shared Particle → Script order does not reproduce
the static workset's retail Script → Particle → hooks order (TS-51).
Asynchronous landblock streaming has no one-call retail equivalent. Its
convergence pre-pass remains before inbound CreateObject dispatch so a newly
accepted projection can find a resident bucket in the same frame. This
pre-pass does not advance an object's retail clock. `WorldRevealCoordinator`
remains the single login/portal readiness owner.
## 3. Fixed production phase order
The complete host-update graph is frozen as follows:
1. retry canonical live-entity teardown tombstones; an `AggregateException` is
logged and later phases continue, while any other exception propagates;
2. normalize the host delta, advance the shared PES clock once, and publish it;
3. advance pending origin retirement/recenter, select the observer, tick
streaming, and rebucket rescued persistent entities;
4. tick `InputDispatcher`, consume one raw mouse sample, and advance combat
attack intent;
5. enter the non-reentrant inbound-event envelope and advance the pre-network
live-object phase:
- local object and pre-network movement/jump output;
- queued selection/use output after movement edges;
- ordinary live objects;
- the distinct static-animation workset;
- equipped children and static hook completion;
- fades, every deferred non-`AnimationDone` hook, and current effect/light
anchors;
- particle visibility and particle simulation;
- PhysicsScript queues;
6. drain inbound session traffic inside one `GpuWorldState.MutationBatch`;
7. run the local player's post-network `CommandInterpreter` position check;
8. perform the ordinary post-network non-advancing root/child/emitter/light
reconcile;
9. advance liveness expiry from the absolute client timer;
10. activate/tick local teleport transit. A Place edge commits the player and
live root, immediately performs its own non-advancing reconcile, then
notifies world reveal and continues tunnel state;
11. evaluate one-shot player-mode auto-entry;
12. update fly or chase/player camera presentation. If the local player was
first created by this frame's inbound pass, publish its non-advancing
projection, immediately reconcile a second time, then update the camera.
Those three reconcile edges—ordinary post-network, teleport Place, and
inbound-created-player projection—are separate accepted production edges. Any
future consolidation is a behavior change with its own oracle, not part of
this extraction.
An invalid/non-positive host delta becomes zero for the PES/object/particle/
tunnel delta but never blocks inbound traffic, absolute-time liveness,
recenter convergence, or camera-state publication. Raw mouse idle uses its
independent monotonic input clock. A missing gameplay camera/input owner never
suppresses outer phases; the streaming controller may skip observer-driven
streaming when it has no valid live or offline observer, while origin
retirement still advances. Devtools keyboard capture pauses player/camera
control including autorun; retained chat capture suppresses held WASD while
autorun continues; mouse capture ends mouse-look but does not suppress
keyboard, combat, network, streaming, teleport, or teardown.
## 4. Architecture and interfaces
### 4.1 Per-frame input
Use an immutable value equivalent to:
```csharp
public readonly record struct UpdateFrameInput(double HostDeltaSeconds);
```
Derived facts are read by their owning controller from stable runtime owners
or from a narrowly typed state source. Do not pass `GameWindow`, expose its
private fields wholesale, or create a service locator. Mutable player/session
owners that legitimately change during login are represented by one focused
state/source interface per domain, not anonymous closures into window methods.
`UpdateFrameClock` is constructed before any materializer/network owner that
needs the current PES time. It owns the normalized host/PES clock and exposes a
narrow `CurrentScriptTime` source; `_physicsScriptGameTime` does not remain a
`GameWindow` field captured by later owners. Absolute liveness time and raw
mouse idle time remain distinct monotonic sources and are not derived from the
normalized simulation delta.
### 4.2 Object phase
`LiveObjectFrameController` owns the body currently in
`AdvanceLiveObjectRuntimeCore`. It composes existing focused owners and keeps
their retail order; it does not absorb their algorithms or identity maps.
`LiveSpatialPresentationReconciler` owns the current no-time refresh body.
`RetailLiveFrameCoordinator` depends on those typed owners plus the existing
session and local-player owners. Its public `Tick` remains the one explicit
object → network → command → reconcile barrier.
`RetailInboundEventDispatcher` continues wrapping the complete object phase.
Reentrant packets queue until the current object's manager tail completes.
The hot path remains allocation-free after warmup.
The extraction inventory includes the complete transitive update graph, not
only `RetailLiveFrameCoordinator`: `RetailLocalPlayerFrameController` loses
substantial window captures in favor of a focused local-player slot, movement
input source, outbound owner, and projection owner;
`LiveEntityAnimationPresenter` receives a concrete animation-presentation
context instead of retaining `GameWindow` as
`ILiveAnimationPresentationContext`; scheduler clock/player/projectile sources
are similarly narrow. Existing small value/policy callbacks are acceptable
only when they do not call a substantial body back on the window or create a
second identity/state owner.
### 4.3 Streaming phase
`StreamingFrameController` owns observer selection and the sealed-dungeon
gate. It receives the existing `StreamingOriginRecenterCoordinator`,
`StreamingController`, `LiveWorldOriginState`/center state, camera/player
state, physics current cell, `GpuWorldState`, and `LiveEntityRuntime` through
typed seams. It never builds, publishes, or retires a landblock; those remain
Slice 5 owners.
The controller preserves:
- no pre-login hardcoded-Holtburg streaming;
- real-player-origin readiness before first streaming;
- destination-origin pinning while in portal space;
- authoritative player landblock during the login hold;
- offline fly-camera observation only outside live in-world mode;
- sealed EnvCell collapse using the cell's own landblock identity;
- teleport-hold suppression of the stale source-cell dungeon gate;
- rescued-entity rebucketing after the accepted streaming tick.
### 4.4 Input and camera phases
`GameplayInputFrameController` owns per-frame dispatcher hold delivery,
UI-capture transitions, raw mouse sample/filter/idle handling, combat attack
ticking, and the complete mouse-look/cursor-capture lifetime. It is the single
owner of mouse-look state, saved cursor mode, begin/end/hide/restore, and
movement teardown across press/release, capture change, focus loss, teleport,
session reset, player-mode exit, and shutdown. Action press/release callbacks
remain on `InputDispatcher` and movement stays owned by
`PlayerMovementController`.
`PlayerModeController` owns enter/exit/toggle construction and the current
player-controller, physics-host, chase-camera, and chase-mode slot. Manual
input, `PlayerModeAutoEntry`, and local teleport activation all enter through
that one owner. Streaming, camera, local-frame, and teleport owners receive
narrow read sources from it; no path calls the substantial current
`EnterPlayerModeNow`/exit bodies back on `GameWindow` or duplicates player-mode
state.
`CameraFrameController` owns held fly-camera controls, retail chase offsets,
post-network player presentation sampling, and both chase-camera updates. It
does not own physics, target selection, or camera formulas. The local player
created by the inbound pass gets a non-advancing projection before its first
draw exactly as today, followed by the current second spatial reconcile before
the camera samples that projection.
### 4.5 Teleport phase
`LocalPlayerTeleportController` owns only the current local-player transit
state machine and presentation edges. It composes
`TeleportTransitCoordinator`,
`TeleportViewPlaneController`, `PortalTunnelPresentation`,
`WorldRevealCoordinator`, streaming priority, and the session LoginComplete
send. Checkpoint E first extracts the currently window-owned local aim,
destination, hold/forced, activation, placement, reset, and session-clear
state into this owner plus a concrete local placement collaborator where
needed. It does not reuse `RemoteTeleportPlacement`, and network/session sinks
do not retain `AimTeleportDestination` or reset callbacks into `GameWindow`.
`LiveEntityLivenessController`, tombstone retry, and `PlayerModeAutoEntry`
remain separate focused phases; teleport does not absorb them.
### 4.6 Composition, thread, and lifetime
The clock and focused dynamic sources are constructed before their first
consumer. The final orchestrator is composed exactly once at the end of a
successful `OnLoad`; `OnUpdate` fails fast if that invariant is broken rather
than running a nullable partial graph. Every phase mutates state only on the
existing update/render thread. Streaming worker completion remains behind
`StreamingController.Tick`; the extraction adds no lock, task, or async entry.
Phase controllers borrow their input, session, streaming, portal, camera, and
render-presentation owners; they do not dispose shared dependencies. Any new
event subscription has explicit symmetric detach. During shutdown the update
collaborators remain alive through the first-stage `LiveSessionController`
reset, then retire before their underlying input/camera/portal/streaming
resources. Existing retryable shutdown ordering remains authoritative.
## 5. Checkpoints and acceptance
### A — contract and guards
- record the exact production phase graph and every mutable dependency;
- correct and pin the retail oracle for physics-hook retention/reentrant
append, inactive object behavior, and static Script → Particle → hooks; pin
the existing TS-50/TS-51 divergences without reordering runtime behavior in
this ownership-only slice;
- add an explicit orchestrator order test covering all twelve phases and the
three conditional reconcile edges;
- pin normalized simulation/PES/particle/tunnel time separately from absolute
liveness and raw-mouse idle clocks for NaN, both infinities, non-positive
values, and finite values above `float.MaxValue`;
- pin missing live/offline camera behavior and the separate devtools keyboard,
retained-chat keyboard, and mouse-capture contracts;
- pin teardown retry success, `AggregateException` log-and-continue, other
exception propagation, and next-frame retry;
- add source guards that prevent `OnUpdate` or the coordinator from regaining
substantial callback bodies, and prevent any transitive update owner from
retaining `GameWindow` or one mega service context;
- verify current production traces before cutover.
### B — object/network barrier
- move the pre-network animation/effect body into
`LiveObjectFrameController`;
- move no-time root/child/effect refresh into
`LiveSpatialPresentationReconciler`;
- replace coordinator callbacks with typed collaborators;
- preserve semantic/visual hook separation, owner-incarnation revalidation,
static-after-ordinary order, ordinary Particle-before-Script, the current
shared-tail TS-51 adaptation, and once-per-host tails;
- delete `AdvanceLiveObjectRuntimeCore` and
`ReconcileLiveObjectSpatialPresentation` from `GameWindow`.
Focused tests cover `RetailObjectActivityGate` and
`RetailObjectQuantumClock`: parented/cell-less/Frozen suspension, inactive
Particle/Script-only time, Hidden+Active adjustment/hooks/manager time, and
separate ordinary epsilon/Min/Max/Huge plus static epsilon/Huge boundaries.
They also cover incomplete physics-hook
retention, same-drain animation-hook append, deferred `CallPES`/default-script/
translucency routing, physics-hook append deferred until the next drain,
completed physics-hook removal preserving the snapshotted successor, static
hooks, callback GUID reuse, and reentrant inbound packets. Particle then Script
must run exactly once for an empty world,
multiple ordinary/static owners, invalid delta, and reentrant inbound work;
the one published PES clock remains visible from hook capture through the
script tick.
### C — streaming frame
- extract readiness/recenter advancement, observer selection, dungeon gate,
streaming tick, and rescued projection rebucketing;
- test login-before-player-spawn, outdoor/dungeon/portal-hold/offline observer
cases, missing live/offline camera sources, and cell-landblock identity;
- prove same-frame streaming completion precedes CreateObject projection into
the resident bucket, unknown pre-login origin enqueues nothing, and recenter
retirement continues while ordinary streaming is gated;
- verify no build/publication/retirement ownership leaks back from Slice 5.
### D — input frame
- extract dispatcher tick, UI mouse-capture edge, raw sample/filter/idle path,
combat intent tick, and the one cross-lifetime mouse-look/cursor owner;
- test capture entry while mouse-look is active, zero-idle drift stop,
production action ordering, devtools-vs-chat autorun semantics, mouse-only
capture, and focus/teleport teardown;
- prove every non-input phase still runs under each capture mode;
- retain diagnostic semantic input as the unattended connected gate seam.
### E — local teleport and post-network world frame
- extract the currently window-owned local-player teleport state and every
aim/destination/activation/place/reset/session port before moving its tick;
- retain liveness and auto-entry as separate orchestrator calls around pending
transit activation, readiness/hold, tunnel events, materialization, and
reveal completion;
- test loaded and pending destinations, forced-readiness boundary, same-cell
respawn, LoginComplete ordering, delete/reset, and no second reveal owner;
- pin Place → immediate reconcile → reveal and old-GUID liveness candidate →
inbound delete/recreate generation safety;
- preserve the completed portal visuals and world-reveal barrier unchanged.
### F/G — camera, cutover, and deletion
- extract player-mode enter/exit/toggle and current-owner state before camera
or teleport cutover; retain `PlayerModeAutoEntry` only as its one-shot
readiness guard;
- extract fly camera, retail chase adjustment, local post-network projection,
combat target tracking, and chase-camera publication;
- pin inbound-created projection → immediate second reconcile → camera, with
exact root/child/emitter/light refresh counts and no duplicate time advance;
- compose `UpdateFrameOrchestrator` only after all dependencies exist;
- replace `OnUpdate` with profiler scope plus one `Tick` call;
- remove obsolete frame fields/facades and prove there is no back-reference to
`GameWindow` from an extracted owner;
- measure line/field/method count. The target is below 8,000 lines, but the
ownership/ordering exit criteria outrank the count.
### H — release and connected gates
After all three independent corrected-diff reviews are clean:
1. run every focused App/Core/Net/UI test project;
2. run `dotnet build AcDream.slnx -c Release`;
3. run the full Release suite;
4. run the connected lifecycle/reconnect gate;
5. run the synchronized nine-stop resource soak with movement, turn, jump,
combat, portal materialization waits, fatal-log scan, and graceful close;
6. compare lifecycle, outbound movement, materialization, entity/animation,
frame-profile, resource, and teardown traces with the Slice 5 baseline;
7. audit the divergence register, architecture, milestones, roadmap, issues,
`AGENTS.md`, `CLAUDE.md`, and durable memory.
The slice requires no new visual behavior. If connected screenshots or traces
change, return to the responsible checkpoint; do not defer the difference to
Slice 7.
## 6. Review gate for every checkpoint
After each implementation checkpoint:
1. run focused tests and inspect the complete diff;
2. retail-conformance review checks named functions, exact order, Hidden/
quantum semantics, and adaptation boundaries;
3. architecture review checks dependency direction, absence of a replacement
god object/service locator, same-thread mutation, and symmetric teardown;
4. adversarial review checks independent clocks, callback reentrancy,
owner/GUID reuse, login/portal transitions, capture modes, missing observer
sources, teardown failures, and early-return phase suppression;
5. fix every confirmed root cause and repeat review until clean;
6. build/test Release and commit the checkpoint before starting the next one.
## 7. Divergence and documentation bookkeeping
TS-50, TS-51, and the newly documented existing host-order TS-53 remain until
their exact adaptations are ported; this slice must not overclaim retail
parity. IA-14/AD-22 and the lifetime/resource rows are audited against the
final owner graph but changed only if their mechanism actually changes. Any
newly discovered unavoidable adaptation gets a register row in the same
commit. The Slice 6 closeout updates the code-structure table,
roadmap, milestones, session docs, and
`project_gamewindow_decomposition.md` with exact commits, test counts, gates,
and final metrics.