diff --git a/docs/plans/2026-07-24-modern-runtime-slices-f-g-render-scene.md b/docs/plans/2026-07-24-modern-runtime-slices-f-g-render-scene.md index decb0e5c..cff746cf 100644 --- a/docs/plans/2026-07-24-modern-runtime-slices-f-g-render-scene.md +++ b/docs/plans/2026-07-24-modern-runtime-slices-f-g-render-scene.md @@ -443,6 +443,23 @@ publication; session reset retains its separate ordered teardown drain. The next capped redrive must prove both zero pending deltas and expose or clear any remaining projection mismatch before uncapped evidence begins. +The exact `9fab1feb` capped route proved the final commit boundary: no pending +or rejected delta mismatch recurred. At Sawato revisit the referee then +preserved the remaining fault as +`sourceChannel=live id=projection:03000000000F4279 field=presence`. +Category counts showed two equipped children missing while every ordinary root +and static projection matched. The equipped controller intentionally removes +child presentation during portal transit without discarding the accepted +parent relation; destination recomposition publishes `ProjectionPoseReady`, +not a second logical `EntityReady`. The live journal incorrectly required the +child to be already retained before accepting that callback. The correction +treats the equipped owner's composed-pose callback as the authoritative +attached-child presence/update edge. A permanent test proves a current +attachment removed for presentation is registered again on its first +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. + ## 6. Slice G — Frame product and production cutover ### G0 — Borrowed double-buffered frame product diff --git a/src/AcDream.App/Rendering/Scene/LiveRenderProjectionJournal.cs b/src/AcDream.App/Rendering/Scene/LiveRenderProjectionJournal.cs index 12d2b4f6..0b6ff53a 100644 --- a/src/AcDream.App/Rendering/Scene/LiveRenderProjectionJournal.cs +++ b/src/AcDream.App/Rendering/Scene/LiveRenderProjectionJournal.cs @@ -70,12 +70,16 @@ internal sealed class LiveRenderProjectionJournal : ILiveRenderProjectionSink { if (!_runtime.TryGetRecord(serverGuid, out LiveEntityRecord record) || record.WorldEntity is not { } entity - || !_byLocalId.ContainsKey(entity.Id) || record.ProjectionKind is not LiveEntityProjectionKind.Attached) { return; } + // This callback is the attached-projection owner's authoritative + // presence edge as well as its pose-update edge. A child can leave + // presentation during a portal transition while its accepted parent + // relation remains alive; the first composed destination pose must + // therefore restore a projection removed by that transition. Upsert(record, entity, record.IsSpatiallyVisible); } diff --git a/tests/AcDream.App.Tests/Rendering/LiveRenderProjectionJournalTests.cs b/tests/AcDream.App.Tests/Rendering/LiveRenderProjectionJournalTests.cs index 9e85d63d..871ffc9b 100644 --- a/tests/AcDream.App.Tests/Rendering/LiveRenderProjectionJournalTests.cs +++ b/tests/AcDream.App.Tests/Rendering/LiveRenderProjectionJournalTests.cs @@ -286,6 +286,32 @@ public sealed class LiveRenderProjectionJournalTests Assert.Equal(0, harness.Projections.ProjectionCount); } + [Fact] + public void ProjectionPoseReady_RecoversCurrentAttachmentAfterPresentationRemoval() + { + Harness harness = CreateHarness(loadLandblock: true); + LiveEntityRecord record = Materialize( + harness, + Guid, + instance: 10, + projectionKind: LiveEntityProjectionKind.Attached); + Assert.True(harness.Projections.OnEntityReady( + LiveEntityReadyCandidate.Capture(record))); + harness.Journal.DrainTo(harness.Scene); + harness.Projections.OnProjectionRemoved(record.WorldEntity!.Id); + harness.Journal.DrainTo(harness.Scene); + + 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); + Assert.Equal(record.LocalEntityId, recovered.Record.Source.LocalEntityId); + } + private static Harness CreateHarness(bool loadLandblock) { var state = new GpuWorldState();