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:
parent
f7ff9f4eea
commit
d9446030e6
32 changed files with 2777 additions and 92 deletions
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue