feat(A.5 T12): inject mesh-build dependency into LandblockStreamer
Replaces the T7-temporary default! MeshData placeholder. Streamer now takes Func<uint, LoadedLandblock?, LandblockMeshData?> at construction; the worker calls it after _loadLandblock succeeds and passes the pre-built mesh into LandblockStreamResult.Loaded. GameWindow's buildMeshOrNull factory takes the already-loaded LoadedLandblock (lb.Heightmap is the LandBlock dat object), so no additional dat read is needed — _heightTable and _blendCtx are read-only after init, _surfaceCache is ConcurrentDictionary (T9). Zero dat lock needed inside the mesh-build closure. StreamingController._applyTerrain delegate signature widened to Action<LoadedLandblock, LandblockMeshData> so the pre-built mesh flows render-thread-side via the Loaded result. ApplyLoadedTerrainLocked now accepts meshData and calls _terrain.AddLandblock directly, skipping the per-frame LandblockMesh.Build that previously ran on the render thread (~5ms per LB at radius=12 first traversal). StreamingControllerTests updated: all four applyTerrain lambdas adapted to the two-arg Action signature. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
00bb030c9f
commit
0405947bac
3 changed files with 43 additions and 27 deletions
|
|
@ -1584,11 +1584,24 @@ public sealed class GameWindow : IDisposable
|
|||
Console.WriteLine($"streaming: radius={_streamingRadius} (window={2*_streamingRadius+1}×{2*_streamingRadius+1})");
|
||||
|
||||
// Phase A.5 T11+: the streamer now runs on a dedicated worker thread.
|
||||
// loadLandblock and buildMeshOrNull are called on the worker; both
|
||||
// closures acquire _datLock (T10) before touching DatCollection.
|
||||
// T12 wires the real mesh-build factory below.
|
||||
// loadLandblock acquires _datLock (T10) before touching DatCollection.
|
||||
// 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 => BuildLandblockForStreaming(id));
|
||||
loadLandblock: id => BuildLandblockForStreaming(id),
|
||||
buildMeshOrNull: (id, lb) =>
|
||||
{
|
||||
if (lb is null || _heightTable is null || _blendCtx is null || _surfaceCache is null)
|
||||
return null;
|
||||
uint lbX = (id >> 24) & 0xFFu;
|
||||
uint lbY = (id >> 16) & 0xFFu;
|
||||
// _surfaceCache is ConcurrentDictionary (T9) — safe from worker thread.
|
||||
// _heightTable and _blendCtx are read-only after initialization.
|
||||
// lb.Heightmap is the pre-loaded LandBlock; no dat read needed here.
|
||||
return AcDream.Core.Terrain.LandblockMesh.Build(
|
||||
lb.Heightmap, lbX, lbY, _heightTable, _blendCtx, _surfaceCache);
|
||||
});
|
||||
_streamer.Start();
|
||||
|
||||
_streamingController = new AcDream.App.Streaming.StreamingController(
|
||||
|
|
@ -4987,24 +5000,26 @@ public sealed class GameWindow : IDisposable
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// Phase A.1: render-thread callback from StreamingController.Tick
|
||||
/// Phase A.1 / A.5 T12: render-thread callback from StreamingController.Tick
|
||||
/// whenever a new landblock's terrain + entities are ready for GPU upload.
|
||||
/// Mirrors the terrain-build + entity-upload part of the old preload.
|
||||
/// Phase A.5 T12: the worker pre-builds <paramref name="meshData"/> off the
|
||||
/// render thread via <see cref="AcDream.Core.Terrain.LandblockMesh.Build"/>;
|
||||
/// this callback no longer pays that CPU cost.
|
||||
/// Must only be called from the render thread.
|
||||
/// </summary>
|
||||
private void ApplyLoadedTerrain(AcDream.Core.World.LoadedLandblock lb)
|
||||
private void ApplyLoadedTerrain(AcDream.Core.World.LoadedLandblock lb,
|
||||
AcDream.Core.Terrain.LandblockMeshData meshData)
|
||||
{
|
||||
if (_terrain is null || _dats is null || _blendCtx is null
|
||||
|| _heightTable is null || _surfaceCache is null) return;
|
||||
if (_terrain is null || _dats is null) return;
|
||||
|
||||
// Phase A.1 hotfix: render-thread path also takes the dat lock so it
|
||||
// doesn't race with BuildLandblockForStreaming on the worker thread.
|
||||
// Hold the lock across the entire apply because we read dats below
|
||||
// (GfxObj sub-mesh builds) and mutate the shared _surfaceCache from
|
||||
// LandblockMesh.Build.
|
||||
// Hold the lock across the entity hydration below (GfxObj sub-mesh
|
||||
// builds). The terrain mesh is pre-built by the worker (T12) and passed
|
||||
// in via meshData, so LandblockMesh.Build no longer runs under this lock.
|
||||
lock (_datLock)
|
||||
{
|
||||
ApplyLoadedTerrainLocked(lb);
|
||||
ApplyLoadedTerrainLocked(lb, meshData);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -5114,10 +5129,12 @@ public sealed class GameWindow : IDisposable
|
|||
_pendingCells.Add(loaded);
|
||||
}
|
||||
|
||||
private void ApplyLoadedTerrainLocked(AcDream.Core.World.LoadedLandblock lb)
|
||||
private void ApplyLoadedTerrainLocked(AcDream.Core.World.LoadedLandblock lb,
|
||||
AcDream.Core.Terrain.LandblockMeshData meshData)
|
||||
{
|
||||
if (_terrain is null || _dats is null || _blendCtx is null
|
||||
|| _heightTable is null || _surfaceCache is null) return;
|
||||
// _blendCtx / _surfaceCache no longer needed here (mesh pre-built by worker).
|
||||
// _heightTable still needed for physics TerrainSurface below.
|
||||
if (_terrain is null || _dats is null || _heightTable is null) return;
|
||||
|
||||
uint lbXu = (lb.LandblockId >> 24) & 0xFFu;
|
||||
uint lbYu = (lb.LandblockId >> 16) & 0xFFu;
|
||||
|
|
@ -5128,10 +5145,8 @@ public sealed class GameWindow : IDisposable
|
|||
(lbY - _liveCenterY) * 192f,
|
||||
0f);
|
||||
|
||||
// Build terrain mesh data on the render thread (pure CPU; acceptable
|
||||
// for the MVP; a future pass can move it to the worker thread).
|
||||
var meshData = AcDream.Core.Terrain.LandblockMesh.Build(
|
||||
lb.Heightmap, lbXu, lbYu, _heightTable, _blendCtx, _surfaceCache);
|
||||
// Phase A.5 T12: terrain mesh is pre-built by the worker thread and
|
||||
// passed in via meshData. No longer rebuilt here on the render thread.
|
||||
_terrain.AddLandblock(lb.LandblockId, meshData, origin);
|
||||
|
||||
// Step 4: drain pending LoadedCells from the worker thread.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AcDream.Core.Terrain;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Streaming;
|
||||
|
|
@ -19,7 +20,7 @@ public sealed class StreamingController
|
|||
private readonly Action<uint> _enqueueLoad;
|
||||
private readonly Action<uint> _enqueueUnload;
|
||||
private readonly Func<int, IReadOnlyList<LandblockStreamResult>> _drainCompletions;
|
||||
private readonly Action<LoadedLandblock> _applyTerrain;
|
||||
private readonly Action<LoadedLandblock, LandblockMeshData> _applyTerrain;
|
||||
private readonly Action<uint>? _removeTerrain;
|
||||
private readonly GpuWorldState _state;
|
||||
private StreamingRegion? _region;
|
||||
|
|
@ -48,7 +49,7 @@ public sealed class StreamingController
|
|||
Action<uint> enqueueLoad,
|
||||
Action<uint> enqueueUnload,
|
||||
Func<int, IReadOnlyList<LandblockStreamResult>> drainCompletions,
|
||||
Action<LoadedLandblock> applyTerrain,
|
||||
Action<LoadedLandblock, LandblockMeshData> applyTerrain,
|
||||
GpuWorldState state,
|
||||
int radius,
|
||||
Action<uint>? removeTerrain = null)
|
||||
|
|
@ -92,7 +93,7 @@ public sealed class StreamingController
|
|||
switch (result)
|
||||
{
|
||||
case LandblockStreamResult.Loaded loaded:
|
||||
_applyTerrain(loaded.Landblock);
|
||||
_applyTerrain(loaded.Landblock, loaded.MeshData);
|
||||
_state.AddLandblock(loaded.Landblock);
|
||||
break;
|
||||
case LandblockStreamResult.Unloaded unloaded:
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ public class StreamingControllerTests
|
|||
enqueueLoad: fake.EnqueueLoad,
|
||||
enqueueUnload: fake.EnqueueUnload,
|
||||
drainCompletions: fake.DrainCompletions,
|
||||
applyTerrain: _ => { },
|
||||
applyTerrain: (_, _) => { },
|
||||
state: state,
|
||||
radius: 2);
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ public class StreamingControllerTests
|
|||
var fake = new FakeStreamer();
|
||||
var controller = new StreamingController(
|
||||
fake.EnqueueLoad, fake.EnqueueUnload, fake.DrainCompletions,
|
||||
_ => { }, state, radius: 2);
|
||||
(_, _) => { }, state, radius: 2);
|
||||
|
||||
controller.Tick(50, 50);
|
||||
fake.Loads.Clear();
|
||||
|
|
@ -72,7 +72,7 @@ public class StreamingControllerTests
|
|||
var applied = new List<LoadedLandblock>();
|
||||
var controller = new StreamingController(
|
||||
fake.EnqueueLoad, fake.EnqueueUnload, fake.DrainCompletions,
|
||||
applied.Add, state, radius: 2);
|
||||
(lb, _) => applied.Add(lb), state, radius: 2);
|
||||
|
||||
// Note: LoadedLandblock's actual fields are LandblockId, Heightmap,
|
||||
// Entities (positional record). Adjust if the first positional arg
|
||||
|
|
@ -93,7 +93,7 @@ public class StreamingControllerTests
|
|||
var fake = new FakeStreamer();
|
||||
var controller = new StreamingController(
|
||||
fake.EnqueueLoad, fake.EnqueueUnload, fake.DrainCompletions,
|
||||
_ => { }, state, radius: 2);
|
||||
(_, _) => { }, state, radius: 2);
|
||||
|
||||
var lb = new LoadedLandblock(0x32320FFEu, new LandBlock(), System.Array.Empty<WorldEntity>());
|
||||
state.AddLandblock(lb);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue