From 9fab1feb46bcaa23a8a30b499b7c0e05754a9cdb Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 24 Jul 2026 23:00:31 +0200 Subject: [PATCH] fix(rendering): publish scene deltas at update boundary Move the ordinary shadow-scene drain out of the conditional spatial reconciler and into an explicit final update-frame commit phase. This publishes deltas after streaming, network, teleport, camera, and conditional reconciles even while world simulation is quiesced. Release gate: 3,733 App tests / 3 skips; 8,217 complete-solution tests / 5 skips. Co-Authored-By: OpenAI Codex --- ...4-modern-runtime-slices-f-g-render-scene.md | 14 ++++++++++++++ .../Composition/FrameRootComposition.cs | 1 + .../Composition/SessionPlayerComposition.cs | 3 +-- .../Update/LiveObjectFrameController.cs | 6 +----- .../Update/RenderSceneUpdateCommitPhase.cs | 18 ++++++++++++++++++ .../Update/UpdateFrameOrchestrator.cs | 9 +++++++++ .../World/UpdateFrameOrchestratorTests.cs | 15 +++++++++++++-- 7 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 src/AcDream.App/Update/RenderSceneUpdateCommitPhase.cs 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; } = [];