acdream/docs/plans/2026-07-21-gamewindow-slice-2-live-animation-presentation.md
Erik 9a150e2421 docs(architecture): close live animation slice
Record the shipped presentation owner, exact frame-handoff lifetime, retail short-frame behavior, current GameWindow metrics, and Slice 3 as the next structural boundary.
2026-07-21 10:03:20 +02:00

392 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# GameWindow Slice 2 — live animation presentation ownership
**Status:** Complete 2026-07-21. Automated and three-agent review gates pass;
connected visual regression gate is carried to the final campaign gate.
**Parent program:** [`docs/architecture/code-structure.md`](../architecture/code-structure.md), Slice 2.
**Baseline:** `9ad8113c`; `GameWindow.cs` is 14,912 lines, 278 fields, and
191 methods before this slice.
**Result:** `GameWindow.cs` is 14,546 lines, 277 fields, and 190 methods.
The slice also closed stale-schedule ABA across replacement, appearance
rebinding, static rebind, and borrowed sequencer buffers. Forty-one focused
Release tests and the full 6,575-pass / 5-skip Release suite are green.
**Behavior rule:** This is an ownership extraction over the accepted R6 object
frame plus one named-retail conformance correction: short AnimFrames retain
the prior/rest pose of trailing CPartArray parts. It must not otherwise change
sequence time, root motion, collision, manager order, hook semantics, part
transforms, render eligibility, or effect attachment.
## 1. Outcome
`GameWindow` stops owning final animated-part presentation. A focused
`LiveEntityAnimationPresenter` consumes the already-completed scheduler output
and publishes the exact same draw and effect state:
- final drawable `MeshRef` transforms;
- indexed rigid part poses and availability;
- `EntityEffectPoseRegistry` publication;
- live static-animation frame handoff;
- legacy animation interpolation state;
- one-time `MotionDone` binding;
- presentation-specific sequence/part diagnostics.
The shipped owners remain unchanged:
- `LiveEntityAnimationScheduler` owns ordinary-object clocks, PartArray
advance, root motion, one movement owner, control-hook capture, and the
retail manager tail;
- `RetailStaticAnimatingObjectScheduler` owns the separate retail
`static_animating_objects` time/workset and its deferred process-hooks tail;
- `EquippedChildRenderController` owns equipped-child composition;
- `AnimationHookFrameQueue` owns incarnation-scoped semantic/presentation-hook
delivery;
- `LiveEntityRuntime` remains the only identity, incarnation, component, and
spatial-lifetime owner.
`GameWindow.AdvanceLiveObjectRuntimeCore` retains only the explicit frame-order
calls between these owners.
## 2. Retail and project oracles
No new AC algorithm is introduced. The extraction is pinned to:
- [`2026-07-19-r6-update-object-order-pseudocode.md`](../research/2026-07-19-r6-update-object-order-pseudocode.md)
`UpdatePositionInternal`, `process_hooks`, and manager-tail ordering.
- [`2026-07-19-r6-complete-root-frame-pseudocode.md`](../research/2026-07-19-r6-complete-root-frame-pseudocode.md)
— complete root `Frame`, ordinary/static worksets, part/effect publication.
- [`memory/project_animation_runtime.md`](../../memory/project_animation_runtime.md)
— appearance-update and description-before-enter-world lifetime invariants.
- `CPhysicsObj::UpdatePositionInternal @ 0x00512C30`.
- `CPhysicsObj::UpdateObjectInternal @ 0x005156B0`.
- `CPhysicsObj::process_hooks @ 0x00511550`.
- `CPhysicsObj::animate_static_object @ 0x00513DF0`.
- `CPartArray::Update @ 0x00517DB0`.
- `CPartArray::UpdateParts @ 0x005190F0`.
- `CPartArray::AnimationDone @ 0x00517D30`.
- `CPhysicsObj::MotionDone @ 0x0050FDB0`.
- `Frame::combine @ 0x005122E0`.
ACE/DatReaderWriter and the installed retail DAT remain secondary fixtures for
motion-table/animation schema interpretation. Named retail is authoritative
for ordering and transform semantics.
## 3. Fixed frame-order contract
One ordinary live-object frame remains:
```text
local player pre-network object phase
selection outbound boundary
publish PhysicsScript clock
LiveEntityAnimationScheduler.Tick
RetailStaticAnimatingObjectScheduler.Tick
LiveEntityAnimationPresenter.Present
EquippedChildRenderController.Tick
RetailStaticAnimatingObjectScheduler.ProcessHooks
translucency advance
AnimationHookFrameQueue.Drain
effect-root refresh
attached-emitter refresh
live-light refresh
particle visibility
particle simulation
PhysicsScript simulation
```
The presenter advances no clock and drains no hook. It only consumes poses
produced earlier in the same frame. Static `process_hooks` remains after live
parts and equipped children, as retail's `animate_static_object` requires.
## 4. Ownership and interfaces
### 4.1 Animation component state
Move `GameWindow.AnimatedEntity` and `GameWindow.AnimatedPartTemplate` to
top-level App rendering types:
```text
LiveEntityAnimationState
LiveAnimationPartTemplate
```
`LiveEntityAnimationState` remains the exact component stored in
`LiveEntityRecord.AnimationRuntime`. It owns no GUID map and no lifetime. It
retains:
- the exact `WorldEntity`, Setup, Animation, and optional sequencer;
- legacy low/high/current frame and framerate;
- ObjScale, part template, and part availability;
- prepared local-player pose and complete root-frame scratch;
- reusable `MeshRefs`, effect-part-pose, and diagnostics scratch.
Appearance updates mutate this same component and preserve sequencer/playback
state. Logical replacement creates a new component.
### 4.2 Presentation context
Define a narrow App interface equivalent to:
```csharp
interface ILiveAnimationPresentationContext
{
uint LocalPlayerGuid { get; }
MotionInterpreter? ResolveMotionInterpreter(LiveEntityRecord record);
}
```
`GameWindow` implements only these constant-time lookups. The presenter cannot
reach windowing, GL, networking, input, streaming, physics, or arbitrary
`GameWindow` methods.
### 4.3 Presenter
Use an API equivalent to:
```csharp
void Present(
IReadOnlyDictionary<uint, LiveEntityAnimationSchedule> schedules);
void PrepareAnimation(
LiveEntityRecord record,
LiveEntityAnimationState state);
```
The presenter receives:
- canonical `LiveEntityRuntime` provider for exact-record validation;
- `EntityEffectPoseRegistry`;
- `ILiveAnimationPresentationContext`;
- a late-bound `ILiveStaticPartFrameSource` (the static scheduler is created
after the presenter);
- startup-resolved hidden-part diagnostic index;
- typed animation diagnostic flags.
The presenter owns its own reusable `LiveEntityRecord` snapshot. It does not
borrow `LiveEntityAnimationRuntimeView`'s reusable enumerator snapshot across
the synchronous `EffectPoseChanged` callback boundary. Nested enumeration by a
subscriber therefore cannot invalidate the outer presentation pass.
Each schedule carries the exact `LiveEntityRecord`, `WorldEntity`, animation
component, `ObjectClockEpoch`, and projection-mutation version which produced
it. The presenter validates all of those identities before consuming ordinary
or static frames and again before each externally observable publication. A
dictionary key/local ID is an index only; it is never proof of incarnation.
The static prepared-frame source is queried late, only after this validation.
Its take operation receives the exact record, entity, animation component,
object-clock epoch, and projection/residency versions; it validates those
tokens before clearing and returning prepared frames.
It keeps no GUID map, no animation clock, no packet/session reference, and no
render-backend object.
### 4.4 Diagnostics
Move the surviving `SEQSTATE`, `CURRNODE`, `PARTSDIAG`, and `MOTIONDONE` gates
behind one typed animation diagnostic owner. Environment variables are read
once; the presenter does not perform per-frame environment lookups. Throttle
state follows the exact animation component lifetime instead of living on the
remote-motion component.
## 5. Transform invariants
For every part index, preserve these two deliberately different transforms.
### Drawable visual transform
```text
Setup DefaultScale
× animated part orientation
× animated part translation
× entity ObjScale
```
This is the existing renderer/hydration contract. It scales geometry and the
part's translated placement.
### Rigid effect/attachment transform
```text
animated part orientation
× translation(animated origin × entity ObjScale)
```
Retail `CPartArray::UpdateParts` scales the part-frame origin only. Setup
DefaultScale is `gfxobj_scale`, not rigid part-frame state, so particles,
lights, attachments, and multipart collision must never inherit it.
Additional invariants:
- Sequencer frames win when present. Retail updates only
`min(CPartArray.num_parts, AnimFrame.num_parts)`; missing trailing frames
retain both their previous drawable visual transform and their previous
indexed rigid/effect pose (the two distinct initial rest transforms on first
presentation) instead of snapping either channel to identity.
- Legacy animation interpolates origin linearly and orientation spherically,
clamps interpolation to `[0,1]`, and wraps over the inclusive frame span.
- Live static prepared frames override the ordinary schedule for that owner.
- `ComposeParts == false` changes no mesh or indexed pose.
- Non-drawable/debug-hidden parts remain in indexed effect poses but not
drawable `MeshRef`s.
- `PartAvailability` is published unchanged and controls effect part lookup.
- Mesh identity, surface overrides, and list reuse remain unchanged.
- Entity mesh/indexed-part state is installed before effect-pose publication.
## 6. MotionDone invariants
- Bind before any sequencer can complete; repeated calls are idempotent.
- The scheduler validates the exact owner before invoking `PrepareAnimation`.
- Resolve the player/remote `MotionInterpreter` at callback time because that
consumer may be created after the animation component.
- Never capture a legitimate null interpreter permanently.
- Never complete a motion from pose publication; `AnimationHookFrameQueue`
remains the semantic `AnimationDone` boundary.
- Logical removal/replacement stops the old component from advancing; no
presenter-owned callback queue survives it.
- The callback revalidates the stable captured record, entity, and animation
component before resolving a same-GUID interpreter. It deliberately does
not capture object-clock/projection epochs: the same retail CPartArray and
completion consumer survive legitimate rebucketing and leave/re-enter.
GUID reuse still cannot deliver an old completion to a replacement.
- `MOTIONDONE` diagnostics observe but never alter queue state.
## 7. Execution sequence
### Commit 0 — plan and baseline
- Land this plan.
- Record exact baseline metrics and current frame order.
- Register no new divergence; this slice moves an accepted mechanism.
### Commit 1 — extract component types and pure presenter
- Move animation component/template types out of `GameWindow`.
- Add the presentation context and typed diagnostics owner.
- Implement `LiveEntityAnimationPresenter` by moving the current composition
body line-for-line, retaining the baseline trailing-part fallback initially.
- Add focused tests before runtime cutover.
### Commit 2 — cut over runtime ownership
- Construct the presenter beside the scheduler.
- Route scheduler preparation, local-player pre-advance, spawn-time binding,
and final presentation through it.
- Delete `TickAnimations`, `EnsureMotionDoneBinding`, nested animation types,
and presentation-only remote diagnostic fields from `GameWindow`.
- Keep child/static-hook/effect ordering visible in
`AdvanceLiveObjectRuntimeCore`.
### Commit 3 — retail trailing-part conformance
- Record the `CPartArray::UpdateParts` minimum-count/retain behavior in the
R6 pseudocode note.
- Preserve both visual and rigid prior/rest trailing transforms.
- Add first-presentation and later-shorter-frame conformance tests plus
ordinary/live-static/DAT-static parity coverage.
- Update the divergence register in this commit if the audited register
contains a corresponding row; introduce no silent behavior delta.
### Commit 4 — review, gate, and ledger close
- Run retail-conformance, architecture/integration, and adversarial read-only
reviews against the complete slice.
- Fix every confirmed finding and repeat review until clean.
- Run focused animation, scheduler, static, effect, projectile, remote-motion,
and local-player tests.
- Run Release solution build and full Release suite.
- Record new `GameWindow` line/field/method counts.
- Update code structure, roadmap, milestones, issues if any, durable animation
memory, `AGENTS.md`, and `CLAUDE.md`.
## 8. Automated acceptance matrix
### Composition
- Sequencer pose composes every part and preserves surface overrides.
- Missing sequencer trailing parts retain their previous/rest indexed pose.
- Legacy current/next frame interpolation and inclusive wrapping are exact.
- Legacy hidden/single-frame owners compose only when scheduled.
- Static prepared frames override ordinary frames exactly once.
- `ComposeParts == false` preserves prior mesh/pose state.
### Visual versus rigid transforms
- Setup nonuniform DefaultScale affects drawable geometry only.
- ObjScale affects drawable geometry and translated placement.
- Rigid effect poses scale only the translated origin.
- Nondrawable and debug-hidden parts remain available to effects.
- Unavailable parts remain indexed but fail effect-part lookup.
### Lifetime and ordering
- A removed/replaced animation component is skipped by the runtime-view
snapshot and cannot publish into its replacement.
- A schedule prepared for incarnation A is rejected if A is withdrawn,
replaced, or reprojected before presentation, including same-local-ID ABA.
- Reentrant deletion/replacement during pose publication prevents every later
write for the stale owner; no borrowed scratch buffer crosses a callback.
- Effect-pose publication sees the entity's already-installed indexed parts.
- Same-frame animation hooks drain only after current parts and children.
- Static process-hooks runs only after static parts and children are current.
- Presentation allocates no per-frame mesh/effect lists in steady state.
### Motion completion
- Binding is idempotent.
- A consumer created after binding receives completion.
- Player and remote resolution select the correct interpreter.
- Missing consumer is a no-op.
- Diagnostics do not duplicate `MotionDone`.
- A legitimate rebucket/object-clock rebase preserves the binding.
- Delete plus GUID/local-ID reuse rejects completion from the old component.
### Cross-owner order and reconciliation
The presenter-level portions below are covered by Slice 2. The full
instrumented owner-order trace and non-advancing spatial-reconcile assertion
remain assigned to Slice 6, where the update sequence becomes one testable
orchestrator instead of a `GameWindow` method. Slice 2 preserves that order but
does not claim an end-to-end orchestrator test prematurely.
- One instrumented object-frame test pins presenter, children, static hooks,
fades, ordinary hook drain, effect roots, emitters, lights, particles, and
scripts. A PES-created particle first simulates on the following frame.
- Authoritative spatial reconciliation performs only root/effect refresh,
child composition, emitter refresh, and light refresh; it advances no
animation, hook, fade, particle, script, or object clock.
- Appearance shrink from three parts to one preserves playback while removing
stale MeshRefs, indexed poses, availability, and effect-registry entries.
- Ordinary, live-static handoff, and DAT-static composition share conformance
fixtures for scale, sparse availability, and surface overrides.
- An equipped child consumes the just-published parent indexed pose in the
same frame, including ObjScale and nested attachment composition.
- An animated projectile's trail/light anchor observes the same-quantum root
and part pose, never the previous frame.
`EquippedChildRenderController` callback-mutation hardening is an adjacent
Slice-6 frame-orchestration concern. Slice 2 does not claim it: the known
reentrancy case is recorded as a follow-up unless a focused test demonstrates
the existing traversal is already safe.
- A nested animation-runtime enumeration triggered by `EffectPoseChanged`
cannot overwrite or truncate the presenter's private outer snapshot.
## 9. Connected gate
Run one normal capped Release client and verify behavior remains unchanged:
1. local idle, walk, run, strafe, turn, jump, combat, and stop transitions;
2. observe a remote player walk/run/turn without pose stalls or blips;
3. open/close a door and kill a creature; static/reactive final poses persist;
4. equip/unequip armor and switch melee/bow/wand without animation loss;
5. fire an arrow and cast a projectile spell; trails remain attached to the
current missile/hand pose;
6. cast a protection and recall/portal; body effects follow current parts;
7. portal/relog and confirm no old pose or completion reaches a replacement.
Any failure returns to the owning commit. No fallback pose cache, delayed
retry, grace period, or GUID-only suppression is acceptable.
## 10. Subagent policy
One primary agent performs all edits. Read-only agents independently audit
retail conformance, architecture/lifetime, and adversarial tests before the
plan and after every implementation correction. No subagent edits the shared
slice concurrently.