fix(rendering): retain withdrawn attachment identities
Separate attached-child spatial withdrawal from logical teardown. ProjectionRemoved now deactivates the retained scene identity, allowing residency or destination-pose callbacks to reactivate it; resource unregister remains the sole destruction edge. Release gate: 3,735 App tests / 3 skips; 8,219 complete-solution tests / 5 skips. Co-Authored-By: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
bb1f4a64bf
commit
81e2f1a575
4 changed files with 81 additions and 12 deletions
|
|
@ -460,6 +460,25 @@ destination pose, while passive synchronization still cannot resurrect a
|
||||||
removed attachment. The exact corrected commit must repeat the capped route
|
removed attachment. The exact corrected commit must repeat the capped route
|
||||||
before uncapped and dense-town gates.
|
before uncapped and dense-town gates.
|
||||||
|
|
||||||
|
The exact `bb1f4a64` capped route showed that destination-pose recovery covers
|
||||||
|
attachments which are recomposed, but Sawato's retained population has seven
|
||||||
|
children whose accepted relation is not replayed on revisit. The first missing
|
||||||
|
identity changed with that population (`projection:03000000000F4286`);
|
||||||
|
baseline had 43 roots + 8 children and revisit had the same 43 roots but only
|
||||||
|
1 retained child. Existing rendering still presents those children through
|
||||||
|
the rehydrated spatial entity set.
|
||||||
|
|
||||||
|
The lifecycle correction is to distinguish spatial withdrawal from logical
|
||||||
|
unregistration. `ProjectionRemoved` means an equipped child left cell
|
||||||
|
presentation; it now retains the exact scene identity with `Draw` inactive.
|
||||||
|
The existing projection-visibility edge can reactivate that record on
|
||||||
|
same-location rehydration, and a destination pose can still refresh it.
|
||||||
|
`OnResourceUnregister` remains the only logical teardown edge. Tests now pin
|
||||||
|
inactive retention, passive non-reactivation, exact reactivation, duplicate
|
||||||
|
removal, and final logical teardown. Missing-projection diagnostics also carry
|
||||||
|
the expected class, server GUID, local ID, and parent cell for any further
|
||||||
|
failure. Repeat capped F5 on the exact corrective commit before advancing.
|
||||||
|
|
||||||
## 6. Slice G — Frame product and production cutover
|
## 6. Slice G — Frame product and production cutover
|
||||||
|
|
||||||
### G0 — Borrowed double-buffered frame product
|
### G0 — Borrowed double-buffered frame product
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,21 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
||||||
&& tracked.Projection.ProjectionClass is
|
&& tracked.Projection.ProjectionClass is
|
||||||
RenderProjectionClass.EquippedChild)
|
RenderProjectionClass.EquippedChild)
|
||||||
{
|
{
|
||||||
Remove(tracked);
|
if (_runtime.IsCurrentRecord(tracked.Record)
|
||||||
|
&& ReferenceEquals(
|
||||||
|
tracked.Record.WorldEntity,
|
||||||
|
tracked.Entity))
|
||||||
|
{
|
||||||
|
// Leaving cell presentation is not logical destruction. Keep
|
||||||
|
// the projection identity inactive so a later residency or
|
||||||
|
// destination-pose edge can reactivate the same attachment
|
||||||
|
// without replaying EntityReady.
|
||||||
|
Upsert(tracked.Record, tracked.Entity, spatiallyVisible: false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Remove(tracked);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -323,7 +323,11 @@ internal sealed class RenderSceneShadowComparisonController :
|
||||||
if (!query.TryGet(id, out RenderProjectionRecord actual))
|
if (!query.TryGet(id, out RenderProjectionRecord actual))
|
||||||
{
|
{
|
||||||
return $"sourceChannel={Channel(id)} id={id} field=presence "
|
return $"sourceChannel={Channel(id)} id={id} field=presence "
|
||||||
+ "expected=present actual=missing";
|
+ "expected=present actual=missing "
|
||||||
|
+ $"expectedClass={fingerprint.ProjectionClass} "
|
||||||
|
+ $"serverGuid=0x{fingerprint.ServerGuid:X8} "
|
||||||
|
+ $"localEntityId=0x{fingerprint.EntityId:X8} "
|
||||||
|
+ $"parentCell=0x{fingerprint.ParentCellId:X8}";
|
||||||
}
|
}
|
||||||
|
|
||||||
string? mismatch = CompareProjection(
|
string? mismatch = CompareProjection(
|
||||||
|
|
|
||||||
|
|
@ -140,13 +140,14 @@ public sealed class LiveRenderProjectionJournalTests
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
incarnation,
|
incarnation,
|
||||||
harness.Journal.Pending[0].Record.OwnerIncarnation);
|
harness.Journal.Pending[0].Record.OwnerIncarnation);
|
||||||
|
Assert.True(harness.Runtime.WithdrawLiveEntityProjection(Guid));
|
||||||
harness.Projections.OnProjectionRemoved(record.WorldEntity.Id);
|
harness.Projections.OnProjectionRemoved(record.WorldEntity.Id);
|
||||||
harness.Projections.OnProjectionRemoved(record.WorldEntity.Id);
|
harness.Projections.OnProjectionRemoved(record.WorldEntity.Id);
|
||||||
Assert.Equal(2, harness.Journal.Count);
|
Assert.Equal(2, harness.Journal.Count);
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
RenderProjectionDeltaKind.Unregister,
|
RenderProjectionDeltaKind.UpdateFlags,
|
||||||
harness.Journal.Pending[1].Kind);
|
harness.Journal.Pending[1].Kind);
|
||||||
Assert.Equal(0, harness.Projections.ProjectionCount);
|
Assert.Equal(1, harness.Projections.ProjectionCount);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -266,7 +267,7 @@ public sealed class LiveRenderProjectionJournalTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void SynchronizeActiveSources_DoesNotResurrectRemovedAttachment()
|
public void SynchronizeActiveSources_DoesNotReactivateWithdrawnAttachment()
|
||||||
{
|
{
|
||||||
Harness harness = CreateHarness(loadLandblock: true);
|
Harness harness = CreateHarness(loadLandblock: true);
|
||||||
LiveEntityRecord record = Materialize(
|
LiveEntityRecord record = Materialize(
|
||||||
|
|
@ -277,13 +278,18 @@ public sealed class LiveRenderProjectionJournalTests
|
||||||
Assert.True(harness.Projections.OnEntityReady(
|
Assert.True(harness.Projections.OnEntityReady(
|
||||||
LiveEntityReadyCandidate.Capture(record)));
|
LiveEntityReadyCandidate.Capture(record)));
|
||||||
harness.Journal.DrainTo(harness.Scene);
|
harness.Journal.DrainTo(harness.Scene);
|
||||||
|
Assert.True(harness.Runtime.WithdrawLiveEntityProjection(Guid));
|
||||||
harness.Projections.OnProjectionRemoved(record.WorldEntity!.Id);
|
harness.Projections.OnProjectionRemoved(record.WorldEntity!.Id);
|
||||||
harness.Journal.DrainTo(harness.Scene);
|
harness.Journal.DrainTo(harness.Scene);
|
||||||
|
|
||||||
harness.Projections.SynchronizeActiveSources();
|
harness.Projections.SynchronizeActiveSources();
|
||||||
|
|
||||||
Assert.Equal(0, harness.Journal.Count);
|
Assert.Equal(0, harness.Journal.Count);
|
||||||
Assert.Equal(0, harness.Projections.ProjectionCount);
|
Assert.Equal(1, harness.Projections.ProjectionCount);
|
||||||
|
Assert.True(harness.Scene.OpenQuery().TryGet(
|
||||||
|
LiveRenderProjectionJournal.ProjectionId(record.WorldEntity!.Id),
|
||||||
|
out RenderProjectionRecord inactive));
|
||||||
|
Assert.True((inactive.Flags & RenderProjectionFlags.Draw) == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -298,20 +304,46 @@ public sealed class LiveRenderProjectionJournalTests
|
||||||
Assert.True(harness.Projections.OnEntityReady(
|
Assert.True(harness.Projections.OnEntityReady(
|
||||||
LiveEntityReadyCandidate.Capture(record)));
|
LiveEntityReadyCandidate.Capture(record)));
|
||||||
harness.Journal.DrainTo(harness.Scene);
|
harness.Journal.DrainTo(harness.Scene);
|
||||||
|
Assert.True(harness.Runtime.WithdrawLiveEntityProjection(Guid));
|
||||||
harness.Projections.OnProjectionRemoved(record.WorldEntity!.Id);
|
harness.Projections.OnProjectionRemoved(record.WorldEntity!.Id);
|
||||||
harness.Journal.DrainTo(harness.Scene);
|
harness.Journal.DrainTo(harness.Scene);
|
||||||
|
Assert.True(harness.Runtime.RebucketLiveEntity(Guid, CellId));
|
||||||
|
|
||||||
harness.Projections.OnProjectionPoseReady(record.ServerGuid);
|
harness.Projections.OnProjectionPoseReady(record.ServerGuid);
|
||||||
|
|
||||||
RenderProjectionDelta recovered =
|
RenderProjectionDelta recovered = Assert.Single(
|
||||||
Assert.Single(harness.Journal.Pending.ToArray());
|
harness.Journal.Pending.ToArray(),
|
||||||
Assert.Equal(RenderProjectionDeltaKind.Register, recovered.Kind);
|
delta => delta.Kind is RenderProjectionDeltaKind.UpdateFlags);
|
||||||
Assert.Equal(
|
|
||||||
RenderProjectionClass.EquippedChild,
|
|
||||||
recovered.Record.ProjectionClass);
|
|
||||||
Assert.Equal(record.LocalEntityId, recovered.Record.Source.LocalEntityId);
|
Assert.Equal(record.LocalEntityId, recovered.Record.Source.LocalEntityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WithdrawnAttachment_LogicalTeardownUnregistersRetainedIdentity()
|
||||||
|
{
|
||||||
|
Harness harness = CreateHarness(loadLandblock: true);
|
||||||
|
const ushort instance = 11;
|
||||||
|
LiveEntityRecord record = Materialize(
|
||||||
|
harness,
|
||||||
|
Guid,
|
||||||
|
instance,
|
||||||
|
projectionKind: LiveEntityProjectionKind.Attached);
|
||||||
|
Assert.True(harness.Projections.OnEntityReady(
|
||||||
|
LiveEntityReadyCandidate.Capture(record)));
|
||||||
|
harness.Journal.DrainTo(harness.Scene);
|
||||||
|
Assert.True(harness.Runtime.WithdrawLiveEntityProjection(Guid));
|
||||||
|
harness.Projections.OnProjectionRemoved(record.WorldEntity!.Id);
|
||||||
|
harness.Journal.DrainTo(harness.Scene);
|
||||||
|
|
||||||
|
Assert.True(harness.Runtime.UnregisterLiveEntity(
|
||||||
|
new DeleteObject.Parsed(Guid, instance),
|
||||||
|
isLocalPlayer: false));
|
||||||
|
|
||||||
|
RenderProjectionDelta removed =
|
||||||
|
Assert.Single(harness.Journal.Pending.ToArray());
|
||||||
|
Assert.Equal(RenderProjectionDeltaKind.Unregister, removed.Kind);
|
||||||
|
Assert.Equal(0, harness.Projections.ProjectionCount);
|
||||||
|
}
|
||||||
|
|
||||||
private static Harness CreateHarness(bool loadLandblock)
|
private static Harness CreateHarness(bool loadLandblock)
|
||||||
{
|
{
|
||||||
var state = new GpuWorldState();
|
var state = new GpuWorldState();
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue