From 0afd741ea7e78565323d6f7140b363d926137d3f Mon Sep 17 00:00:00 2001 From: Erik Date: Sun, 10 May 2026 08:20:20 +0200 Subject: [PATCH] feat(A.5 T18): use cached WorldEntity AABB in dispatcher; populate at register MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per Phase A.5 spec §4.6 Change #2: WalkEntities's per-entity AABB frustum cull was recomputing Position±5 per frame per entity. With ~10.7K entities (N1=4) at 240 FPS that is ~2.5M wasted Vector3 ops/sec. Read the AABB from the WorldEntity cache (T8 schema) instead. RefreshAabb runs lazily on AabbDirty=true. Populate at register time: - LandblockLoader.BuildEntitiesFromInfo: RefreshAabb after each new WorldEntity construction (stabs + buildings). Refactored from inline object-initializer to named variable to enable the call. - EntitySpawnAdapter.OnCreate: RefreshAabb after entity state init (position/rotation already set via the WorldEntity passed in). Dynamic entities (NPCs, players) move every frame via direct Position writes in GameWindow.cs. Migrated all three per-frame write sites to SetPosition() (T8 mutator) so AabbDirty propagates: - line 5942: player entity render position update - line 6951: remote animated entity interpolated path - line 7279: remote animated entity landing/movement path The lazy RefreshAabb in WalkEntities catches up on the next frame after any SetPosition call — render thread only, no races. Build green, 986 passed / 8 pre-existing failures unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/AcDream.App/Rendering/GameWindow.cs | 6 +++--- src/AcDream.App/Rendering/Wb/EntitySpawnAdapter.cs | 6 ++++++ src/AcDream.Core/World/LandblockLoader.cs | 12 ++++++++---- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/AcDream.App/Rendering/GameWindow.cs b/src/AcDream.App/Rendering/GameWindow.cs index 018892a..f788b83 100644 --- a/src/AcDream.App/Rendering/GameWindow.cs +++ b/src/AcDream.App/Rendering/GameWindow.cs @@ -5939,7 +5939,7 @@ public sealed class GameWindow : IDisposable // the physics-resolved location each frame. if (_entitiesByServerGuid.TryGetValue(_playerServerGuid, out var pe)) { - pe.Position = result.RenderPosition; + pe.SetPosition(result.RenderPosition); // A.5 T18: SetPosition propagates AabbDirty pe.Rotation = System.Numerics.Quaternion.CreateFromAxisAngle( System.Numerics.Vector3.UnitZ, _playerController.Yaw - MathF.PI / 2f); @@ -6948,7 +6948,7 @@ public sealed class GameWindow : IDisposable rm.MaxSeqSpeedSinceLastUP = seqSpeedNow; } - ae.Entity.Position = rm.Body.Position; + ae.Entity.SetPosition(rm.Body.Position); // A.5 T18: SetPosition propagates AabbDirty ae.Entity.Rotation = rm.Body.Orientation; } else @@ -7276,7 +7276,7 @@ public sealed class GameWindow : IDisposable } } - ae.Entity.Position = rm.Body.Position; + ae.Entity.SetPosition(rm.Body.Position); // A.5 T18: SetPosition propagates AabbDirty ae.Entity.Rotation = rm.Body.Orientation; } } diff --git a/src/AcDream.App/Rendering/Wb/EntitySpawnAdapter.cs b/src/AcDream.App/Rendering/Wb/EntitySpawnAdapter.cs index eb05d92..6303220 100644 --- a/src/AcDream.App/Rendering/Wb/EntitySpawnAdapter.cs +++ b/src/AcDream.App/Rendering/Wb/EntitySpawnAdapter.cs @@ -128,6 +128,12 @@ public sealed class EntitySpawnAdapter } } + // A.5 T18: populate cached AABB so WalkEntities reads from the cache + // rather than recomputing Position±5 per frame. Called here because + // all entity-state initialization (position, rotation) is complete + // by this point via the WorldEntity passed in. + entity.RefreshAabb(); + // Build the per-entity AnimatedEntityState. The sequencer factory // may return a stub (in tests) or a fully-constructed sequencer from // the MotionTable (in production). Factory must not return null — diff --git a/src/AcDream.Core/World/LandblockLoader.cs b/src/AcDream.Core/World/LandblockLoader.cs index 4234c11..fc3d30e 100644 --- a/src/AcDream.Core/World/LandblockLoader.cs +++ b/src/AcDream.Core/World/LandblockLoader.cs @@ -42,28 +42,32 @@ public static class LandblockLoader { if (!IsSupported(stab.Id)) continue; - result.Add(new WorldEntity + var stabEntity = new WorldEntity { Id = nextId++, SourceGfxObjOrSetupId = stab.Id, Position = stab.Frame.Origin, Rotation = stab.Frame.Orientation, MeshRefs = Array.Empty(), - }); + }; + stabEntity.RefreshAabb(); // A.5 T18: populate cached AABB at construction + result.Add(stabEntity); } foreach (var building in info.Buildings) { if (!IsSupported(building.ModelId)) continue; - result.Add(new WorldEntity + var buildingEntity = new WorldEntity { Id = nextId++, SourceGfxObjOrSetupId = building.ModelId, Position = building.Frame.Origin, Rotation = building.Frame.Orientation, MeshRefs = Array.Empty(), - }); + }; + buildingEntity.RefreshAabb(); // A.5 T18: populate cached AABB at construction + result.Add(buildingEntity); } return result;