fix(A.5): strip far-tier entities in worker (Bug A — far tier optimization)

Phase A.5's two-tier streaming spec promised that far-tier landblocks
ship terrain ONLY — no entities, no scenery, no interior cells. T13/T16
wired the controller side (RecenterTo emits ToLoadFar/ToLoadNear/ToPromote;
controller passes JobKind to the worker), but the worker's HandleJob
never branched on Kind: every load called BuildLandblockForStreaming
which runs the full hydration + scenery generation + interior cell path.

Result: at default radii (N₁=4 / N₂=12), 540 far-tier LBs each loaded
their full entity layer (~132 entities/LB → ~71K entities total) into
GpuWorldState. The dispatcher then walked all ~54K entities per frame
(post-frustum-cull), driving the entity dispatcher cpu_us from ~3.6ms
median (T24 baseline) to ~18-21ms (post-T22.5 horizon-test). User-
observed: 40 FPS / 25ms frame time at horizon-safe settings; system
crash at full High preset.

Minimum-diff fix: in LandblockStreamer.HandleJob, after
_loadLandblock returns, strip Entities to empty for LoadFar before
posting Loaded. Worker still does wasted hydration CPU (off the render
thread, harmless). Render-side dispatcher walk drops from ~54K to ~10K
entities/frame.

Math: post-fix entity dispatcher should drop to ~3-4ms median at N₁=4 /
N₂=12 (matches T24's 3.6ms at radius=5 single-tier, since N₁=4 has 33%
fewer near entities than N₁=5).

Future optimization (N.6 / A.6): plumb JobKind through
BuildLandblockForStreaming so the worker also skips the wasted CPU.
Out of A.5 scope.

Bug B (T17 WalkEntities allocation) is a smaller perf hit — defer if
post-Bug-A FPS is acceptable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-05-10 09:10:42 +02:00
parent 28d2c6018e
commit 9217fd93cd

View file

@ -177,11 +177,19 @@ public sealed class LandblockStreamer : IDisposable
switch (job) switch (job)
{ {
case LandblockStreamJob.Load load: case LandblockStreamJob.Load load:
// TODO(A.5 T16): route by load.Kind. LoadFar will skip // A.5 T26 follow-up (Bug A): far-tier LBs must NOT contribute
// LandBlockInfo + scenery generation; PromoteToNear will skip // entities to GpuWorldState — that defeats the whole purpose of
// mesh build (terrain already on GPU). Today every Kind takes // the two-tier split. The factory still builds the full entity
// the full-load path via _loadLandblock, which matches today's // layer (LandblockLoader + scenery generation + interior cells)
// single-tier semantics. // regardless of Kind because it doesn't know about JobKind today.
// We strip Entities here for far-tier results so the render-
// thread dispatcher walks only near-tier (~10K) entities, not
// all (~71K) entities at radius=12.
//
// Wasted worker-thread CPU is acceptable (it's off the render
// thread). A future optimization (TODO N.6 or A.6) plumbs Kind
// through BuildLandblockForStreaming so the dat read + scenery
// generation are skipped entirely for far-tier.
try try
{ {
var lb = _loadLandblock(load.LandblockId); var lb = _loadLandblock(load.LandblockId);
@ -200,6 +208,14 @@ public sealed class LandblockStreamer : IDisposable
} }
var tier = load.Kind == LandblockStreamJobKind.LoadFar var tier = load.Kind == LandblockStreamJobKind.LoadFar
? LandblockStreamTier.Far : LandblockStreamTier.Near; ? LandblockStreamTier.Far : LandblockStreamTier.Near;
if (tier == LandblockStreamTier.Far && lb.Entities.Count > 0)
{
// Strip entities — far-tier ships terrain only.
lb = new LoadedLandblock(
lb.LandblockId,
lb.Heightmap,
System.Array.Empty<AcDream.Core.World.WorldEntity>());
}
_outbox.Writer.TryWrite(new LandblockStreamResult.Loaded( _outbox.Writer.TryWrite(new LandblockStreamResult.Loaded(
load.LandblockId, tier, lb, mesh)); load.LandblockId, tier, lb, mesh));
} }