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,
|
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>
|
/// <summary>
|
||||||
/// Logical-resource seam coordinated by <see cref="LiveEntityRuntime"/>.
|
/// Logical-resource seam coordinated by <see cref="LiveEntityRuntime"/>.
|
||||||
/// Spatial bucketing is deliberately absent: registering or removing meshes,
|
/// Spatial bucketing is deliberately absent: registering or removing meshes,
|
||||||
|
|
@ -249,6 +261,8 @@ public sealed class LiveEntityRecord
|
||||||
public IRuntimeProjectile? ProjectileRuntime => Canonical.Projectile;
|
public IRuntimeProjectile? ProjectileRuntime => Canonical.Projectile;
|
||||||
public ILiveEntityEffectProfile? EffectProfile { get; internal set; }
|
public ILiveEntityEffectProfile? EffectProfile { get; internal set; }
|
||||||
public bool ResourcesRegistered { get; internal set; }
|
public bool ResourcesRegistered { get; internal set; }
|
||||||
|
internal LiveEntityMaterializationResidence? MaterializationResidence
|
||||||
|
{ get; set; }
|
||||||
public bool IsSpatiallyProjected { get; internal set; }
|
public bool IsSpatiallyProjected { get; internal set; }
|
||||||
public bool IsSpatiallyVisible { get; internal set; }
|
public bool IsSpatiallyVisible { get; internal set; }
|
||||||
internal ulong ProjectionMutationVersion { get; set; }
|
internal ulong ProjectionMutationVersion { get; set; }
|
||||||
|
|
@ -578,10 +592,20 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
||||||
Func<uint, WorldEntity> factory,
|
Func<uint, WorldEntity> factory,
|
||||||
LiveEntityProjectionKind projectionKind,
|
LiveEntityProjectionKind projectionKind,
|
||||||
Action<LiveEntityRecord>? initializeProjection,
|
Action<LiveEntityRecord>? initializeProjection,
|
||||||
out LiveEntityRecord? record)
|
out LiveEntityRecord? record,
|
||||||
|
LiveEntityMaterializationResidence residence =
|
||||||
|
LiveEntityMaterializationResidence.LegacyImmediate)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(expectedCanonical);
|
ArgumentNullException.ThrowIfNull(expectedCanonical);
|
||||||
ArgumentNullException.ThrowIfNull(factory);
|
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;
|
record = null;
|
||||||
if (_isClearing
|
if (_isClearing
|
||||||
|| _sessionClearPendingFinalization
|
|| _sessionClearPendingFinalization
|
||||||
|
|
@ -605,6 +629,7 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
||||||
{
|
{
|
||||||
record = _projections.AddMaterializing(expectedCanonical);
|
record = _projections.AddMaterializing(expectedCanonical);
|
||||||
createdSidecar = true;
|
createdSidecar = true;
|
||||||
|
record.MaterializationResidence = residence;
|
||||||
initializeProjection?.Invoke(record);
|
initializeProjection?.Invoke(record);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
|
@ -618,6 +643,13 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
||||||
throw;
|
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)
|
if (record.WorldEntity is null)
|
||||||
{
|
{
|
||||||
|
|
@ -653,6 +685,10 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
||||||
}
|
}
|
||||||
|
|
||||||
record.WorldEntity = entity;
|
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;
|
_isRegisteringResources = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|
@ -726,6 +762,18 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
||||||
}
|
}
|
||||||
RefreshPresentation(record);
|
RefreshPresentation(record);
|
||||||
WorldEntity materialized = record.WorldEntity!;
|
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)
|
if (!RebucketLiveEntity(serverGuid, fullCellId)
|
||||||
|| !_projections.TryGet(expectedCanonical, out LiveEntityRecord? current)
|
|| !_projections.TryGet(expectedCanonical, out LiveEntityRecord? current)
|
||||||
|| !ReferenceEquals(current, record)
|
|| !ReferenceEquals(current, record)
|
||||||
|
|
@ -746,6 +794,14 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
||||||
if (!_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)
|
if (!_projections.TryGetCurrent(serverGuid, out LiveEntityRecord? record)
|
||||||
|| record.WorldEntity is not { } entity)
|
|| record.WorldEntity is not { } entity)
|
||||||
return false;
|
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);
|
RuntimeEntityKey key = RequireProjectionKey(record);
|
||||||
bool wasProjected = record.IsSpatiallyProjected;
|
bool wasProjected = record.IsSpatiallyProjected;
|
||||||
|
|
@ -3031,8 +3087,11 @@ public sealed class LiveEntityRuntime : ILiveEntityRadarSource
|
||||||
return;
|
return;
|
||||||
|
|
||||||
PhysicsStateFlags state = record.FinalPhysicsState;
|
PhysicsStateFlags state = record.FinalPhysicsState;
|
||||||
entity.IsDrawVisible =
|
bool residenceVisible = record.MaterializationResidence is not
|
||||||
(state & (PhysicsStateFlags.NoDraw | PhysicsStateFlags.Hidden)) == 0;
|
LiveEntityMaterializationResidence.AwaitRuntimePlacement
|
||||||
|
|| record.IsSpatiallyProjected;
|
||||||
|
entity.IsDrawVisible = residenceVisible
|
||||||
|
&& (state & (PhysicsStateFlags.NoDraw | PhysicsStateFlags.Hidden)) == 0;
|
||||||
|
|
||||||
bool interactionVisible = record.IsSpatiallyVisible
|
bool interactionVisible = record.IsSpatiallyVisible
|
||||||
&& record.ProjectionKind is LiveEntityProjectionKind.World
|
&& record.ProjectionKind is LiveEntityProjectionKind.World
|
||||||
|
|
|
||||||
|
|
@ -202,6 +202,131 @@ public sealed class LiveEntityRuntimeTests
|
||||||
Assert.Equal(new[] { false, true }, visibilityEdges);
|
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]
|
[Fact]
|
||||||
public void StaticRootCommit_HiddenObjectUpdatesPoseWithoutRestoringShadow()
|
public void StaticRootCommit_HiddenObjectUpdatesPoseWithoutRestoringShadow()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue