From bb1f4a64bf88ac5117bfd225b4bfa631b3fade16 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 24 Jul 2026 23:11:48 +0200 Subject: [PATCH] fix(rendering): restore equipped scene projections on pose Treat the equipped-child controller's composed-pose callback as the authoritative attached projection presence edge. Portal transit can withdraw presentation while retaining the accepted parent relation, so the first destination pose must register the child again. Release gate: 3,734 App tests / 3 skips; 8,218 complete-solution tests / 5 skips. Co-Authored-By: OpenAI Codex --- ...-modern-runtime-slices-f-g-render-scene.md | 17 ++++++++++++ .../Scene/LiveRenderProjectionJournal.cs | 6 ++++- .../LiveRenderProjectionJournalTests.cs | 26 +++++++++++++++++++ 3 files changed, 48 insertions(+), 1 deletion(-) 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();