fix(streaming): close #54 — plumb JobKind through BuildLandblockForStreaming
Bug A's fix (commit `9217fd9`) patched at the worker output by stripping
entities from far-tier `LoadedLandblock`s after the full `LoadNear` path
ran. The worker still wasted CPU on `LandBlockInfo` reads + entity
hydration + `SceneryGenerator` math + interior-cell walks for ~544
far-tier LBs at radius=12, just to throw the work away.
This commit plumbs `LandblockStreamJobKind` through to the factory so the
worker can branch at the source:
- `LandblockStreamer.cs`: replace the `Func<uint, LoadedLandblock?>`
factory with `Func<uint, LandblockStreamJobKind, LoadedLandblock?>` as
the primary ctor signature. Add a back-compat overload that wraps the
old single-arg signature (`(id, _) => loadLandblock(id)`) so existing
test code keeps compiling without modification — the 5 ctor sites in
`LandblockStreamerTests.cs` now resolve to the overload. `HandleJob`
passes `load.Kind` to the factory; the post-load entity-strip is
retained as a `Debug.Assert` + Release safety net.
- `GameWindow.cs`: `BuildLandblockForStreaming(uint, JobKind)` branches
on `kind == LoadFar` at the top — reads only the `LandBlock` heightmap
dat and returns a `LoadedLandblock` with `Array.Empty<WorldEntity>()`.
Skips `LandblockLoader.Load` (which reads `LandBlockInfo`),
`BuildSceneryEntitiesForStreaming`, and `BuildInteriorEntitiesForStreaming`
entirely. Near-tier path is unchanged. Both call sites updated to pass
the kind through the lambda: `(id, kind) => BuildLandblockForStreaming(id, kind)`.
Tests: 1688/1696 (8 pre-existing physics/input failures unchanged).
Streaming-targeted filter (30 tests covering LandblockStreamer +
StreamingController + StreamingRegion) all green via the back-compat
overload — no test code needed updating.
Per-LB worker cost on far-tier: was ~tens of ms (full hydration,
including LandBlockInfo + scenery generation + interior cells); now a
single `LandBlock` dat read (~sub-ms).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b19f1d10ec
commit
bf31e59805
2 changed files with 66 additions and 22 deletions
|
|
@ -1652,7 +1652,7 @@ public sealed class GameWindow : IDisposable
|
|||
// 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 => BuildLandblockForStreaming(id),
|
||||
loadLandblock: (id, kind) => BuildLandblockForStreaming(id, kind),
|
||||
buildMeshOrNull: (id, lb) =>
|
||||
{
|
||||
if (lb is null || _heightTable is null || _blendCtx is null || _surfaceCache is null)
|
||||
|
|
@ -4639,8 +4639,18 @@ public sealed class GameWindow : IDisposable
|
|||
/// DatReaderWriter) and pure CPU work. No GL calls here.
|
||||
///
|
||||
/// MVP scope: stabs only. Scenery + interior added in Task 8.
|
||||
///
|
||||
/// ISSUE #54 (post-A.5): far-tier loads (<c>kind == LoadFar</c>) skip
|
||||
/// LandBlockInfo + scenery + interior hydration. They return only the
|
||||
/// LandBlock heightmap dat record + an empty entity list — enough for
|
||||
/// terrain-mesh build on the next phase. Near-tier loads run the full
|
||||
/// path. This replaces Bug A's post-load entity strip in
|
||||
/// <see cref="AcDream.App.Streaming.LandblockStreamer"/> with an
|
||||
/// early-out at the source.
|
||||
/// </summary>
|
||||
private AcDream.Core.World.LoadedLandblock? BuildLandblockForStreaming(uint landblockId)
|
||||
private AcDream.Core.World.LoadedLandblock? BuildLandblockForStreaming(
|
||||
uint landblockId,
|
||||
AcDream.App.Streaming.LandblockStreamJobKind kind)
|
||||
{
|
||||
if (_dats is null) return null;
|
||||
|
||||
|
|
@ -4653,14 +4663,31 @@ public sealed class GameWindow : IDisposable
|
|||
// contention by pre-building render-thread work on the worker.
|
||||
lock (_datLock)
|
||||
{
|
||||
return BuildLandblockForStreamingLocked(landblockId);
|
||||
return BuildLandblockForStreamingLocked(landblockId, kind);
|
||||
}
|
||||
}
|
||||
|
||||
private AcDream.Core.World.LoadedLandblock? BuildLandblockForStreamingLocked(uint landblockId)
|
||||
private AcDream.Core.World.LoadedLandblock? BuildLandblockForStreamingLocked(
|
||||
uint landblockId,
|
||||
AcDream.App.Streaming.LandblockStreamJobKind kind)
|
||||
{
|
||||
if (_dats is null) return null;
|
||||
|
||||
// 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)
|
||||
{
|
||||
var heightmapOnly = _dats.Get<DatReaderWriter.DBObjs.LandBlock>(landblockId);
|
||||
if (heightmapOnly is null) return null;
|
||||
return new AcDream.Core.World.LoadedLandblock(
|
||||
landblockId,
|
||||
heightmapOnly,
|
||||
System.Array.Empty<AcDream.Core.World.WorldEntity>());
|
||||
}
|
||||
|
||||
var baseLoaded = AcDream.Core.World.LandblockLoader.Load(_dats, landblockId);
|
||||
if (baseLoaded is null) return null;
|
||||
|
||||
|
|
@ -8157,7 +8184,7 @@ public sealed class GameWindow : IDisposable
|
|||
_streamer.Dispose();
|
||||
|
||||
_streamer = new AcDream.App.Streaming.LandblockStreamer(
|
||||
loadLandblock: id => BuildLandblockForStreaming(id),
|
||||
loadLandblock: (id, kind) => BuildLandblockForStreaming(id, kind),
|
||||
buildMeshOrNull: (id, lb) =>
|
||||
{
|
||||
if (lb is null || _heightTable is null || _blendCtx is null || _surfaceCache is null)
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ public sealed class LandblockStreamer : IDisposable
|
|||
/// </summary>
|
||||
public const int DefaultDrainBatchSize = 4;
|
||||
|
||||
private readonly Func<uint, LoadedLandblock?> _loadLandblock;
|
||||
private readonly Func<uint, LandblockStreamJobKind, LoadedLandblock?> _loadLandblock;
|
||||
private readonly Func<uint, LoadedLandblock?, AcDream.Core.Terrain.LandblockMeshData?> _buildMeshOrNull;
|
||||
private readonly Channel<LandblockStreamJob> _inbox;
|
||||
private readonly Channel<LandblockStreamResult> _outbox;
|
||||
|
|
@ -60,8 +60,15 @@ public sealed class LandblockStreamer : IDisposable
|
|||
private Thread? _worker;
|
||||
private int _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Primary ctor — the factory takes the job's <see cref="LandblockStreamJobKind"/>
|
||||
/// so it can branch on far-tier vs near-tier and skip entity hydration on far-tier
|
||||
/// loads (heightmap-only). See ISSUE #54: prior to this signature the worker always
|
||||
/// called the full-load path and stripped entities at the output, wasting per-LB
|
||||
/// <c>LandBlockInfo</c> + <c>SceneryGenerator</c> work.
|
||||
/// </summary>
|
||||
public LandblockStreamer(
|
||||
Func<uint, LoadedLandblock?> loadLandblock,
|
||||
Func<uint, LandblockStreamJobKind, LoadedLandblock?> loadLandblock,
|
||||
Func<uint, LoadedLandblock?, AcDream.Core.Terrain.LandblockMeshData?>? buildMeshOrNull = null)
|
||||
{
|
||||
_loadLandblock = loadLandblock;
|
||||
|
|
@ -74,6 +81,19 @@ public sealed class LandblockStreamer : IDisposable
|
|||
new UnboundedChannelOptions { SingleReader = true, SingleWriter = true });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Back-compat overload — wraps a kind-agnostic factory so existing test code
|
||||
/// that doesn't care about the JobKind branch keeps compiling. The wrapper
|
||||
/// ignores the kind and calls the factory once per LB regardless of tier.
|
||||
/// New production code should use the primary 2-arg ctor.
|
||||
/// </summary>
|
||||
public LandblockStreamer(
|
||||
Func<uint, LoadedLandblock?> loadLandblock,
|
||||
Func<uint, LoadedLandblock?, AcDream.Core.Terrain.LandblockMeshData?>? buildMeshOrNull = null)
|
||||
: this((id, _) => loadLandblock(id), buildMeshOrNull)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Activate the dedicated background worker thread. Idempotent and
|
||||
/// thread-safe: concurrent callers will only spawn one worker; subsequent
|
||||
|
|
@ -177,22 +197,15 @@ public sealed class LandblockStreamer : IDisposable
|
|||
switch (job)
|
||||
{
|
||||
case LandblockStreamJob.Load load:
|
||||
// A.5 T26 follow-up (Bug A): far-tier LBs must NOT contribute
|
||||
// entities to GpuWorldState — that defeats the whole purpose of
|
||||
// the two-tier split. The factory still builds the full entity
|
||||
// layer (LandblockLoader + scenery generation + interior cells)
|
||||
// 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.
|
||||
// ISSUE #54 (post-A.5): JobKind is now plumbed through to the
|
||||
// factory, so far-tier loads can skip LandBlockInfo + scenery
|
||||
// + interior hydration on the worker thread (heightmap-only).
|
||||
// The post-load entity-strip below is retained as a Debug
|
||||
// assertion + Release safety net for the case where a buggy
|
||||
// factory returns far-tier with entities anyway.
|
||||
try
|
||||
{
|
||||
var lb = _loadLandblock(load.LandblockId);
|
||||
var lb = _loadLandblock(load.LandblockId, load.Kind);
|
||||
if (lb is null)
|
||||
{
|
||||
_outbox.Writer.TryWrite(new LandblockStreamResult.Failed(
|
||||
|
|
@ -210,7 +223,11 @@ public sealed class LandblockStreamer : IDisposable
|
|||
? LandblockStreamTier.Far : LandblockStreamTier.Near;
|
||||
if (tier == LandblockStreamTier.Far && lb.Entities.Count > 0)
|
||||
{
|
||||
// Strip entities — far-tier ships terrain only.
|
||||
// Belt-and-suspenders: factory should have skipped
|
||||
// entity hydration for LoadFar. If it didn't, fail
|
||||
// loud in Debug builds and strip in Release.
|
||||
System.Diagnostics.Debug.Assert(false,
|
||||
$"Far-tier factory should skip entity hydration; got {lb.Entities.Count} entities for LB 0x{load.LandblockId:X8}");
|
||||
lb = new LoadedLandblock(
|
||||
lb.LandblockId,
|
||||
lb.Heightmap,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue