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 4a3bf91a..decb0e5c 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
@@ -429,6 +429,20 @@ updates, and active animated-static synchronization. The capped, uncapped, and
dense-town routes are all rerun from the corrective commit; no F5 success is
claimed from the rejected `056bbd4e` route.
+The next exact capped route on `91463db5` completed all nine stops and shut
+down gracefully, but F5 again rejected it. A transition frame reached the
+post-render referee with 5,111 pending deltas because the only ordinary drain
+was embedded in the spatial reconciler, which is intentionally skipped while
+the world is quiesced. Later revisit frames also diverged, but the bounded
+first-mismatch recorder correctly retained the earlier journal-ordering fault.
+The correction makes scene publication an explicit final update-frame commit
+phase after streaming, network, teleport, camera, and every conditional
+spatial reconcile. It runs even while world simulation is quiesced. The former
+reconciler drain is removed, leaving one ordinary update/render-boundary
+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.
+
## 6. Slice G — Frame product and production cutover
### G0 — Borrowed double-buffered frame product
diff --git a/src/AcDream.App/Composition/FrameRootComposition.cs b/src/AcDream.App/Composition/FrameRootComposition.cs
index 61c55cb9..c3e16388 100644
--- a/src/AcDream.App/Composition/FrameRootComposition.cs
+++ b/src/AcDream.App/Composition/FrameRootComposition.cs
@@ -532,6 +532,7 @@ internal sealed class FrameRootCompositionPhase
session.LocalTeleport,
new PlayerModeAutoEntryFramePhase(session.PlayerModeAutoEntry),
cameraFrame,
+ new RenderSceneUpdateCommitPhase(live.RenderSceneShadow),
live.WorldAvailability);
Fault(FrameRootCompositionPoint.UpdateRootCreated);
diff --git a/src/AcDream.App/Composition/SessionPlayerComposition.cs b/src/AcDream.App/Composition/SessionPlayerComposition.cs
index 4bfafab9..aae39e74 100644
--- a/src/AcDream.App/Composition/SessionPlayerComposition.cs
+++ b/src/AcDream.App/Composition/SessionPlayerComposition.cs
@@ -606,8 +606,7 @@ internal sealed class SessionPlayerCompositionPhase
live.EquippedChildren,
content.ParticleSink,
live.Lights,
- live.RenderSceneShadow?.LiveProjections,
- live.RenderSceneShadow);
+ live.RenderSceneShadow?.LiveProjections);
var localPlayerAnimation = new LocalPlayerAnimationController(
live.LiveEntities,
d.PlayerIdentity,
diff --git a/src/AcDream.App/Update/LiveObjectFrameController.cs b/src/AcDream.App/Update/LiveObjectFrameController.cs
index dd62774d..8a3f6a7c 100644
--- a/src/AcDream.App/Update/LiveObjectFrameController.cs
+++ b/src/AcDream.App/Update/LiveObjectFrameController.cs
@@ -238,15 +238,13 @@ internal sealed class LiveSpatialPresentationReconciler : ILiveSpatialReconcileP
private readonly ParticleHookSink _particleSink;
private readonly LiveEntityLightController _lights;
private readonly ILiveRenderProjectionSink? _renderProjections;
- private readonly RenderSceneShadowRuntime? _renderSceneShadow;
public LiveSpatialPresentationReconciler(
EntityEffectController entityEffects,
EquippedChildRenderController equippedChildren,
ParticleHookSink particleSink,
LiveEntityLightController lights,
- ILiveRenderProjectionSink? renderProjections = null,
- RenderSceneShadowRuntime? renderSceneShadow = null)
+ ILiveRenderProjectionSink? renderProjections = null)
{
_entityEffects = entityEffects ?? throw new ArgumentNullException(nameof(entityEffects));
_equippedChildren = equippedChildren
@@ -254,7 +252,6 @@ internal sealed class LiveSpatialPresentationReconciler : ILiveSpatialReconcileP
_particleSink = particleSink ?? throw new ArgumentNullException(nameof(particleSink));
_lights = lights ?? throw new ArgumentNullException(nameof(lights));
_renderProjections = renderProjections;
- _renderSceneShadow = renderSceneShadow;
}
public void Reconcile()
@@ -264,6 +261,5 @@ internal sealed class LiveSpatialPresentationReconciler : ILiveSpatialReconcileP
_renderProjections?.SynchronizeActiveSources();
_particleSink.RefreshAttachedEmitters();
_lights.Refresh();
- _renderSceneShadow?.DrainUpdateBoundary();
}
}
diff --git a/src/AcDream.App/Update/RenderSceneUpdateCommitPhase.cs b/src/AcDream.App/Update/RenderSceneUpdateCommitPhase.cs
new file mode 100644
index 00000000..c5ac8618
--- /dev/null
+++ b/src/AcDream.App/Update/RenderSceneUpdateCommitPhase.cs
@@ -0,0 +1,18 @@
+using AcDream.App.Rendering.Scene;
+
+namespace AcDream.App.Update;
+
+///
+/// Publishes every render-projection delta at the final update/render
+/// boundary, after streaming, network, teleport, and camera-owned spatial
+/// mutations have completed.
+///
+internal sealed class RenderSceneUpdateCommitPhase : IUpdateFrameCommitPhase
+{
+ private readonly RenderSceneShadowRuntime? _shadow;
+
+ public RenderSceneUpdateCommitPhase(RenderSceneShadowRuntime? shadow) =>
+ _shadow = shadow;
+
+ public void Commit() => _shadow?.DrainUpdateBoundary();
+}
diff --git a/src/AcDream.App/Update/UpdateFrameOrchestrator.cs b/src/AcDream.App/Update/UpdateFrameOrchestrator.cs
index 16d3540c..2e6a029a 100644
--- a/src/AcDream.App/Update/UpdateFrameOrchestrator.cs
+++ b/src/AcDream.App/Update/UpdateFrameOrchestrator.cs
@@ -106,6 +106,11 @@ internal interface ICameraFramePhase
void Tick(UpdateFrameTiming timing);
}
+internal interface IUpdateFrameCommitPhase
+{
+ void Commit();
+}
+
///
/// Owns the accepted acdream host-update phase graph.
///
@@ -127,6 +132,7 @@ internal sealed class UpdateFrameOrchestrator : AcDream.App.Rendering.IGameUpdat
private readonly ILocalPlayerTeleportFramePhase _teleport;
private readonly IPlayerModeAutoEntryFramePhase _playerModeAutoEntry;
private readonly ICameraFramePhase _camera;
+ private readonly IUpdateFrameCommitPhase _commit;
private readonly IWorldGenerationAvailability _availability;
public UpdateFrameOrchestrator(
@@ -141,6 +147,7 @@ internal sealed class UpdateFrameOrchestrator : AcDream.App.Rendering.IGameUpdat
ILocalPlayerTeleportFramePhase teleport,
IPlayerModeAutoEntryFramePhase playerModeAutoEntry,
ICameraFramePhase camera,
+ IUpdateFrameCommitPhase commit,
IWorldGenerationAvailability? availability = null)
{
_teardown = teardown ?? throw new ArgumentNullException(nameof(teardown));
@@ -156,6 +163,7 @@ internal sealed class UpdateFrameOrchestrator : AcDream.App.Rendering.IGameUpdat
_playerModeAutoEntry = playerModeAutoEntry
?? throw new ArgumentNullException(nameof(playerModeAutoEntry));
_camera = camera ?? throw new ArgumentNullException(nameof(camera));
+ _commit = commit ?? throw new ArgumentNullException(nameof(commit));
_availability = availability ?? AlwaysAvailableWorldGeneration.Instance;
}
@@ -185,5 +193,6 @@ internal sealed class UpdateFrameOrchestrator : AcDream.App.Rendering.IGameUpdat
_teleport.Tick(timing.SimulationDeltaSecondsSingle);
_playerModeAutoEntry.TryEnter();
_camera.Tick(timing);
+ _commit.Commit();
}
}
diff --git a/tests/AcDream.App.Tests/World/UpdateFrameOrchestratorTests.cs b/tests/AcDream.App.Tests/World/UpdateFrameOrchestratorTests.cs
index 6388c06a..5b2ef59c 100644
--- a/tests/AcDream.App.Tests/World/UpdateFrameOrchestratorTests.cs
+++ b/tests/AcDream.App.Tests/World/UpdateFrameOrchestratorTests.cs
@@ -30,6 +30,7 @@ public sealed class UpdateFrameOrchestratorTests
"teleport",
"auto-entry",
"camera",
+ "commit",
],
calls);
}
@@ -65,6 +66,7 @@ public sealed class UpdateFrameOrchestratorTests
"inbound-player-projection",
"inbound-player-reconcile",
"camera",
+ "commit",
],
calls);
}
@@ -89,7 +91,7 @@ public sealed class UpdateFrameOrchestratorTests
[
"teardown", "clock", "streaming", "input", "objects",
"network", "commands", "ordinary-reconcile", "liveness",
- "teleport", "auto-entry", "camera",
+ "teleport", "auto-entry", "camera", "commit",
],
calls);
Assert.Equal([0.0], observed.PublishedTimes);
@@ -153,6 +155,7 @@ public sealed class UpdateFrameOrchestratorTests
Assert.Equal(3, calls.Count(call => call == "network"));
Assert.Equal(3, calls.Count(call => call == "teleport"));
Assert.Equal(3, calls.Count(call => call == "camera"));
+ Assert.Equal(3, calls.Count(call => call == "commit"));
Assert.Equal([0.25f, 0.25f, 0.25f], observed.LiveDeltas);
Assert.Equal([0.25f, 0.25f, 0.25f], observed.TeleportDeltas);
}
@@ -190,7 +193,7 @@ public sealed class UpdateFrameOrchestratorTests
[
"teardown", "clock", "streaming", "input", "objects", "network",
"commands", "ordinary-reconcile", "liveness", "teleport",
- "auto-entry", "camera",
+ "auto-entry", "camera", "commit",
];
Assert.Equal(oneFrame.Concat(oneFrame), calls);
}
@@ -228,6 +231,7 @@ public sealed class UpdateFrameOrchestratorTests
typeof(ILocalPlayerTeleportFramePhase),
typeof(IPlayerModeAutoEntryFramePhase),
typeof(ICameraFramePhase),
+ typeof(IUpdateFrameCommitPhase),
typeof(IWorldGenerationAvailability),
];
@@ -907,6 +911,7 @@ public sealed class UpdateFrameOrchestratorTests
new RecordingTeleport(calls, observed, teleportPlace),
new RecordingAutoEntry(calls),
new RecordingCamera(calls, observed, inboundCreatedPlayer),
+ new RecordingCommit(calls),
availability);
}
@@ -1023,6 +1028,12 @@ public sealed class UpdateFrameOrchestratorTests
}
}
+ private sealed class RecordingCommit(List calls)
+ : IUpdateFrameCommitPhase
+ {
+ public void Commit() => calls.Add("commit");
+ }
+
private sealed class FrameObservations
{
public List PublishedTimes { get; } = [];