feat(app): stage live entities before runtime placement
This commit is contained in:
parent
3f800a4aec
commit
74103f75b5
2 changed files with 187 additions and 3 deletions
|
|
@ -30,6 +30,18 @@ public enum LiveEntityProjectionKind
|
|||
Attached,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects whether App may publish a newly constructed sidecar immediately or
|
||||
/// must keep it non-spatial until the canonical Runtime SetPosition receipt is
|
||||
/// projected. The latter is the production CreateObject path; the former is a
|
||||
/// temporary compatibility seam for callers not yet cut over in Slice 4B2.
|
||||
/// </summary>
|
||||
internal enum LiveEntityMaterializationResidence
|
||||
{
|
||||
LegacyImmediate,
|
||||
AwaitRuntimePlacement,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logical-resource seam coordinated by <see cref="LiveEntityRuntime"/>.
|
||||
/// Spatial bucketing is deliberately absent: registering or removing meshes,
|
||||
|
|
@ -249,6 +261,8 @@ public sealed class LiveEntityRecord
|
|||
public IRuntimeProjectile? ProjectileRuntime => Canonical.Projectile;
|
||||
public ILiveEntityEffectProfile? EffectProfile { get; internal set; }
|
||||
public bool ResourcesRegistered { get; internal set; }
|
||||
internal LiveEntityMaterializationResidence? MaterializationResidence
|
||||
{ get; set; }
|
||||
public bool IsSpatiallyProjected { get; internal set; }
|
||||
public bool IsSpatiallyVisible { get; internal set; }
|
||||
internal ulong ProjectionMutationVersion { get; set; }
|
||||
|
|
@ -578,10 +592,20 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
Func<uint, WorldEntity> factory,
|
||||
LiveEntityProjectionKind projectionKind,
|
||||
Action<LiveEntityRecord>? initializeProjection,
|
||||
out LiveEntityRecord? record)
|
||||
out LiveEntityRecord? record,
|
||||
LiveEntityMaterializationResidence residence =
|
||||
LiveEntityMaterializationResidence.LegacyImmediate)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(expectedCanonical);
|
||||
ArgumentNullException.ThrowIfNull(factory);
|
||||
if (residence is LiveEntityMaterializationResidence
|
||||
.AwaitRuntimePlacement
|
||||
&& projectionKind is not LiveEntityProjectionKind.World)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Only a top-level world projection can await canonical Runtime placement.",
|
||||
nameof(projectionKind));
|
||||
}
|
||||
record = null;
|
||||
if (_isClearing
|
||||
|| _sessionClearPendingFinalization
|
||||
|
|
@ -605,6 +629,7 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
{
|
||||
record = _projections.AddMaterializing(expectedCanonical);
|
||||
createdSidecar = true;
|
||||
record.MaterializationResidence = residence;
|
||||
initializeProjection?.Invoke(record);
|
||||
}
|
||||
catch
|
||||
|
|
@ -618,6 +643,13 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
throw;
|
||||
}
|
||||
}
|
||||
else if (record.MaterializationResidence != residence)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Live entity 0x{serverGuid:X8}/{expectedCanonical.Incarnation} "
|
||||
+ $"cannot change its materialization residence from "
|
||||
+ $"{record.MaterializationResidence} to {residence}.");
|
||||
}
|
||||
|
||||
if (record.WorldEntity is null)
|
||||
{
|
||||
|
|
@ -653,6 +685,10 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
}
|
||||
|
||||
record.WorldEntity = entity;
|
||||
// Runtime-placement residence is sticky for this incarnation and
|
||||
// must suppress draw before arbitrary resource callbacks observe
|
||||
// or re-enter the sidecar.
|
||||
RefreshPresentation(record);
|
||||
_isRegisteringResources = true;
|
||||
try
|
||||
{
|
||||
|
|
@ -726,6 +762,18 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
}
|
||||
RefreshPresentation(record);
|
||||
WorldEntity materialized = record.WorldEntity!;
|
||||
if (residence is LiveEntityMaterializationResidence
|
||||
.AwaitRuntimePlacement)
|
||||
{
|
||||
// Resource construction is a logical App-side ownership edge, not
|
||||
// world residence. Runtime's immutable Place receipt is the only
|
||||
// path which may install the accepted frame and spatial bucket.
|
||||
record.IsSpatiallyProjected = false;
|
||||
record.IsSpatiallyVisible = false;
|
||||
RefreshSpatialPresentationIndexes(record);
|
||||
RefreshPresentation(record);
|
||||
return materialized;
|
||||
}
|
||||
if (!RebucketLiveEntity(serverGuid, fullCellId)
|
||||
|| !_projections.TryGet(expectedCanonical, out LiveEntityRecord? current)
|
||||
|| !ReferenceEquals(current, record)
|
||||
|
|
@ -746,6 +794,14 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
if (!_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)
|
||||
|| record.WorldEntity is not { } entity)
|
||||
return false;
|
||||
if (record.MaterializationResidence is
|
||||
LiveEntityMaterializationResidence.AwaitRuntimePlacement)
|
||||
{
|
||||
// The private Runtime Place path below performs a presentation-
|
||||
// only bucket update. This legacy API also commits canonical
|
||||
// Runtime residence and cannot touch a cut-over incarnation.
|
||||
return false;
|
||||
}
|
||||
|
||||
RuntimeEntityKey key = RequireProjectionKey(record);
|
||||
bool wasProjected = record.IsSpatiallyProjected;
|
||||
|
|
@ -3031,8 +3087,11 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
|||
return;
|
||||
|
||||
PhysicsStateFlags state = record.FinalPhysicsState;
|
||||
entity.IsDrawVisible =
|
||||
(state & (PhysicsStateFlags.NoDraw | PhysicsStateFlags.Hidden)) == 0;
|
||||
bool residenceVisible = record.MaterializationResidence is not
|
||||
LiveEntityMaterializationResidence.AwaitRuntimePlacement
|
||||
|| record.IsSpatiallyProjected;
|
||||
entity.IsDrawVisible = residenceVisible
|
||||
&& (state & (PhysicsStateFlags.NoDraw | PhysicsStateFlags.Hidden)) == 0;
|
||||
|
||||
bool interactionVisible = record.IsSpatiallyVisible
|
||||
&& record.ProjectionKind is LiveEntityProjectionKind.World
|
||||
|
|
|
|||
|
|
@ -202,6 +202,131 @@ public sealed class LiveEntityRuntimeTests
|
|||
Assert.Equal(new[] { false, true }, visibilityEdges);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RuntimePlacementPendingMaterialization_OwnsResourcesWithoutPublishingResidence()
|
||||
{
|
||||
const uint guid = 0x70000081u;
|
||||
const uint cell = 0x01010001u;
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
||||
var resources = new CallbackResources();
|
||||
LiveEntityRuntime runtime = LiveEntityRuntimeFixture.Create(
|
||||
spatial,
|
||||
resources);
|
||||
LiveEntityRegistrationResult registration = runtime.RegisterLiveEntity(
|
||||
Spawn(guid, instance: 1, positionSequence: 1, cell));
|
||||
RuntimeEntityRecord canonical = Assert.IsType<RuntimeEntityRecord>(
|
||||
registration.Canonical);
|
||||
uint acceptedCell = canonical.FullCellId;
|
||||
ulong spatialAuthority = canonical.SpatialAuthorityVersion;
|
||||
ulong placementCommit = canonical.PlacementCommitVersion;
|
||||
ulong clockEpoch = canonical.ObjectClockEpoch;
|
||||
bool clockActive = canonical.ObjectClock.IsActive;
|
||||
RuntimePhysicsOwnershipSnapshot physicsOwnership =
|
||||
runtime.Physics.CaptureOwnership();
|
||||
var visibilityEdges = new List<bool>();
|
||||
runtime.ProjectionVisibilityChanged += (_, visible) =>
|
||||
visibilityEdges.Add(visible);
|
||||
resources.OnRegister = entity =>
|
||||
{
|
||||
Assert.False(entity.IsDrawVisible);
|
||||
Assert.True(runtime.TryGetRecord(guid, out LiveEntityRecord current));
|
||||
Assert.False(current.IsSpatiallyProjected);
|
||||
Assert.False(current.IsSpatiallyVisible);
|
||||
Assert.False(runtime.RebucketLiveEntity(guid, cell));
|
||||
};
|
||||
|
||||
WorldEntity entity = Assert.IsType<WorldEntity>(
|
||||
runtime.MaterializeLiveEntity(
|
||||
canonical,
|
||||
cell,
|
||||
id => Entity(id, guid),
|
||||
LiveEntityProjectionKind.World,
|
||||
initializeProjection: null,
|
||||
out LiveEntityRecord? record,
|
||||
LiveEntityMaterializationResidence.AwaitRuntimePlacement));
|
||||
|
||||
Assert.NotNull(record);
|
||||
Assert.Same(entity, record!.WorldEntity);
|
||||
Assert.True(record.ResourcesRegistered);
|
||||
Assert.False(record.IsSpatiallyProjected);
|
||||
Assert.False(record.IsSpatiallyVisible);
|
||||
Assert.False(entity.IsDrawVisible);
|
||||
Assert.Equal(acceptedCell, canonical.FullCellId);
|
||||
Assert.Equal(spatialAuthority, canonical.SpatialAuthorityVersion);
|
||||
Assert.Equal(placementCommit, canonical.PlacementCommitVersion);
|
||||
Assert.Equal(clockEpoch, canonical.ObjectClockEpoch);
|
||||
Assert.Equal(clockActive, canonical.ObjectClock.IsActive);
|
||||
Assert.Equal(physicsOwnership, runtime.Physics.CaptureOwnership());
|
||||
Assert.Equal(0, runtime.SpatialRootObjectCount);
|
||||
Assert.Equal(1, resources.RegisterCount);
|
||||
Assert.Empty(spatial.Entities);
|
||||
Assert.Empty(runtime.VisibleRecords);
|
||||
Assert.Empty(visibilityEdges);
|
||||
|
||||
WorldEntity retained = Assert.IsType<WorldEntity>(
|
||||
runtime.MaterializeLiveEntity(
|
||||
canonical,
|
||||
cell,
|
||||
id => Entity(id, guid),
|
||||
LiveEntityProjectionKind.World,
|
||||
initializeProjection: null,
|
||||
out LiveEntityRecord? retainedRecord,
|
||||
LiveEntityMaterializationResidence.AwaitRuntimePlacement));
|
||||
Assert.Same(entity, retained);
|
||||
Assert.Same(record, retainedRecord);
|
||||
Assert.Equal(1, resources.RegisterCount);
|
||||
Assert.False(runtime.RebucketLiveEntity(guid, cell));
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
runtime.MaterializeLiveEntity(
|
||||
canonical,
|
||||
cell,
|
||||
id => Entity(id, guid),
|
||||
LiveEntityProjectionKind.World,
|
||||
initializeProjection: null,
|
||||
out _,
|
||||
LiveEntityMaterializationResidence.LegacyImmediate));
|
||||
Assert.Empty(spatial.Entities);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LegacyMaterialization_CannotSwitchToRuntimePlacementResidence()
|
||||
{
|
||||
const uint guid = 0x70000082u;
|
||||
const uint cell = 0x01010001u;
|
||||
var spatial = new GpuWorldState();
|
||||
spatial.AddLandblock(EmptyLandblock(0x0101FFFFu));
|
||||
var resources = new RecordingResources();
|
||||
var runtime = LiveEntityRuntimeFixture.Create(spatial, resources);
|
||||
RuntimeEntityRecord canonical = Assert.IsType<RuntimeEntityRecord>(
|
||||
runtime.RegisterLiveEntity(
|
||||
Spawn(guid, instance: 1, positionSequence: 1, cell)).Canonical);
|
||||
WorldEntity entity = Assert.IsType<WorldEntity>(
|
||||
runtime.MaterializeLiveEntity(
|
||||
canonical,
|
||||
cell,
|
||||
id => Entity(id, guid),
|
||||
LiveEntityProjectionKind.World,
|
||||
initializeProjection: null,
|
||||
out LiveEntityRecord? record));
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
runtime.MaterializeLiveEntity(
|
||||
canonical,
|
||||
cell,
|
||||
id => Entity(id, guid),
|
||||
LiveEntityProjectionKind.World,
|
||||
initializeProjection: null,
|
||||
out _,
|
||||
LiveEntityMaterializationResidence.AwaitRuntimePlacement));
|
||||
|
||||
Assert.True(record!.IsSpatiallyProjected);
|
||||
Assert.True(record.IsSpatiallyVisible);
|
||||
Assert.True(entity.IsDrawVisible);
|
||||
Assert.Single(spatial.Entities);
|
||||
Assert.Equal(1, resources.RegisterCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaticRootCommit_HiddenObjectUpdatesPoseWithoutRestoringShadow()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue