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
|
|
@ -30,6 +30,7 @@ internal sealed record ContentEffectsAudioResult(
|
|||
MagicCatalog MagicCatalog,
|
||||
IAnimationLoader AnimationLoader,
|
||||
LiveEntityCollisionBuilder CollisionBuilder,
|
||||
LiveCollisionAssetPublisher CollisionAssets,
|
||||
EmitterDescRegistry EmitterRegistry,
|
||||
ParticleSystem ParticleSystem,
|
||||
ParticleHookSink ParticleSink,
|
||||
|
|
@ -95,6 +96,9 @@ internal interface IContentEffectsAudioCompositionFactory
|
|||
IDatReaderWriter dats,
|
||||
IAnimationLoader animationLoader,
|
||||
bool dumpMotionEnabled);
|
||||
LiveCollisionAssetPublisher CreateLiveCollisionAssetPublisher(
|
||||
PhysicsDataCache physicsData,
|
||||
IPreparedAssetSource preparedAssets);
|
||||
EmitterDescRegistry CreateEmitterRegistry(IDatReaderWriter dats);
|
||||
ParticleSystem CreateParticleSystem(EmitterDescRegistry emitters);
|
||||
ParticleHookSink CreateParticleSink(
|
||||
|
|
@ -165,6 +169,15 @@ internal sealed class RetailContentEffectsAudioCompositionFactory
|
|||
animationLoader,
|
||||
dumpMotionEnabled));
|
||||
|
||||
public LiveCollisionAssetPublisher CreateLiveCollisionAssetPublisher(
|
||||
PhysicsDataCache physicsData,
|
||||
IPreparedAssetSource preparedAssets) =>
|
||||
new(
|
||||
physicsData,
|
||||
preparedAssets as IPreparedCollisionSource
|
||||
?? throw new NotSupportedException(
|
||||
"Production prepared assets must expose collision data."));
|
||||
|
||||
public EmitterDescRegistry CreateEmitterRegistry(IDatReaderWriter dats) =>
|
||||
new(dats);
|
||||
|
||||
|
|
@ -332,6 +345,10 @@ internal sealed class ContentEffectsAudioCompositionPhase :
|
|||
_dependencies.DumpMotionEnabled);
|
||||
_publication.PublishLiveEntityCollisionBuilder(collision);
|
||||
Fault(ContentEffectsAudioCompositionPoint.CollisionBuilderPublished);
|
||||
LiveCollisionAssetPublisher collisionAssets =
|
||||
_factory.CreateLiveCollisionAssetPublisher(
|
||||
_dependencies.PhysicsDataCache,
|
||||
preparedAssets);
|
||||
|
||||
EmitterDescRegistry emitters = _factory.CreateEmitterRegistry(dats);
|
||||
_publication.PublishEmitterRegistry(emitters);
|
||||
|
|
@ -403,6 +420,7 @@ internal sealed class ContentEffectsAudioCompositionPhase :
|
|||
magic,
|
||||
animations,
|
||||
collision,
|
||||
collisionAssets,
|
||||
emitters,
|
||||
particles,
|
||||
particleSink,
|
||||
|
|
|
|||
|
|
@ -441,6 +441,9 @@ internal sealed class FrameRootCompositionPhase
|
|||
d.FrameProfiler,
|
||||
content.Dats,
|
||||
foundation.Residency,
|
||||
d.PhysicsEngine.DataCache
|
||||
?? throw new InvalidOperationException(
|
||||
"Lifecycle automation requires the canonical physics cache."),
|
||||
currentRenderSceneOracle,
|
||||
renderSceneShadowComparison,
|
||||
renderFrameProduct);
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ using AcDream.App.Settings;
|
|||
using AcDream.App.Streaming;
|
||||
using AcDream.App.Update;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Content;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Chat;
|
||||
|
|
@ -252,11 +253,16 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
$"(window={2 * farRadius + 1}x{2 * farRadius + 1})");
|
||||
Fault(SessionPlayerCompositionPoint.StreamingRadiiResolved);
|
||||
|
||||
IPreparedCollisionSource preparedCollisions =
|
||||
content.PreparedAssets as IPreparedCollisionSource ??
|
||||
throw new NotSupportedException(
|
||||
"Production prepared assets must expose the matching " +
|
||||
"prepared-collision catalog.");
|
||||
var landblockBuildFactory = new LandblockBuildFactory(
|
||||
content.Dats,
|
||||
preparedCollisions,
|
||||
d.DatLock,
|
||||
world.TerrainBuild.HeightTable,
|
||||
d.PhysicsDataCache,
|
||||
d.Options.DumpSceneryZ);
|
||||
var streamerLease = scope.Acquire(
|
||||
"landblock streamer",
|
||||
|
|
@ -399,7 +405,7 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
d.Options,
|
||||
content.Dats,
|
||||
live.LiveEntities,
|
||||
d.PhysicsDataCache,
|
||||
content.CollisionAssets,
|
||||
content.AnimationLoader,
|
||||
live.EntitySpawnAdapter,
|
||||
world.Foundation.TextureCache,
|
||||
|
|
@ -689,7 +695,7 @@ internal sealed class SessionPlayerCompositionPhase
|
|||
d.MotionBindings,
|
||||
content.Dats,
|
||||
d.DatLock,
|
||||
d.PhysicsDataCache,
|
||||
content.CollisionAssets,
|
||||
d.AnimatedEntities,
|
||||
localPlayerAnimation,
|
||||
localPlayerShadow,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using AcDream.App.Rendering.Residency;
|
|||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.UI.Testing;
|
||||
using AcDream.Core.Physics;
|
||||
|
||||
namespace AcDream.App.Diagnostics;
|
||||
|
||||
|
|
@ -81,6 +82,14 @@ internal sealed record WorldLifecycleResourceSnapshot(
|
|||
long DatObjectCacheHits,
|
||||
long DatObjectCacheMisses,
|
||||
long DatObjectCacheEvictions,
|
||||
int PhysicsGraphGfxObjs,
|
||||
int PhysicsGraphSetups,
|
||||
int PhysicsGraphCells,
|
||||
int PhysicsFlatGfxObjs,
|
||||
int PhysicsFlatSetups,
|
||||
int PhysicsFlatCells,
|
||||
int PhysicsFlatEnvCells,
|
||||
CollisionShadowStats CollisionShadow,
|
||||
StreamingWorkDiagnostics StreamingWork,
|
||||
ResidencySnapshot Residency,
|
||||
double Fps,
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ using AcDream.App.Rendering.Residency;
|
|||
using AcDream.App.Streaming;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Content;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Vfx;
|
||||
using DatReaderWriter.Lib.IO;
|
||||
|
||||
|
|
@ -39,6 +40,7 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
private readonly FrameProfiler _frameProfiler;
|
||||
private readonly IDatReaderWriter _dats;
|
||||
private readonly ResidencyManager _residency;
|
||||
private readonly PhysicsDataCache _physics;
|
||||
private readonly ICurrentRenderSceneOracleSnapshotSource?
|
||||
_renderSceneOracle;
|
||||
private readonly IRenderSceneShadowSnapshotSource?
|
||||
|
|
@ -63,6 +65,7 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
FrameProfiler frameProfiler,
|
||||
IDatReaderWriter dats,
|
||||
ResidencyManager residency,
|
||||
PhysicsDataCache physics,
|
||||
ICurrentRenderSceneOracleSnapshotSource? renderSceneOracle = null,
|
||||
IRenderSceneShadowSnapshotSource? renderSceneShadow = null,
|
||||
IRenderFrameProductSnapshotSource? renderFrameProduct = null)
|
||||
|
|
@ -92,6 +95,7 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
||||
_residency = residency
|
||||
?? throw new ArgumentNullException(nameof(residency));
|
||||
_physics = physics ?? throw new ArgumentNullException(nameof(physics));
|
||||
_renderSceneOracle = renderSceneOracle;
|
||||
_renderSceneShadow = renderSceneShadow;
|
||||
_renderFrameProduct = renderFrameProduct;
|
||||
|
|
@ -184,6 +188,14 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
DatObjectCacheHits: datObjectCacheStats.Hits,
|
||||
DatObjectCacheMisses: datObjectCacheStats.Misses,
|
||||
DatObjectCacheEvictions: datObjectCacheStats.Evictions,
|
||||
PhysicsGraphGfxObjs: _physics.GfxObjCount,
|
||||
PhysicsGraphSetups: _physics.SetupCount,
|
||||
PhysicsGraphCells: _physics.CellStructCount,
|
||||
PhysicsFlatGfxObjs: _physics.FlatGfxObjCount,
|
||||
PhysicsFlatSetups: _physics.FlatSetupCount,
|
||||
PhysicsFlatCells: _physics.FlatCellStructCount,
|
||||
PhysicsFlatEnvCells: _physics.FlatEnvCellCount,
|
||||
CollisionShadow: _physics.CollisionShadowStats,
|
||||
StreamingWork: _streaming.WorkDiagnostics,
|
||||
Residency: residency,
|
||||
Fps: render.Fps,
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ internal sealed class PlayerModeController :
|
|||
private readonly ILiveEntityMotionRuntimeBindings _motionBindings;
|
||||
private readonly IDatReaderWriter _dats;
|
||||
private readonly object _datLock;
|
||||
private readonly PhysicsDataCache _physicsDataCache;
|
||||
private readonly LiveCollisionAssetPublisher _collisionAssets;
|
||||
private readonly LiveEntityAnimationRuntimeView<LiveEntityAnimationState> _animations;
|
||||
private readonly LocalPlayerAnimationController _animation;
|
||||
private readonly LocalPlayerShadowSynchronizer _shadow;
|
||||
|
|
@ -59,7 +59,7 @@ internal sealed class PlayerModeController :
|
|||
ILiveEntityMotionRuntimeBindings motionBindings,
|
||||
IDatReaderWriter dats,
|
||||
object datLock,
|
||||
PhysicsDataCache physicsDataCache,
|
||||
LiveCollisionAssetPublisher collisionAssets,
|
||||
LiveEntityAnimationRuntimeView<LiveEntityAnimationState> animations,
|
||||
LocalPlayerAnimationController animation,
|
||||
LocalPlayerShadowSynchronizer shadow,
|
||||
|
|
@ -82,7 +82,8 @@ internal sealed class PlayerModeController :
|
|||
_motionBindings = motionBindings ?? throw new ArgumentNullException(nameof(motionBindings));
|
||||
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
||||
_datLock = datLock ?? throw new ArgumentNullException(nameof(datLock));
|
||||
_physicsDataCache = physicsDataCache ?? throw new ArgumentNullException(nameof(physicsDataCache));
|
||||
_collisionAssets = collisionAssets ??
|
||||
throw new ArgumentNullException(nameof(collisionAssets));
|
||||
_animations = animations ?? throw new ArgumentNullException(nameof(animations));
|
||||
_animation = animation ?? throw new ArgumentNullException(nameof(animation));
|
||||
_shadow = shadow ?? throw new ArgumentNullException(nameof(shadow));
|
||||
|
|
@ -536,7 +537,9 @@ internal sealed class PlayerModeController :
|
|||
setup = _dats.Get<DatReaderWriter.DBObjs.Setup>(
|
||||
playerEntity.SourceGfxObjOrSetupId);
|
||||
if (setup is not null)
|
||||
_physicsDataCache.CacheSetup(playerEntity.SourceGfxObjOrSetupId, setup);
|
||||
_collisionAssets.CacheSetup(
|
||||
playerEntity.SourceGfxObjOrSetupId,
|
||||
setup);
|
||||
controller.StepUpHeight = setup is { StepUpHeight: > 0f }
|
||||
? setup.StepUpHeight
|
||||
: 0.4f;
|
||||
|
|
|
|||
68
src/AcDream.App/Physics/LiveCollisionAssetPublisher.cs
Normal file
68
src/AcDream.App/Physics/LiveCollisionAssetPublisher.cs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
using AcDream.Content;
|
||||
using AcDream.Core.Physics;
|
||||
using DatReaderWriter.DBObjs;
|
||||
|
||||
namespace AcDream.App.Physics;
|
||||
|
||||
/// <summary>
|
||||
/// Publishes the graph and prepared collision views for DAT objects discovered
|
||||
/// through the live-object path. Streamed statics carry their prepared closure
|
||||
/// on <c>LandblockBuild</c>; live CreateObject/appearance data arrives outside
|
||||
/// that transaction and therefore uses this matching strict publisher.
|
||||
/// </summary>
|
||||
internal sealed class LiveCollisionAssetPublisher
|
||||
{
|
||||
private readonly PhysicsDataCache _cache;
|
||||
private readonly IPreparedCollisionSource _source;
|
||||
|
||||
public LiveCollisionAssetPublisher(
|
||||
PhysicsDataCache cache,
|
||||
IPreparedCollisionSource source)
|
||||
{
|
||||
_cache = cache ?? throw new ArgumentNullException(nameof(cache));
|
||||
_source = source ?? throw new ArgumentNullException(nameof(source));
|
||||
}
|
||||
|
||||
public void CacheGfxObj(uint id, GfxObj gfxObj)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(gfxObj);
|
||||
FlatGfxObjCollisionAsset? prepared =
|
||||
_cache.GetFlatGfxObj(id) is not null
|
||||
? null
|
||||
: Require(
|
||||
_source.ReadGfxObjCollision(id),
|
||||
"GfxObj collision",
|
||||
id);
|
||||
_cache.CacheGfxObj(id, gfxObj, prepared);
|
||||
}
|
||||
|
||||
public void CacheSetup(uint id, Setup setup)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(setup);
|
||||
FlatSetupCollision? prepared =
|
||||
_cache.GetFlatSetup(id) is not null
|
||||
? null
|
||||
: Require(
|
||||
_source.ReadSetupCollision(id),
|
||||
"Setup collision",
|
||||
id);
|
||||
_cache.CacheSetup(id, setup, prepared);
|
||||
}
|
||||
|
||||
private static T Require<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}. " +
|
||||
"Live collision cannot publish only one representation.");
|
||||
}
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
|||
private readonly RuntimeOptions _options;
|
||||
private readonly IDatReaderWriter _dats;
|
||||
private readonly LiveEntityRuntime _runtime;
|
||||
private readonly PhysicsDataCache _physicsData;
|
||||
private readonly LiveCollisionAssetPublisher _collisionAssets;
|
||||
private readonly IAnimationLoader _animationLoader;
|
||||
private readonly EntitySpawnAdapter _spawnAdapter;
|
||||
private readonly TextureCache _textures;
|
||||
|
|
@ -62,7 +62,7 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
|||
RuntimeOptions options,
|
||||
IDatReaderWriter dats,
|
||||
LiveEntityRuntime runtime,
|
||||
PhysicsDataCache physicsData,
|
||||
LiveCollisionAssetPublisher collisionAssets,
|
||||
IAnimationLoader animationLoader,
|
||||
EntitySpawnAdapter spawnAdapter,
|
||||
TextureCache textures,
|
||||
|
|
@ -82,7 +82,8 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
|||
_options = options ?? throw new ArgumentNullException(nameof(options));
|
||||
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
||||
_runtime = runtime ?? throw new ArgumentNullException(nameof(runtime));
|
||||
_physicsData = physicsData ?? throw new ArgumentNullException(nameof(physicsData));
|
||||
_collisionAssets = collisionAssets ??
|
||||
throw new ArgumentNullException(nameof(collisionAssets));
|
||||
_animationLoader = animationLoader ?? throw new ArgumentNullException(nameof(animationLoader));
|
||||
_spawnAdapter = spawnAdapter ?? throw new ArgumentNullException(nameof(spawnAdapter));
|
||||
_textures = textures ?? throw new ArgumentNullException(nameof(textures));
|
||||
|
|
@ -172,7 +173,7 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
|||
|
||||
Setup? setup = _dats.Get<Setup>(canonicalSpawn.SetupTableId.Value);
|
||||
if (setup is not null)
|
||||
_physicsData.CacheSetup(canonicalSpawn.SetupTableId.Value, setup);
|
||||
_collisionAssets.CacheSetup(canonicalSpawn.SetupTableId.Value, setup);
|
||||
if (setup is null)
|
||||
{
|
||||
_missingSetup++;
|
||||
|
|
@ -282,7 +283,7 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
|||
continue;
|
||||
}
|
||||
|
||||
_physicsData.CacheGfxObj(part.GfxObjId, gfx);
|
||||
_collisionAssets.CacheGfxObj(part.GfxObjId, gfx);
|
||||
if (dumpClothing)
|
||||
{
|
||||
var subMeshes = GfxObjMesh.Build(gfx, _dats);
|
||||
|
|
@ -430,7 +431,7 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
|||
}
|
||||
|
||||
if (slotZeroGfx is not null)
|
||||
_physicsData.CacheGfxObj(slotZeroId, slotZeroGfx);
|
||||
_collisionAssets.CacheGfxObj(slotZeroId, slotZeroGfx);
|
||||
return slotZeroId;
|
||||
}
|
||||
|
||||
|
|
@ -512,7 +513,7 @@ internal sealed class DatLiveEntityProjectionMaterializer
|
|||
}
|
||||
continue;
|
||||
}
|
||||
_physicsData.CacheGfxObj(parts[partIndex].GfxObjId, gfx);
|
||||
_collisionAssets.CacheGfxObj(parts[partIndex].GfxObjId, gfx);
|
||||
|
||||
Dictionary<uint, uint>? resolved = null;
|
||||
foreach (var surfaceQid in gfx.Surfaces)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ namespace AcDream.App.Streaming;
|
|||
public sealed record LandblockBuild(
|
||||
LoadedLandblock Landblock,
|
||||
EnvCellLandblockBuild? EnvCells = null,
|
||||
LandblockBuildOrigin Origin = default)
|
||||
LandblockBuildOrigin Origin = default,
|
||||
LandblockCollisionBuild? Collisions = null)
|
||||
{
|
||||
public uint LandblockId => Landblock.LandblockId;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -46,6 +46,8 @@ public sealed class LandblockPhysicsPublication
|
|||
internal int BuildingCursor { get; set; }
|
||||
internal bool BaseCommitted { get; set; }
|
||||
internal int GfxCursor { get; set; }
|
||||
internal uint[] SetupObjectIds { get; set; } = Array.Empty<uint>();
|
||||
internal int SetupCursor { get; set; }
|
||||
internal int PriorStaticCursor { get; set; }
|
||||
internal int StaticCursor { get; set; }
|
||||
internal int BspOwnerCount { get; set; }
|
||||
|
|
@ -199,7 +201,7 @@ public sealed class LandblockPhysicsPublisher
|
|||
build.Landblock.PhysicsDats ?? PhysicsDatBundle.Empty;
|
||||
BuildingInfo[] buildings = datBundle.Info?.Buildings.ToArray()
|
||||
?? Array.Empty<BuildingInfo>();
|
||||
return new LandblockPhysicsPublication(
|
||||
var publication = new LandblockPhysicsPublication(
|
||||
_receiptOwner,
|
||||
build,
|
||||
origin,
|
||||
|
|
@ -207,6 +209,10 @@ public sealed class LandblockPhysicsPublisher
|
|||
buildings,
|
||||
_physicsEngine.ShadowObjects.CaptureStaticOwnersForLandblock(
|
||||
build.Landblock.LandblockId));
|
||||
publication.SetupObjectIds = build.Collisions is { } collisions
|
||||
? [.. collisions.SetupIds]
|
||||
: datBundle.Setups.Keys.Order().ToArray();
|
||||
return publication;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -223,6 +229,14 @@ public sealed class LandblockPhysicsPublisher
|
|||
|
||||
IReadOnlyList<WorldEntity> entities =
|
||||
publication.Build.Landblock.Entities;
|
||||
if (publication.Build.Collisions is { } collisions)
|
||||
{
|
||||
publication.GfxObjectIds = [.. collisions.GfxObjIds];
|
||||
publication.PreparationCursor = entities.Count;
|
||||
publication.PreparationCommitted = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (publication.PreparationCursor < entities.Count)
|
||||
{
|
||||
WorldEntity entity = entities[publication.PreparationCursor];
|
||||
|
|
@ -389,10 +403,33 @@ public sealed class LandblockPhysicsPublisher
|
|||
long cacheStarted = Stopwatch.GetTimestamp();
|
||||
uint gfxObjectId = publication.GfxObjectIds[publication.GfxCursor];
|
||||
if (datBundle.GfxObjs.TryGetValue(gfxObjectId, out var gfx))
|
||||
_physicsDataCache.CacheGfxObj(gfxObjectId, gfx);
|
||||
{
|
||||
FlatGfxObjCollisionAsset? prepared = null;
|
||||
publication.Build.Collisions?.GfxObjs.TryGetValue(
|
||||
gfxObjectId,
|
||||
out prepared);
|
||||
_physicsDataCache.CacheGfxObj(
|
||||
gfxObjectId,
|
||||
gfx,
|
||||
prepared);
|
||||
}
|
||||
publication.GfxCursor++;
|
||||
_gfxCacheTicks += Stopwatch.GetTimestamp() - cacheStarted;
|
||||
}
|
||||
else if (publication.SetupCursor < publication.SetupObjectIds.Length)
|
||||
{
|
||||
uint setupId =
|
||||
publication.SetupObjectIds[publication.SetupCursor];
|
||||
if (datBundle.Setups.TryGetValue(setupId, out var setup))
|
||||
{
|
||||
FlatSetupCollision? prepared = null;
|
||||
publication.Build.Collisions?.Setups.TryGetValue(
|
||||
setupId,
|
||||
out prepared);
|
||||
_physicsDataCache.CacheSetup(setupId, setup, prepared);
|
||||
}
|
||||
publication.SetupCursor++;
|
||||
}
|
||||
else if (publication.PriorStaticCursor
|
||||
< publication.PriorStaticOwnerIds.Length)
|
||||
{
|
||||
|
|
@ -486,11 +523,21 @@ public sealed class LandblockPhysicsPublisher
|
|||
Matrix4x4.CreateFromQuaternion(rotation)
|
||||
* Matrix4x4.CreateTranslation(cellOriginWorld);
|
||||
|
||||
FlatCellStructureCollisionAsset? preparedStructure = null;
|
||||
FlatEnvCellTopology? preparedTopology = null;
|
||||
publication.Build.Collisions?.CellStructures.TryGetValue(
|
||||
envCellId,
|
||||
out preparedStructure);
|
||||
publication.Build.Collisions?.EnvCells.TryGetValue(
|
||||
envCellId,
|
||||
out preparedTopology);
|
||||
_physicsDataCache.CacheCellStruct(
|
||||
envCellId,
|
||||
envCell,
|
||||
cellStruct,
|
||||
physicsCellTransform);
|
||||
physicsCellTransform,
|
||||
preparedStructure,
|
||||
preparedTopology);
|
||||
|
||||
var worldVertices = new Dictionary<ushort, Vector3>(
|
||||
cellStruct.VertexArray.Vertices.Count);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue