feat(streaming): shadow-publish flat collision assets

Carry one immutable prepared collision closure with each accepted near-tier generation and install graph plus flat views through the same retained publication receipt. Apply the same strict package-only rule to live entities, add exact sampled graph-authoritative comparison artifacts and lifecycle counters, and prove cancellation, demotion, rehydrate, revisit, teardown, reconnect, and the nine-stop route with 14,064 zero-mismatch samples.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-25 16:38:54 +02:00
parent f7ff9f4eea
commit d9446030e6
32 changed files with 2777 additions and 92 deletions

View file

@ -1,3 +1,4 @@
using System.Collections.Immutable;
using DatReaderWriter;
using AcDream.Content;
@ -17,19 +18,21 @@ namespace AcDream.App.Streaming;
public sealed class LandblockBuildFactory
{
private readonly IDatReaderWriter _dats;
private readonly IPreparedCollisionSource _preparedCollisions;
private readonly object _datLock;
private readonly float[] _heightTable;
private readonly AcDream.Core.Physics.PhysicsDataCache _physicsDataCache;
private readonly bool _dumpSceneryZ;
public LandblockBuildFactory(
IDatReaderWriter dats,
IPreparedCollisionSource preparedCollisions,
object datLock,
float[] heightTable,
AcDream.Core.Physics.PhysicsDataCache physicsDataCache,
bool dumpSceneryZ = false)
{
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
_preparedCollisions = preparedCollisions ??
throw new ArgumentNullException(nameof(preparedCollisions));
_datLock = datLock ?? throw new ArgumentNullException(nameof(datLock));
ArgumentNullException.ThrowIfNull(heightTable);
if (heightTable.Length < 256)
@ -37,7 +40,6 @@ public sealed class LandblockBuildFactory
"The retail terrain height table must contain at least 256 entries.",
nameof(heightTable));
_heightTable = (float[])heightTable.Clone();
_physicsDataCache = physicsDataCache ?? throw new ArgumentNullException(nameof(physicsDataCache));
_dumpSceneryZ = dumpSceneryZ;
}
@ -73,6 +75,7 @@ public sealed class LandblockBuildFactory
// lock during a CreateObject flood) AND lock-HOLD (the intrinsic build
// cost). Identical work in both branches; the probe branch only adds the
// stopwatch + log. No behavior change when ProbeTeleportEnabled is false.
LandblockBuild? build;
if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeTeleportEnabled)
{
var sw = System.Diagnostics.Stopwatch.StartNew();
@ -80,17 +83,28 @@ public sealed class LandblockBuildFactory
{
long waitedMs = sw.ElapsedMilliseconds;
sw.Restart();
var built = BuildLocked(request);
build = BuildLocked(request);
AcDream.Core.Physics.PhysicsDiagnostics.LogTeleport(
"BUILD", request.LandblockId,
$"waited={waitedMs}ms held={sw.ElapsedMilliseconds}ms kind={request.Kind}");
return built;
}
}
lock (_datLock)
else
{
return BuildLocked(request);
lock (_datLock)
build = BuildLocked(request);
}
if (build is null ||
request.Kind == LandblockStreamJobKind.LoadFar)
{
return build;
}
return build with
{
Collisions = BuildPreparedCollisionClosure(build.Landblock),
};
}
private AcDream.App.Streaming.LandblockBuild? BuildLocked(
@ -143,7 +157,6 @@ public sealed class LandblockBuildFactory
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(e.SourceGfxObjOrSetupId);
if (gfx is not null)
{
_physicsDataCache.CacheGfxObj(e.SourceGfxObjOrSetupId, gfx);
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
if (pb is not null) stabBounds.Add(System.Numerics.Matrix4x4.Identity, pb.Value);
meshRefs.Add(new AcDream.Core.World.MeshRef(
@ -156,13 +169,11 @@ public sealed class LandblockBuildFactory
var setup = _dats.Get<DatReaderWriter.DBObjs.Setup>(e.SourceGfxObjOrSetupId);
if (setup is not null)
{
_physicsDataCache.CacheSetup(e.SourceGfxObjOrSetupId, setup);
var flat = AcDream.Core.Meshing.SetupMesh.Flatten(setup);
foreach (var mr in flat)
{
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(mr.GfxObjId);
if (gfx is null) continue;
_physicsDataCache.CacheGfxObj(mr.GfxObjId, gfx);
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
if (pb is not null) stabBounds.Add(mr.PartTransform, pb.Value);
meshRefs.Add(mr);
@ -293,6 +304,103 @@ public sealed class LandblockBuildFactory
return new AcDream.Core.World.PhysicsDatBundle(lbInfo, envCells, environments, setups, gfxObjs);
}
/// <summary>
/// Reads the exact prepared collision closure after the serialized DAT
/// transaction completes. The immutable package is independently
/// thread-safe, so these reads never extend the shared DAT-reader lock.
/// Missing or corrupt prepared data fails the complete near-tier build;
/// production must not publish a graph-only partial generation.
/// </summary>
private AcDream.Core.World.LandblockCollisionBuild
BuildPreparedCollisionClosure(
AcDream.Core.World.LoadedLandblock landblock)
{
AcDream.Core.World.PhysicsDatBundle dats =
landblock.PhysicsDats ??
AcDream.Core.World.PhysicsDatBundle.Empty;
ImmutableArray<uint> gfxObjIds = [.. dats.GfxObjs.Keys.Order()];
ImmutableArray<uint> setupIds = [.. dats.Setups.Keys.Order()];
ImmutableArray<uint> envCellIds = [.. dats.EnvCells.Keys.Order()];
var gfxObjs =
ImmutableDictionary.CreateBuilder<
uint,
AcDream.Core.Physics.FlatGfxObjCollisionAsset>();
var setups =
ImmutableDictionary.CreateBuilder<
uint,
AcDream.Core.Physics.FlatSetupCollision>();
var cellStructures =
ImmutableDictionary.CreateBuilder<
uint,
AcDream.Core.Physics.FlatCellStructureCollisionAsset>();
var envCells =
ImmutableDictionary.CreateBuilder<
uint,
AcDream.Core.Physics.FlatEnvCellTopology>();
foreach (uint id in gfxObjIds)
{
gfxObjs.Add(
id,
RequirePrepared(
_preparedCollisions.ReadGfxObjCollision(id),
"GfxObj collision",
id));
}
foreach (uint id in setupIds)
{
setups.Add(
id,
RequirePrepared(
_preparedCollisions.ReadSetupCollision(id),
"Setup collision",
id));
}
foreach (uint id in envCellIds)
{
cellStructures.Add(
id,
RequirePrepared(
_preparedCollisions.ReadCellStructureCollision(id),
"CellStruct collision",
id));
envCells.Add(
id,
RequirePrepared(
_preparedCollisions.ReadEnvCellTopology(id),
"EnvCell topology",
id));
}
return new AcDream.Core.World.LandblockCollisionBuild(
gfxObjs.ToImmutable(),
setups.ToImmutable(),
cellStructures.ToImmutable(),
envCells.ToImmutable(),
gfxObjIds,
setupIds,
envCellIds);
}
private static T RequirePrepared<T>(
PreparedCollisionReadResult<T> result,
string kind,
uint id)
where T : class
{
if (result.Status == PreparedAssetReadStatus.Loaded &&
result.Data is not null)
{
return result.Data;
}
throw new InvalidDataException(
$"{kind} 0x{id:X8} is {result.Status}. " +
"The complete near-tier generation cannot be published.");
}
/// <summary>
/// Phase A.1 Task 8: generate scenery (trees, rocks, bushes) for a single
/// landblock on the worker thread. Pure CPU — no GL calls.
@ -363,7 +471,6 @@ public sealed class LandblockBuildFactory
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(spawn.ObjectId);
if (gfx is not null)
{
_physicsDataCache.CacheGfxObj(spawn.ObjectId, gfx);
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
if (pb is not null) sceneryBounds.Add(scaleMat, pb.Value);
meshRefs.Add(new AcDream.Core.World.MeshRef(spawn.ObjectId, scaleMat));
@ -374,13 +481,11 @@ public sealed class LandblockBuildFactory
var setup = _dats.Get<DatReaderWriter.DBObjs.Setup>(spawn.ObjectId);
if (setup is not null)
{
_physicsDataCache.CacheSetup(spawn.ObjectId, setup);
var flat = AcDream.Core.Meshing.SetupMesh.Flatten(setup);
foreach (var mr in flat)
{
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(mr.GfxObjId);
if (gfx is null) continue;
_physicsDataCache.CacheGfxObj(mr.GfxObjId, gfx);
// Compose: part's own transform, then the spawn's scale.
var partXf = mr.PartTransform * scaleMat;
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
@ -687,7 +792,6 @@ public sealed class LandblockBuildFactory
var gfx = _dats.Get<DatReaderWriter.DBObjs.GfxObj>(stab.Id);
if (gfx is not null)
{
_physicsDataCache.CacheGfxObj(stab.Id, gfx);
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
if (pb is not null) interiorBounds.Add(System.Numerics.Matrix4x4.Identity, pb.Value);
meshRefs.Add(new AcDream.Core.World.MeshRef(stab.Id, System.Numerics.Matrix4x4.Identity));
@ -702,7 +806,6 @@ public sealed class LandblockBuildFactory
var setup = _dats.Get<DatReaderWriter.DBObjs.Setup>(stab.Id);
if (setup is not null)
{
_physicsDataCache.CacheSetup(stab.Id, setup);
stabLightCount = setup.Lights.Count;
var flat = AcDream.Core.Meshing.SetupMesh.Flatten(setup);
if (dumpStab)
@ -727,7 +830,6 @@ public sealed class LandblockBuildFactory
Console.WriteLine($"[dump-entity] HYDRATE src=0x{stab.Id:X8} cell=0x{envCellId:X8} part gfx=0x{mr.GfxObjId:X8} GFXOBJ-NULL -> part dropped");
continue;
}
_physicsDataCache.CacheGfxObj(mr.GfxObjId, gfx);
var pb = AcDream.Core.Meshing.GfxObjBounds.Get(gfx);
if (pb is not null) interiorBounds.Add(mr.PartTransform, pb.Value);
meshRefs.Add(mr);