fix(rendering): synchronize hidden live projections
Inbound state continues while portal reveal blocks ordinary world simulation. Synchronize only the derived render projection after those hidden-frame network and command phases so the first revealed draw cannot observe a stale live transform, without advancing effects, particles, attachments, or lights.
This commit is contained in:
parent
528dec6072
commit
6a026c5ab0
6 changed files with 52 additions and 4 deletions
|
|
@ -517,7 +517,8 @@ internal sealed class FrameRootCompositionPhase
|
|||
session.LiveSession,
|
||||
session.LocalPlayerFrame,
|
||||
session.LiveSpatialReconciler,
|
||||
live.WorldAvailability);
|
||||
live.WorldAvailability,
|
||||
live.RenderSceneShadow?.LiveProjections);
|
||||
var cameraFrame = new CameraFrameController(
|
||||
host.CameraController,
|
||||
d.InputCapture,
|
||||
|
|
|
|||
|
|
@ -1,16 +1,16 @@
|
|||
using AcDream.App.Update;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Rendering.Scene;
|
||||
|
||||
internal interface ILiveRenderProjectionSink
|
||||
internal interface ILiveRenderProjectionSink : IRenderProjectionSyncPhase
|
||||
{
|
||||
bool OnEntityReady(LiveEntityReadyCandidate candidate);
|
||||
void OnProjectionVisibilityChanged(LiveEntityRecord record, bool visible);
|
||||
void OnProjectionPoseReady(uint serverGuid);
|
||||
void OnProjectionRemoved(uint localEntityId);
|
||||
void OnResourceUnregister(WorldEntity entity);
|
||||
void SynchronizeActiveSources();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -34,6 +34,17 @@ internal interface ILiveSpatialReconcilePhase
|
|||
void Reconcile();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Keeps the derived render projection synchronized after inbound mutation
|
||||
/// while normal world simulation/presentation is blocked by portal reveal.
|
||||
/// This is deliberately narrower than full spatial reconciliation: hidden
|
||||
/// frames must not advance particles, effects, attachments, or lights.
|
||||
/// </summary>
|
||||
internal interface IRenderProjectionSyncPhase
|
||||
{
|
||||
void SynchronizeActiveSources();
|
||||
}
|
||||
|
||||
internal interface IParticleRangeSource
|
||||
{
|
||||
float RangeMultiplier { get; }
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ internal sealed class RetailLiveFrameCoordinator : IRetailLiveFramePhase
|
|||
private readonly IPostNetworkCommandFramePhase _localPlayer;
|
||||
private readonly ILiveSpatialReconcilePhase _spatialReconciler;
|
||||
private readonly IWorldGenerationAvailability _availability;
|
||||
private readonly IRenderProjectionSyncPhase? _renderProjectionSync;
|
||||
|
||||
public RetailLiveFrameCoordinator(
|
||||
ILiveObjectFramePhase objects,
|
||||
|
|
@ -32,7 +33,8 @@ internal sealed class RetailLiveFrameCoordinator : IRetailLiveFramePhase
|
|||
ILiveSessionFramePhase session,
|
||||
IPostNetworkCommandFramePhase localPlayer,
|
||||
ILiveSpatialReconcilePhase spatialReconciler,
|
||||
IWorldGenerationAvailability? availability = null)
|
||||
IWorldGenerationAvailability? availability = null,
|
||||
IRenderProjectionSyncPhase? renderProjectionSync = null)
|
||||
{
|
||||
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
|
||||
_worldState = worldState ?? throw new ArgumentNullException(nameof(worldState));
|
||||
|
|
@ -41,6 +43,7 @@ internal sealed class RetailLiveFrameCoordinator : IRetailLiveFramePhase
|
|||
_spatialReconciler = spatialReconciler
|
||||
?? throw new ArgumentNullException(nameof(spatialReconciler));
|
||||
_availability = availability ?? AlwaysAvailableWorldGeneration.Instance;
|
||||
_renderProjectionSync = renderProjectionSync;
|
||||
}
|
||||
|
||||
public void Tick(float deltaSeconds)
|
||||
|
|
@ -53,6 +56,8 @@ internal sealed class RetailLiveFrameCoordinator : IRetailLiveFramePhase
|
|||
_localPlayer.RunPostNetworkCommandPhase();
|
||||
if (_availability.IsWorldAvailable)
|
||||
_spatialReconciler.Reconcile();
|
||||
else
|
||||
_renderProjectionSync?.SynchronizeActiveSources();
|
||||
}
|
||||
|
||||
public static double NormalizeDeltaSeconds(double deltaSeconds) =>
|
||||
|
|
|
|||
|
|
@ -25,3 +25,9 @@ internal sealed class TestLiveSpatialReconcilePhase(Action reconcile)
|
|||
{
|
||||
public void Reconcile() => reconcile();
|
||||
}
|
||||
|
||||
internal sealed class TestRenderProjectionSyncPhase(Action synchronize)
|
||||
: IRenderProjectionSyncPhase
|
||||
{
|
||||
public void SynchronizeActiveSources() => synchronize();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,6 +57,31 @@ public sealed class RecallTeleportAnimationTests
|
|||
Assert.Equal(["network", "commands"], calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LiveFrame_WhileCellsAreBlocked_SynchronizesOnlyRenderProjection()
|
||||
{
|
||||
var calls = new List<string>();
|
||||
var availability = new WorldGenerationAvailabilityState();
|
||||
availability.Begin(1);
|
||||
var frame = new RetailLiveFrameCoordinator(
|
||||
new TestLiveObjectFramePhase(_ => calls.Add("objects")),
|
||||
new GpuWorldState(availability: availability),
|
||||
new TestLiveSessionFramePhase(() => calls.Add("network")),
|
||||
new TestPostNetworkCommandFramePhase(
|
||||
() => calls.Add("commands")),
|
||||
new TestLiveSpatialReconcilePhase(
|
||||
() => calls.Add("spatial")),
|
||||
availability,
|
||||
new TestRenderProjectionSyncPhase(
|
||||
() => calls.Add("render-projection")));
|
||||
|
||||
frame.Tick(1f / 60f);
|
||||
|
||||
Assert.Equal(
|
||||
["network", "commands", "render-projection"],
|
||||
calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LiveFrame_InvalidHostDeltaCannotBlockNetworkDispatch()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue