acdream/src/AcDream.App/Update/UpdateFrameOrchestrator.cs
Erik 9fab1feb46 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 <codex@openai.com>
2026-07-24 23:00:31 +02:00

198 lines
6.6 KiB
C#

namespace AcDream.App.Update;
using AcDream.App.Streaming;
/// <summary>
/// The host-supplied input for one update callback.
/// </summary>
internal readonly record struct UpdateFrameInput(double HostDeltaSeconds);
/// <summary>
/// The normalized simulation time shared by every time-advancing phase in one
/// host update.
/// </summary>
internal readonly record struct UpdateFrameTiming(
double SimulationDeltaSeconds,
float SimulationDeltaSecondsSingle,
double ScriptTime);
/// <summary>
/// Owns the normalized host delta and the monotonic PhysicsScript clock.
/// </summary>
/// <remarks>
/// Retail <c>ScriptManager::AddScriptInternal @ 0x0051B310</c> stamps
/// PhysicsScript work from <c>Timer::cur_time</c> at the call site. acdream
/// uses one update-frame timestamp for packet/default scripts, animation
/// hooks, and the later script drain. Absolute liveness time and raw-mouse
/// idle time intentionally do not enter this clock.
/// </remarks>
internal sealed class UpdateFrameClock : IPhysicsScriptTimeSource
{
public double CurrentScriptTime { get; private set; }
public UpdateFrameTiming Advance(
UpdateFrameInput input,
bool advanceScriptClock = true)
{
double deltaSeconds = NormalizeDeltaSeconds(input.HostDeltaSeconds);
if (advanceScriptClock)
CurrentScriptTime += deltaSeconds;
return new UpdateFrameTiming(
deltaSeconds,
(float)deltaSeconds,
CurrentScriptTime);
}
public static double NormalizeDeltaSeconds(double deltaSeconds) =>
double.IsFinite(deltaSeconds)
&& deltaSeconds > 0.0
&& deltaSeconds <= float.MaxValue
? deltaSeconds
: 0.0;
}
internal interface IPhysicsScriptTimeSource
{
double CurrentScriptTime { get; }
}
internal interface IUpdateFrameTeardownPhase
{
void RetryPendingTeardowns();
}
internal interface IUpdateFrameFailureSink
{
void ReportTeardownFailure(AggregateException error);
}
internal interface IUpdateFrameScriptClockPublisher
{
void PublishTime(double scriptTime);
}
internal interface IStreamingFramePhase
{
void Tick();
}
internal interface IGameplayInputFramePhase
{
void Tick(UpdateFrameTiming timing);
}
internal interface IRetailLiveFramePhase
{
void Tick(float deltaSeconds);
}
internal interface ILiveEntityLivenessFramePhase
{
void Tick();
}
internal interface ILocalPlayerTeleportFramePhase
{
void Tick(float deltaSeconds);
}
internal interface IPlayerModeAutoEntryFramePhase
{
void TryEnter();
}
internal interface ICameraFramePhase
{
void Tick(UpdateFrameTiming timing);
}
internal interface IUpdateFrameCommitPhase
{
void Commit();
}
/// <summary>
/// Owns the accepted acdream host-update phase graph.
/// </summary>
/// <remarks>
/// The nested object/network/command/reconcile order remains owned by
/// <see cref="IRetailLiveFramePhase"/>. This host order is the accepted TS-53
/// adaptation and is not claimed to be the exact retail Client::UseTime order.
/// </remarks>
internal sealed class UpdateFrameOrchestrator : AcDream.App.Rendering.IGameUpdateFrameRoot
{
private readonly IUpdateFrameTeardownPhase _teardown;
private readonly IUpdateFrameFailureSink _failureSink;
private readonly UpdateFrameClock _clock;
private readonly IUpdateFrameScriptClockPublisher _scriptClockPublisher;
private readonly IStreamingFramePhase _streaming;
private readonly IGameplayInputFramePhase _input;
private readonly IRetailLiveFramePhase _liveFrame;
private readonly ILiveEntityLivenessFramePhase _liveness;
private readonly ILocalPlayerTeleportFramePhase _teleport;
private readonly IPlayerModeAutoEntryFramePhase _playerModeAutoEntry;
private readonly ICameraFramePhase _camera;
private readonly IUpdateFrameCommitPhase _commit;
private readonly IWorldGenerationAvailability _availability;
public UpdateFrameOrchestrator(
IUpdateFrameTeardownPhase teardown,
IUpdateFrameFailureSink failureSink,
UpdateFrameClock clock,
IUpdateFrameScriptClockPublisher scriptClockPublisher,
IStreamingFramePhase streaming,
IGameplayInputFramePhase input,
IRetailLiveFramePhase liveFrame,
ILiveEntityLivenessFramePhase liveness,
ILocalPlayerTeleportFramePhase teleport,
IPlayerModeAutoEntryFramePhase playerModeAutoEntry,
ICameraFramePhase camera,
IUpdateFrameCommitPhase commit,
IWorldGenerationAvailability? availability = null)
{
_teardown = teardown ?? throw new ArgumentNullException(nameof(teardown));
_failureSink = failureSink ?? throw new ArgumentNullException(nameof(failureSink));
_clock = clock ?? throw new ArgumentNullException(nameof(clock));
_scriptClockPublisher = scriptClockPublisher
?? throw new ArgumentNullException(nameof(scriptClockPublisher));
_streaming = streaming ?? throw new ArgumentNullException(nameof(streaming));
_input = input ?? throw new ArgumentNullException(nameof(input));
_liveFrame = liveFrame ?? throw new ArgumentNullException(nameof(liveFrame));
_liveness = liveness ?? throw new ArgumentNullException(nameof(liveness));
_teleport = teleport ?? throw new ArgumentNullException(nameof(teleport));
_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;
}
public void Tick(UpdateFrameInput input)
{
try
{
_teardown.RetryPendingTeardowns();
}
catch (AggregateException error)
{
// The canonical teardown tombstone retains unfinished work for the
// next frame. Other exception types are programming failures and
// deliberately propagate.
_failureSink.ReportTeardownFailure(error);
}
UpdateFrameTiming timing = _clock.Advance(
input,
advanceScriptClock: _availability.IsWorldAvailable);
_scriptClockPublisher.PublishTime(timing.ScriptTime);
_streaming.Tick();
_input.Tick(timing);
_liveFrame.Tick(timing.SimulationDeltaSecondsSingle);
if (_availability.IsWorldAvailable)
_liveness.Tick();
_teleport.Tick(timing.SimulationDeltaSecondsSingle);
_playerModeAutoEntry.TryEnter();
_camera.Tick(timing);
_commit.Commit();
}
}