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})");
|
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.
|
// Phase A.5 T11+: the streamer now runs on a dedicated worker thread.
|
||||||
// loadLandblock and buildMeshOrNull are called on the worker; both
|
// loadLandblock acquires _datLock (T10) before touching DatCollection.
|
||||||
// closures acquire _datLock (T10) before touching DatCollection.
|
// buildMeshOrNull (T12) receives the already-loaded LoadedLandblock so
|
||||||
// T12 wires the real mesh-build factory below.
|
// 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(
|
_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();
|
_streamer.Start();
|
||||||
|
|
||||||
_streamingController = new AcDream.App.Streaming.StreamingController(
|
_streamingController = new AcDream.App.Streaming.StreamingController(
|
||||||
|
|
@ -4987,24 +5000,26 @@ public sealed class GameWindow : IDisposable
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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.
|
/// 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.
|
/// Must only be called from the render thread.
|
||||||
/// </summary>
|
/// </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
|
if (_terrain is null || _dats is null) return;
|
||||||
|| _heightTable is null || _surfaceCache is null) return;
|
|
||||||
|
|
||||||
// Phase A.1 hotfix: render-thread path also takes the dat lock so it
|
// Phase A.1 hotfix: render-thread path also takes the dat lock so it
|
||||||
// doesn't race with BuildLandblockForStreaming on the worker thread.
|
// doesn't race with BuildLandblockForStreaming on the worker thread.
|
||||||
// Hold the lock across the entire apply because we read dats below
|
// Hold the lock across the entity hydration below (GfxObj sub-mesh
|
||||||
// (GfxObj sub-mesh builds) and mutate the shared _surfaceCache from
|
// builds). The terrain mesh is pre-built by the worker (T12) and passed
|
||||||
// LandblockMesh.Build.
|
// in via meshData, so LandblockMesh.Build no longer runs under this lock.
|
||||||
lock (_datLock)
|
lock (_datLock)
|
||||||
{
|
{
|
||||||
ApplyLoadedTerrainLocked(lb);
|
ApplyLoadedTerrainLocked(lb, meshData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -5114,10 +5129,12 @@ public sealed class GameWindow : IDisposable
|
||||||
_pendingCells.Add(loaded);
|
_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
|
// _blendCtx / _surfaceCache no longer needed here (mesh pre-built by worker).
|
||||||
|| _heightTable is null || _surfaceCache is null) return;
|
// _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 lbXu = (lb.LandblockId >> 24) & 0xFFu;
|
||||||
uint lbYu = (lb.LandblockId >> 16) & 0xFFu;
|
uint lbYu = (lb.LandblockId >> 16) & 0xFFu;
|
||||||
|
|
@ -5128,10 +5145,8 @@ public sealed class GameWindow : IDisposable
|
||||||
(lbY - _liveCenterY) * 192f,
|
(lbY - _liveCenterY) * 192f,
|
||||||
0f);
|
0f);
|
||||||
|
|
||||||
// Build terrain mesh data on the render thread (pure CPU; acceptable
|
// Phase A.5 T12: terrain mesh is pre-built by the worker thread and
|
||||||
// for the MVP; a future pass can move it to the worker thread).
|
// passed in via meshData. No longer rebuilt here on the render thread.
|
||||||
var meshData = AcDream.Core.Terrain.LandblockMesh.Build(
|
|
||||||
lb.Heightmap, lbXu, lbYu, _heightTable, _blendCtx, _surfaceCache);
|
|
||||||
_terrain.AddLandblock(lb.LandblockId, meshData, origin);
|
_terrain.AddLandblock(lb.LandblockId, meshData, origin);
|
||||||
|
|
||||||
// Step 4: drain pending LoadedCells from the worker thread.
|
// Step 4: drain pending LoadedCells from the worker thread.
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using AcDream.Core.Terrain;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
|
||||||
namespace AcDream.App.Streaming;
|
namespace AcDream.App.Streaming;
|
||||||
|
|
@ -19,7 +20,7 @@ public sealed class StreamingController
|
||||||
private readonly Action<uint> _enqueueLoad;
|
private readonly Action<uint> _enqueueLoad;
|
||||||
private readonly Action<uint> _enqueueUnload;
|
private readonly Action<uint> _enqueueUnload;
|
||||||
private readonly Func<int, IReadOnlyList<LandblockStreamResult>> _drainCompletions;
|
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 Action<uint>? _removeTerrain;
|
||||||
private readonly GpuWorldState _state;
|
private readonly GpuWorldState _state;
|
||||||
private StreamingRegion? _region;
|
private StreamingRegion? _region;
|
||||||
|
|
@ -48,7 +49,7 @@ public sealed class StreamingController
|
||||||
Action<uint> enqueueLoad,
|
Action<uint> enqueueLoad,
|
||||||
Action<uint> enqueueUnload,
|
Action<uint> enqueueUnload,
|
||||||
Func<int, IReadOnlyList<LandblockStreamResult>> drainCompletions,
|
Func<int, IReadOnlyList<LandblockStreamResult>> drainCompletions,
|
||||||
Action<LoadedLandblock> applyTerrain,
|
Action<LoadedLandblock, LandblockMeshData> applyTerrain,
|
||||||
GpuWorldState state,
|
GpuWorldState state,
|
||||||
int radius,
|
int radius,
|
||||||
Action<uint>? removeTerrain = null)
|
Action<uint>? removeTerrain = null)
|
||||||
|
|
@ -92,7 +93,7 @@ public sealed class StreamingController
|
||||||
switch (result)
|
switch (result)
|
||||||
{
|
{
|
||||||
case LandblockStreamResult.Loaded loaded:
|
case LandblockStreamResult.Loaded loaded:
|
||||||
_applyTerrain(loaded.Landblock);
|
_applyTerrain(loaded.Landblock, loaded.MeshData);
|
||||||
_state.AddLandblock(loaded.Landblock);
|
_state.AddLandblock(loaded.Landblock);
|
||||||
break;
|
break;
|
||||||
case LandblockStreamResult.Unloaded unloaded:
|
case LandblockStreamResult.Unloaded unloaded:
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ public class StreamingControllerTests
|
||||||
enqueueLoad: fake.EnqueueLoad,
|
enqueueLoad: fake.EnqueueLoad,
|
||||||
enqueueUnload: fake.EnqueueUnload,
|
enqueueUnload: fake.EnqueueUnload,
|
||||||
drainCompletions: fake.DrainCompletions,
|
drainCompletions: fake.DrainCompletions,
|
||||||
applyTerrain: _ => { },
|
applyTerrain: (_, _) => { },
|
||||||
state: state,
|
state: state,
|
||||||
radius: 2);
|
radius: 2);
|
||||||
|
|
||||||
|
|
@ -53,7 +53,7 @@ public class StreamingControllerTests
|
||||||
var fake = new FakeStreamer();
|
var fake = new FakeStreamer();
|
||||||
var controller = new StreamingController(
|
var controller = new StreamingController(
|
||||||
fake.EnqueueLoad, fake.EnqueueUnload, fake.DrainCompletions,
|
fake.EnqueueLoad, fake.EnqueueUnload, fake.DrainCompletions,
|
||||||
_ => { }, state, radius: 2);
|
(_, _) => { }, state, radius: 2);
|
||||||
|
|
||||||
controller.Tick(50, 50);
|
controller.Tick(50, 50);
|
||||||
fake.Loads.Clear();
|
fake.Loads.Clear();
|
||||||
|
|
@ -72,7 +72,7 @@ public class StreamingControllerTests
|
||||||
var applied = new List<LoadedLandblock>();
|
var applied = new List<LoadedLandblock>();
|
||||||
var controller = new StreamingController(
|
var controller = new StreamingController(
|
||||||
fake.EnqueueLoad, fake.EnqueueUnload, fake.DrainCompletions,
|
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,
|
// Note: LoadedLandblock's actual fields are LandblockId, Heightmap,
|
||||||
// Entities (positional record). Adjust if the first positional arg
|
// Entities (positional record). Adjust if the first positional arg
|
||||||
|
|
@ -93,7 +93,7 @@ public class StreamingControllerTests
|
||||||
var fake = new FakeStreamer();
|
var fake = new FakeStreamer();
|
||||||
var controller = new StreamingController(
|
var controller = new StreamingController(
|
||||||
fake.EnqueueLoad, fake.EnqueueUnload, fake.DrainCompletions,
|
fake.EnqueueLoad, fake.EnqueueUnload, fake.DrainCompletions,
|
||||||
_ => { }, state, radius: 2);
|
(_, _) => { }, state, radius: 2);
|
||||||
|
|
||||||
var lb = new LoadedLandblock(0x32320FFEu, new LandBlock(), System.Array.Empty<WorldEntity>());
|
var lb = new LoadedLandblock(0x32320FFEu, new LandBlock(), System.Array.Empty<WorldEntity>());
|
||||||
state.AddLandblock(lb);
|
state.AddLandblock(lb);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue