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.LiveSession,
|
||||||
session.LocalPlayerFrame,
|
session.LocalPlayerFrame,
|
||||||
session.LiveSpatialReconciler,
|
session.LiveSpatialReconciler,
|
||||||
live.WorldAvailability);
|
live.WorldAvailability,
|
||||||
|
live.RenderSceneShadow?.LiveProjections);
|
||||||
var cameraFrame = new CameraFrameController(
|
var cameraFrame = new CameraFrameController(
|
||||||
host.CameraController,
|
host.CameraController,
|
||||||
d.InputCapture,
|
d.InputCapture,
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,16 @@
|
||||||
|
using AcDream.App.Update;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering.Scene;
|
namespace AcDream.App.Rendering.Scene;
|
||||||
|
|
||||||
internal interface ILiveRenderProjectionSink
|
internal interface ILiveRenderProjectionSink : IRenderProjectionSyncPhase
|
||||||
{
|
{
|
||||||
bool OnEntityReady(LiveEntityReadyCandidate candidate);
|
bool OnEntityReady(LiveEntityReadyCandidate candidate);
|
||||||
void OnProjectionVisibilityChanged(LiveEntityRecord record, bool visible);
|
void OnProjectionVisibilityChanged(LiveEntityRecord record, bool visible);
|
||||||
void OnProjectionPoseReady(uint serverGuid);
|
void OnProjectionPoseReady(uint serverGuid);
|
||||||
void OnProjectionRemoved(uint localEntityId);
|
void OnProjectionRemoved(uint localEntityId);
|
||||||
void OnResourceUnregister(WorldEntity entity);
|
void OnResourceUnregister(WorldEntity entity);
|
||||||
void SynchronizeActiveSources();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,17 @@ internal interface ILiveSpatialReconcilePhase
|
||||||
void Reconcile();
|
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
|
internal interface IParticleRangeSource
|
||||||
{
|
{
|
||||||
float RangeMultiplier { get; }
|
float RangeMultiplier { get; }
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ internal sealed class RetailLiveFrameCoordinator : IRetailLiveFramePhase
|
||||||
private readonly IPostNetworkCommandFramePhase _localPlayer;
|
private readonly IPostNetworkCommandFramePhase _localPlayer;
|
||||||
private readonly ILiveSpatialReconcilePhase _spatialReconciler;
|
private readonly ILiveSpatialReconcilePhase _spatialReconciler;
|
||||||
private readonly IWorldGenerationAvailability _availability;
|
private readonly IWorldGenerationAvailability _availability;
|
||||||
|
private readonly IRenderProjectionSyncPhase? _renderProjectionSync;
|
||||||
|
|
||||||
public RetailLiveFrameCoordinator(
|
public RetailLiveFrameCoordinator(
|
||||||
ILiveObjectFramePhase objects,
|
ILiveObjectFramePhase objects,
|
||||||
|
|
@ -32,7 +33,8 @@ internal sealed class RetailLiveFrameCoordinator : IRetailLiveFramePhase
|
||||||
ILiveSessionFramePhase session,
|
ILiveSessionFramePhase session,
|
||||||
IPostNetworkCommandFramePhase localPlayer,
|
IPostNetworkCommandFramePhase localPlayer,
|
||||||
ILiveSpatialReconcilePhase spatialReconciler,
|
ILiveSpatialReconcilePhase spatialReconciler,
|
||||||
IWorldGenerationAvailability? availability = null)
|
IWorldGenerationAvailability? availability = null,
|
||||||
|
IRenderProjectionSyncPhase? renderProjectionSync = null)
|
||||||
{
|
{
|
||||||
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
|
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
|
||||||
_worldState = worldState ?? throw new ArgumentNullException(nameof(worldState));
|
_worldState = worldState ?? throw new ArgumentNullException(nameof(worldState));
|
||||||
|
|
@ -41,6 +43,7 @@ internal sealed class RetailLiveFrameCoordinator : IRetailLiveFramePhase
|
||||||
_spatialReconciler = spatialReconciler
|
_spatialReconciler = spatialReconciler
|
||||||
?? throw new ArgumentNullException(nameof(spatialReconciler));
|
?? throw new ArgumentNullException(nameof(spatialReconciler));
|
||||||
_availability = availability ?? AlwaysAvailableWorldGeneration.Instance;
|
_availability = availability ?? AlwaysAvailableWorldGeneration.Instance;
|
||||||
|
_renderProjectionSync = renderProjectionSync;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Tick(float deltaSeconds)
|
public void Tick(float deltaSeconds)
|
||||||
|
|
@ -53,6 +56,8 @@ internal sealed class RetailLiveFrameCoordinator : IRetailLiveFramePhase
|
||||||
_localPlayer.RunPostNetworkCommandPhase();
|
_localPlayer.RunPostNetworkCommandPhase();
|
||||||
if (_availability.IsWorldAvailable)
|
if (_availability.IsWorldAvailable)
|
||||||
_spatialReconciler.Reconcile();
|
_spatialReconciler.Reconcile();
|
||||||
|
else
|
||||||
|
_renderProjectionSync?.SynchronizeActiveSources();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static double NormalizeDeltaSeconds(double deltaSeconds) =>
|
public static double NormalizeDeltaSeconds(double deltaSeconds) =>
|
||||||
|
|
|
||||||
|
|
@ -25,3 +25,9 @@ internal sealed class TestLiveSpatialReconcilePhase(Action reconcile)
|
||||||
{
|
{
|
||||||
public void Reconcile() => 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);
|
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]
|
[Fact]
|
||||||
public void LiveFrame_InvalidHostDeltaCannotBlockNetworkDispatch()
|
public void LiveFrame_InvalidHostDeltaCannotBlockNetworkDispatch()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue