docs(architecture): plan GameWindow update-frame extraction

This commit is contained in:
Erik 2026-07-22 00:06:55 +02:00
parent d4ecac1da3
commit a36a7015c4
6 changed files with 594 additions and 37 deletions

View file

@ -8,18 +8,23 @@ Retail oracle: Asheron's Call v11.4186 (September 2013 EoR)
This note pins the control-flow order needed by R6. It deliberately separates
two kinds of animation-hook work:
- **control hooks** such as `AnimDoneHook`, whose result changes the motion
- the immediate **`AnimDoneHook` control path**, whose result changes the motion
queues observed later in the same object update; and
- **pose-dependent presentation hooks** such as particles and sounds, whose
App-layer delivery may wait until the final root/part pose has been
published without changing motion state.
- **all other captured animation hooks**, which acdream currently defers until
the final root/part pose has been published. This includes presentation
hooks such as particles and sounds, but also semantically relevant
`CallPES`, default-script chaining, and translucency hooks.
That split is an acdream presentation boundary. It does not change retail's
logical hook order.
That split is an acdream presentation boundary. Authored FIFO order is
preserved within acdream's deferred non-`AnimationDone` stream, but moving that
stream past retail's manager tail changes manager-relative timing; that is the
registered TS-50 divergence.
## Named-retail sources
- `CPhysicsObj::process_hooks` `0x00511550`
- `CPhysics::UseTime` `0x00509950`
- `CPhysicsObj::animate_static_object` `0x00513DF0`
- `CPhysicsObj::UpdatePositionInternal` `0x00512C30`
- `CPhysicsObj::UpdateObjectInternal` `0x005156B0`
- `CPhysicsObj::update_object` `0x00515D10`
@ -58,6 +63,17 @@ CPhysicsObj::update_object
-> PositionManager::UseTime
-> ParticleManager::UpdateParticles
-> ScriptManager::UpdateScripts
CPhysics::UseTime
-> ordinary object table in the order above
-> static_animating_objects
-> CPhysicsObj::animate_static_object
-> CPartArray::Update
-> UpdatePartsInternal
-> UpdateChildrenInternal
-> ScriptManager::UpdateScripts
-> ParticleManager::UpdateParticles
-> CPhysicsObj::process_hooks
```
## Pseudocode
@ -66,11 +82,16 @@ CPhysicsObj::update_object
```text
if parent != null or cell == null or state contains Frozen:
clear Active transient state
return
refresh active/view-distance state
elapsed = current_physics_time - last_update_time
if elapsed <= FrameEpsilon (0.000199999995 seconds):
last_update_time = current_physics_time
return
if elapsed > HugeQuantum:
last_update_time = current_physics_time
return
@ -85,8 +106,19 @@ if elapsed > MinQuantum:
UpdateObjectInternal(elapsed)
```
The comparisons are strict (`>`), not `>=`. A sub-minimum remainder stays in
the clock for a later call.
The comparisons are strict (`>`), not `>=`. Elapsed time at or below the
roughly 0.2-millisecond frame epsilon is explicitly rebased/discarded. Above
that epsilon, a remainder at or below `MinQuantum` stays in the clock for a
later call; `MaxQuantum` controls subdivision and `HugeQuantum` controls stale
discard. These are separate gates.
`UpdateObjectInternal` then branches on the transient Active bit. An inactive
object skips position integration, hook processing, Detection, Target,
Movement, PartArray, and PositionManager, but still advances its
ParticleManager and ScriptManager tails. A Hidden object that remains Active
is different: it enters `UpdatePositionInternal`, suppresses only PartArray
advance and physics integration, and retains PositionManager adjustment,
hook processing, and the complete manager tail.
### `CPhysicsObj::UpdatePositionInternal(dt, candidate)`
@ -120,13 +152,25 @@ manager tail.
### `CPhysicsObj::process_hooks`
```text
execute each queued physics hook in FIFO order
clear physics-hook queue
for each physics hook present in the linked list walk:
completed = hook.Execute(owner)
if completed:
unlink and destroy that hook
else:
retain it for a later object update
execute each queued animation hook in FIFO order
clear animation-hook queue
for i from zero while i < current animation-hook count:
animation_hooks[i].Execute(owner)
// the bound is live: a hook appended during execution can run in this drain
shrink animation-hook storage and set the count to zero
```
The physics-hook walk snapshots each current node's `next` pointer before
execution. A completed hook is removed; an incomplete hook remains linked.
The animation-hook loop deliberately rereads `m_num` after each callback, so
reentrant append is consumed in the same drain. Neither collection is an
unconditional snapshot-and-clear queue.
For an `AnimDoneHook`:
```text
@ -175,8 +219,52 @@ if script_manager != null:
```
The tail is reached for both visible and Hidden objects when the object quantum
runs. A missing manager is simply skipped. No manager is reordered around
another manager based on local/remote/player/NPC classification.
runs and the object is Active. An inactive object skips this position/manager
body but still reaches Particle then Script. A missing manager is simply
skipped. No manager is reordered around another manager based on
local/remote/player/NPC classification.
### `CPhysicsObj::animate_static_object`
```text
if cell == null:
return
elapsed = current_time - update_time
if elapsed <= FrameEpsilon (0.000199999995 seconds):
update_time = current_time
return
if elapsed > HugeQuantum:
update_time = current_time
return
if part_array != null:
if state contains HasDefaultAnim:
part_array.Update(elapsed)
rotate root by the current angular vector
UpdatePartsInternal()
UpdateChildrenInternal()
if state contains HasDefaultScript and script_manager != null:
script_manager.UpdateScripts()
if particle_manager != null:
particle_manager.UpdateParticles()
process_hooks()
update_time = current_time
```
The static workset has no ordinary-object `MinQuantum` remainder gate or
`MaxQuantum` subdivision loop: it uses only the frame epsilon and
`HugeQuantum`, then passes the complete admitted elapsed time to PartArray.
It therefore has a distinct Script → Particle → hooks tail, not ordinary
objects' Particle → Script tail. acdream currently shares one
ParticleSystem and one PhysicsScriptRunner across every owner and advances
them once after the complete ordinary/static worksets. That is the registered
TS-51 adaptation and must not be silently reordered by an ownership-only
extraction.
## acdream conformance boundary
@ -186,9 +274,11 @@ unsupported `DetectionManager` is represented by an absent callback rather
than an invented behavior.
Animation-hook capture performs `AnimationDone` control effects immediately,
at the `process_hooks` boundary. Pose-dependent routing remains deferred until
the current frame's root/part poses are published. Deferred routing must never
invoke `AnimationDone` a second time and must never own
at the `process_hooks` boundary. Every other captured animation hook currently
remains deferred until the final root/part poses are published, including
particle/audio/light/translucency hooks, `CallPES`, and default-script
chaining. Deferred routing must never invoke `AnimationDone` a second time and
must never own
`MotionTableManager.UseTime`; that belongs only to the PartArray manager-tail
slot.
@ -199,17 +289,32 @@ slot.
2. Target runs before Movement; Movement runs before PartArray; PartArray runs
before PositionManager.
3. A missing manager does not shift or duplicate another callback.
4. Hidden objects skip sequence advance but retain the same manager tail.
5. A visual hook sees a pose published after capture, while an `AnimDoneHook`
changes motion state at capture time.
6. Deferred visual-hook drain never completes an animation twice and never
performs a zero-tick motion sweep.
7. A sub-MinQuantum frame performs no object-tail call; accumulated time is
consumed by a later quantum.
4. Parented, cell-less, and Frozen objects return before clock admission.
5. Inactive objects skip position/hooks/manager time but retain Particle then
Script; Hidden + Active objects skip sequence/physics but retain adjustment,
hooks, and the manager tail.
6. A non-`AnimationDone` hook sees a pose published after capture, while an
`AnimDoneHook` changes motion state at capture time.
7. Deferred hook drain never completes an animation twice and never performs
a zero-tick motion sweep; representative `CallPES`, default-script, and
translucency hooks retain their current deferred path.
8. An incomplete physics hook remains linked, and an animation hook appended
by another animation hook executes in the same retail drain.
9. Static objects retain their distinct Script → Particle → hooks order.
10. Ordinary epsilon/Min/Max/Huge boundaries and static epsilon/Huge
boundaries preserve their separate strict retail comparisons.
## Cross-reference notes
ACE's physics/motion ports remain interpretation aids for manager ownership
and state naming; the retail binary above is authoritative for order. The
existing `RemoteChaseEndToEndHarnessTests` hand-codes acdream's pre-R6 order
and is therefore a migration target, not an oracle.
ACE's `PhysicsObj.cs` corroborates ordinary Particle → Script and static
Script → Particle → hooks order. It does **not** corroborate the retail clock
or hook-lifetime gates: ACE uses its server `TickRate`/Min/Max policy and
clears physics hooks unconditionally, unlike retail's epsilon/Huge behavior
and completion-based hook retention. ACViewer exposes the same client-shaped
methods and manager order but shares ancestry with ACE, so that agreement is
corroboration rather than a second oracle. holtburger's native protocol crates
have no client physics-loop equivalent (its vendored ACViewer copy is not
independent). The retail binary above is authoritative for every boundary.
The existing
`RemoteChaseEndToEndHarnessTests` hand-codes acdream's pre-R6 order and is
therefore a migration target, not an oracle.