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
|
||||
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
|
||||
|
||||
### G0 — Borrowed double-buffered frame product
|
||||
|
|
|
|||
|
|
@ -91,7 +91,21 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink
|
|||
&& tracked.Projection.ProjectionClass is
|
||||
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))
|
||||
{
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -140,13 +140,14 @@ public sealed class LiveRenderProjectionJournalTests
|
|||
Assert.Equal(
|
||||
incarnation,
|
||||
harness.Journal.Pending[0].Record.OwnerIncarnation);
|
||||
Assert.True(harness.Runtime.WithdrawLiveEntityProjection(Guid));
|
||||
harness.Projections.OnProjectionRemoved(record.WorldEntity.Id);
|
||||
harness.Projections.OnProjectionRemoved(record.WorldEntity.Id);
|
||||
Assert.Equal(2, harness.Journal.Count);
|
||||
Assert.Equal(
|
||||
RenderProjectionDeltaKind.Unregister,
|
||||
RenderProjectionDeltaKind.UpdateFlags,
|
||||
harness.Journal.Pending[1].Kind);
|
||||
Assert.Equal(0, harness.Projections.ProjectionCount);
|
||||
Assert.Equal(1, harness.Projections.ProjectionCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
@ -266,7 +267,7 @@ public sealed class LiveRenderProjectionJournalTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void SynchronizeActiveSources_DoesNotResurrectRemovedAttachment()
|
||||
public void SynchronizeActiveSources_DoesNotReactivateWithdrawnAttachment()
|
||||
{
|
||||
Harness harness = CreateHarness(loadLandblock: true);
|
||||
LiveEntityRecord record = Materialize(
|
||||
|
|
@ -277,13 +278,18 @@ public sealed class LiveRenderProjectionJournalTests
|
|||
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);
|
||||
|
||||
harness.Projections.SynchronizeActiveSources();
|
||||
|
||||
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]
|
||||
|
|
@ -298,20 +304,46 @@ public sealed class LiveRenderProjectionJournalTests
|
|||
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.RebucketLiveEntity(Guid, CellId));
|
||||
|
||||
harness.Projections.OnProjectionPoseReady(record.ServerGuid);
|
||||
|
||||
RenderProjectionDelta recovered =
|
||||
Assert.Single(harness.Journal.Pending.ToArray());
|
||||
Assert.Equal(RenderProjectionDeltaKind.Register, recovered.Kind);
|
||||
Assert.Equal(
|
||||
RenderProjectionClass.EquippedChild,
|
||||
recovered.Record.ProjectionClass);
|
||||
RenderProjectionDelta recovered = Assert.Single(
|
||||
harness.Journal.Pending.ToArray(),
|
||||
delta => delta.Kind is RenderProjectionDeltaKind.UpdateFlags);
|
||||
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)
|
||||
{
|
||||
var state = new GpuWorldState();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue