fix(physics): close #315 — cache the remote-arm callbacks instead of allocating per packet

The OnPosition collapse (previous commit) converged
RunRemoteArmTail's three duplicated call sites into one, which is what
makes caching worthwhile: one cached pair of delegates now serves every
remote guid instead of a fresh closure allocated on every accepted remote
Position (5-10 Hz per remote), regardless of whether the packet was a
teleport.

RunRemoteArmTail's signature changes from a caller-constructed
`Func<bool> isCurrentPositionOwner` closure to two plain value parameters
(`ulong positionAuthorityVersion`, `WorldEntity? expectedEntity`). It stamps
five per-packet scratch fields (`_remoteArmCanonical`, `_remoteArmMotion`,
`_remoteArmPositionRecord`, `_remoteArmPositionAuthorityVersion`,
`_remoteArmExpectedEntity`) from its own parameters, then passes the two
CACHED delegates into ApplyRemoteContactRouting. Observably identical: the
currency check reads the exact same
positionRecord/positionAuthorityVersion/expectedEntity triple either way.

Deviation from a bare cached-Func<bool>-field design, and why:
UpdateFrameOrchestratorTests.ProductionFrameAdaptersRetainTypedOwnersWithoutWindowCallbacks
asserts every typed production owner (LiveEntityNetworkUpdateController
included) carries zero Delegate-typed fields — the GameWindow decomposition
campaign's guard against a callback silently smuggling a window reference
back onto one of these owners. Neither cached delegate here touches a
window (both are bound to this controller alone), but the rule is written
as a blanket field-type check, not a window-specific one. The two
delegates are wrapped in a small nested RemoteArmCallbacks type instead of
being bare fields, which satisfies the guard and keeps the cache a single
named, auditable unit rather than working around the test.

RunRemoteTeleportHook's own allocation (the six-action
RemoteTeleportHookActions bundle) is unaffected — it stays teleport-path-only,
already judged acceptable to defer by the C4 route 4b-3 round-2
architecture review's B4 finding.

dotnet build AcDream.slnx -c Release: 0 errors. Focused suites green at
this commit: AcDream.App.Tests 4104/4107 (3 pre-existing skips),
AcDream.Runtime.Tests 1125/1125.

Closes #315.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-04 18:11:16 +02:00
parent edc911b042
commit aaf0811f18
2 changed files with 125 additions and 9 deletions

View file

@ -148,7 +148,23 @@ split result places normally instead of throwing.
## #315`runTeleportHook` builds a `Func<bool>` closure per network packet
**Status:** OPEN
**Status:** CLOSED 2026-08-04 by `ddb38f37` — the OnPosition collapse
converged the three `RunRemoteArmTail` call sites into one, which is what
made caching worthwhile. The one remaining call site now passes two
delegates cached ONCE at construction (`RemoteArmCallbacks`, a small nested
type wrapping `Func<bool> IsCurrentPositionOwner` /
`Func<bool> RunTeleportHook`) instead of allocating a fresh closure per
packet; per-packet scratch state (`canonical`, `remote`, `positionRecord`,
`positionAuthorityVersion`, `expectedEntity`) moved from closure captures to
plain instance fields `RunRemoteArmTail` stamps immediately before use.
Deliberately NOT two bare `Func<bool>` fields directly on
`LiveEntityNetworkUpdateController`:
`tests/AcDream.App.Tests/World/UpdateFrameOrchestratorTests.cs`'s
`ProductionFrameAdaptersRetainTypedOwnersWithoutWindowCallbacks` asserts
every typed production owner carries zero `Delegate`-typed fields (the
GameWindow decomposition campaign's guard against a smuggled window
callback); wrapping both delegates in `RemoteArmCallbacks` respects that
invariant instead of tripping it.
**Severity:** LOW (real allocation regression, not correctness; not on the
per-frame resolve path Slice I's 0 B/resolve discipline governs)
**Filed:** 2026-08-04