refactor(streaming): capture landblock build origin

This commit is contained in:
Erik 2026-07-21 20:12:56 +02:00
parent 2216a02de5
commit 090b0354ff
8 changed files with 522 additions and 54 deletions

View file

@ -2491,8 +2491,8 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
// buildMeshOrNull (T12) receives the already-loaded LoadedLandblock so
// it can call LandblockMesh.Build without a dat read — _heightTable and
// _blendCtx are read-only after init, _surfaceCache is ConcurrentDictionary (T9).
_streamer = new AcDream.App.Streaming.LandblockStreamer(
loadLandblock: (id, kind) => BuildLandblockForStreaming(id, kind),
_streamer = AcDream.App.Streaming.LandblockStreamer.CreateForRequests(
loadLandblock: BuildLandblockForStreaming,
buildMeshOrNull: (id, lb) =>
{
if (lb is null || _heightTable is null || _blendCtx is null || _surfaceCache is null)
@ -2508,7 +2508,14 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
_streamer.Start();
_streamingController = new AcDream.App.Streaming.StreamingController(
enqueueLoad: (id, kind, generation) => _streamer.EnqueueLoad(id, kind, generation),
enqueueLoad: (id, kind, generation) => _streamer.EnqueueLoad(
new AcDream.App.Streaming.LandblockBuildRequest(
id,
kind,
generation,
new AcDream.App.Streaming.LandblockBuildOrigin(
_liveCenterX,
_liveCenterY))),
enqueueUnload: (id, generation) => _streamer.EnqueueUnload(id, generation),
drainCompletions: _streamer.DrainCompletions,
applyTerrain: ApplyLoadedTerrain,
@ -3792,8 +3799,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
/// early-out at the source.
/// </summary>
private AcDream.App.Streaming.LandblockBuild? BuildLandblockForStreaming(
uint landblockId,
AcDream.App.Streaming.LandblockStreamJobKind kind)
AcDream.App.Streaming.LandblockBuildRequest request)
{
if (_dats is null) return null;
@ -3816,31 +3822,31 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
{
long waitedMs = sw.ElapsedMilliseconds;
sw.Restart();
var built = BuildLandblockForStreamingLocked(landblockId, kind);
var built = BuildLandblockForStreamingLocked(request);
AcDream.Core.Physics.PhysicsDiagnostics.LogTeleport(
"BUILD", landblockId,
$"waited={waitedMs}ms held={sw.ElapsedMilliseconds}ms kind={kind}");
"BUILD", request.LandblockId,
$"waited={waitedMs}ms held={sw.ElapsedMilliseconds}ms kind={request.Kind}");
return built;
}
}
lock (_datLock)
{
return BuildLandblockForStreamingLocked(landblockId, kind);
return BuildLandblockForStreamingLocked(request);
}
}
private AcDream.App.Streaming.LandblockBuild? BuildLandblockForStreamingLocked(
uint landblockId,
AcDream.App.Streaming.LandblockStreamJobKind kind)
AcDream.App.Streaming.LandblockBuildRequest request)
{
if (_dats is null) return null;
uint landblockId = request.LandblockId;
// ISSUE #54: far-tier early-out — heightmap only, empty entities.
// Skips the LandBlockInfo dat read AND all entity hydration (stabs
// + buildings) AND the SceneryGenerator AND interior cells. Cuts
// worker-thread cost per far-tier LB from ~tens of ms to a single
// dat read.
if (kind == AcDream.App.Streaming.LandblockStreamJobKind.LoadFar)
if (request.Kind == AcDream.App.Streaming.LandblockStreamJobKind.LoadFar)
{
var heightmapOnly = _dats.Get<DatReaderWriter.DBObjs.LandBlock>(landblockId);
if (heightmapOnly is null) return null;
@ -3849,7 +3855,8 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
landblockId,
heightmapOnly,
System.Array.Empty<AcDream.Core.World.WorldEntity>(),
AcDream.Core.World.PhysicsDatBundle.Empty)); // far tier: no cells/buildings/entities
AcDream.Core.World.PhysicsDatBundle.Empty),
Origin: request.Origin); // far tier: no cells/buildings/entities
}
var baseLoaded = AcDream.Core.World.LandblockLoader.Load(_dats, landblockId);
@ -3858,8 +3865,8 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
int lbX = (int)((landblockId >> 24) & 0xFFu);
int lbY = (int)((landblockId >> 16) & 0xFFu);
var worldOffset = new System.Numerics.Vector3(
(lbX - _liveCenterX) * 192f,
(lbY - _liveCenterY) * 192f,
(lbX - request.Origin.CenterX) * 192f,
(lbY - request.Origin.CenterY) * 192f,
0f);
// Hydrate the stabs: same logic as the old OnLoad preload. Each stab
@ -3926,9 +3933,18 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
// Task 8: merge stabs + scenery + interior into one entity list.
var merged = new List<AcDream.Core.World.WorldEntity>(hydrated);
merged.AddRange(BuildSceneryEntitiesForStreaming(baseLoaded, lbX, lbY));
merged.AddRange(BuildSceneryEntitiesForStreaming(
baseLoaded,
lbX,
lbY,
request.Origin));
var envCellBuild = new AcDream.App.Rendering.Wb.EnvCellLandblockBuildBuilder(landblockId);
merged.AddRange(BuildInteriorEntitiesForStreaming(landblockId, lbX, lbY, envCellBuild));
merged.AddRange(BuildInteriorEntitiesForStreaming(
landblockId,
lbX,
lbY,
request.Origin,
envCellBuild));
// datLock fix: pre-read (under the worker's _datLock) every dat the apply
// consumes, so ApplyLoadedTerrainLocked can run lock-free. Built AFTER
@ -3941,7 +3957,8 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
baseLoaded.Heightmap,
merged,
physicsDats),
completedEnvCells);
completedEnvCells,
request.Origin);
}
private void EnsureEnvCellMeshesAfterPin(
@ -4036,7 +4053,10 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
/// LoadedLandblock instead of iterating worldView.Landblocks.
/// </summary>
private List<AcDream.Core.World.WorldEntity> BuildSceneryEntitiesForStreaming(
AcDream.Core.World.LoadedLandblock lb, int lbX, int lbY)
AcDream.Core.World.LoadedLandblock lb,
int lbX,
int lbY,
AcDream.App.Streaming.LandblockBuildOrigin origin)
{
var result = new List<AcDream.Core.World.WorldEntity>();
if (_dats is null || _heightTable is null) return result;
@ -4069,8 +4089,8 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
if (spawns.Count == 0) return result;
var lbOffset = new System.Numerics.Vector3(
(lbX - _liveCenterX) * 192f,
(lbY - _liveCenterY) * 192f,
(lbX - origin.CenterX) * 192f,
(lbY - origin.CenterY) * 192f,
0f);
// Per-landblock id namespace. The top nibble identifies procedural
@ -4265,6 +4285,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
uint landblockId,
int lbX,
int lbY,
AcDream.App.Streaming.LandblockBuildOrigin origin,
AcDream.App.Rendering.Wb.EnvCellLandblockBuildBuilder envCellBuild)
{
var result = new List<AcDream.Core.World.WorldEntity>();
@ -4274,8 +4295,8 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
if (lbInfo is null || lbInfo.NumCells == 0) return result;
var lbOffset = new System.Numerics.Vector3(
(lbX - _liveCenterX) * 192f,
(lbY - _liveCenterY) * 192f,
(lbX - origin.CenterX) * 192f,
(lbY - origin.CenterY) * 192f,
0f);
// Per-landblock id namespace — see AcDream.Core.World.InteriorEntityIdAllocator
@ -4654,8 +4675,8 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
int lbX = (int)lbXu;
int lbY = (int)lbYu;
var origin = new System.Numerics.Vector3(
(lbX - _liveCenterX) * 192f,
(lbY - _liveCenterY) * 192f,
(lbX - build.Origin.CenterX) * 192f,
(lbY - build.Origin.CenterY) * 192f,
0f);
// Phase A.5 T15/T16: route through AddLandblockWithMesh — the named
@ -4820,7 +4841,7 @@ public sealed class GameWindow : IDisposable, ILiveAnimationPresentationContext
{
// #146 (2026-06-24): clear this landblock's prior building cache so
// the loop re-bases each building's STREAMING-RELATIVE WorldTransform
// with the CURRENT _liveCenter (origin, recomputed above per apply).
// with this completion's captured origin (recomputed above per apply).
// CacheBuilding's per-cell first-wins then applies fresh within this
// pass — mirrors terrain's per-apply WorldOffset re-base (AddLandblock).
// Without it, CacheBuilding's idempotent guard locks the transform at