feat(rendering): compare the incremental shadow scene
Construct Slice F's non-drawing scene only for lifecycle automation, drain accepted static and live deltas at the final update boundary, and compare exact current-path fingerprints at cadence and checkpoints. Publish bounded mismatch, journal, index, digest, and memory evidence without changing normal launches or draw submission. Release: 8,211 passed, 5 skipped.
This commit is contained in:
parent
ff5d86175f
commit
0eb6648589
21 changed files with 937 additions and 20 deletions
|
|
@ -22,7 +22,7 @@ retail-faithful gameplay
|
||||||
| F2 — static projection journal | complete | Static and EnvCell-shell projections now append ordered deltas only after the final activation receipt and exact detach receipt. Same-landblock rehydrate reconciles retained/new/omitted identities; stale Far completions are inert. Seven focused lifecycle tests pass inside the 3,708-App / 8,192-solution Release gate. The scene remains unconstructed and non-drawing in production. |
|
| F2 — static projection journal | complete | Static and EnvCell-shell projections now append ordered deltas only after the final activation receipt and exact detach receipt. Same-landblock rehydrate reconciles retained/new/omitted identities; stale Far completions are inert. Seven focused lifecycle tests pass inside the 3,708-App / 8,192-solution Release gate. The scene remains unconstructed and non-drawing in production. |
|
||||||
| F3 — live/equipped projection | complete | Exact EntityReady/resource teardown, visibility, attachment pose/removal, and active-only final-frame seams now drive generation/incarnation-gated live records. Duplicate CreateObject, pending/loaded rebucket, hidden/appearance, reentrancy, session clear, attachment, and GUID-generation replacement pass 11 focused tests. Production still constructs no render scene. |
|
| F3 — live/equipped projection | complete | Exact EntityReady/resource teardown, visibility, attachment pose/removal, and active-only final-frame seams now drive generation/incarnation-gated live records. Duplicate CreateObject, pending/loaded rebucket, hidden/appearance, reentrancy, session clear, attachment, and GUID-generation replacement pass 11 focused tests. Production still constructs no render scene. |
|
||||||
| F4 — dynamic indices | complete | The contained scene now maintains outdoor/static, per-cell static/dynamic, special dynamic-route, translucent, selectable, light-candidate, and dirty indices incrementally. Final-frame live/equipped synchronization remains active-only, and active animated statics are synchronized from the scheduler's active workset rather than a resident-world scan. The production scene remains unconstructed and non-drawing. |
|
| F4 — dynamic indices | complete | The contained scene now maintains outdoor/static, per-cell static/dynamic, special dynamic-route, translucent, selectable, light-candidate, and dirty indices incrementally. Final-frame live/equipped synchronization remains active-only, and active animated statics are synchronized from the scheduler's active workset rather than a resident-world scan. The production scene remains unconstructed and non-drawing. |
|
||||||
| F5 | active | Continuous lifecycle-automation shadow comparison is the next unit; no production consumer has switched. |
|
| F5 | active — connected gate | Lifecycle automation now constructs the otherwise-absent non-drawing scene, drains ordered static/live deltas after post-network spatial reconciliation, compares the accepted current partition every 30 rendered frames and at every canonical checkpoint, retains only the first field-level mismatch, and publishes scene/journal/index/memory evidence in checkpoint JSON. Focused and complete App suites are green; capped/uncapped nine-stop and dense-Arwic physical gates remain. |
|
||||||
| G0–G5 | pending | No production consumer has switched. |
|
| G0–G5 | pending | No production consumer has switched. |
|
||||||
|
|
||||||
The exact pre-F/G runtime rollback anchor remains `e7d9d6fa`. F0 is
|
The exact pre-F/G runtime rollback anchor remains `e7d9d6fa`. F0 is
|
||||||
|
|
|
||||||
|
|
@ -374,6 +374,20 @@ internal sealed class FrameRootCompositionPhase
|
||||||
&& d.Options.AutomationArtifactDirectory is not null
|
&& d.Options.AutomationArtifactDirectory is not null
|
||||||
? new CurrentRenderSceneOracle()
|
? new CurrentRenderSceneOracle()
|
||||||
: null;
|
: null;
|
||||||
|
if ((currentRenderSceneOracle is null)
|
||||||
|
!= (live.RenderSceneShadow is null))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Lifecycle automation must compose the current-path referee and shadow render scene together.");
|
||||||
|
}
|
||||||
|
RenderSceneShadowComparisonController? renderSceneShadowComparison =
|
||||||
|
currentRenderSceneOracle is not null
|
||||||
|
&& live.RenderSceneShadow is not null
|
||||||
|
? new RenderSceneShadowComparisonController(
|
||||||
|
live.RenderSceneShadow,
|
||||||
|
currentRenderSceneOracle,
|
||||||
|
message => d.Log("[UI-PROBE] " + message))
|
||||||
|
: null;
|
||||||
live.DrawDispatcher.SetCurrentRenderSceneObserver(
|
live.DrawDispatcher.SetCurrentRenderSceneObserver(
|
||||||
currentRenderSceneOracle);
|
currentRenderSceneOracle);
|
||||||
live.SelectionScene.SetCurrentRenderSceneObserver(
|
live.SelectionScene.SetCurrentRenderSceneObserver(
|
||||||
|
|
@ -421,7 +435,8 @@ internal sealed class FrameRootCompositionPhase
|
||||||
d.FrameProfiler,
|
d.FrameProfiler,
|
||||||
content.Dats,
|
content.Dats,
|
||||||
foundation.Residency,
|
foundation.Residency,
|
||||||
currentRenderSceneOracle);
|
currentRenderSceneOracle,
|
||||||
|
renderSceneShadowComparison);
|
||||||
lifecycleAutomation =
|
lifecycleAutomation =
|
||||||
new WorldLifecycleAutomationController(
|
new WorldLifecycleAutomationController(
|
||||||
() => session.WorldReveal.Snapshot,
|
() => session.WorldReveal.Snapshot,
|
||||||
|
|
@ -463,6 +478,14 @@ internal sealed class FrameRootCompositionPhase
|
||||||
renderFrameResources,
|
renderFrameResources,
|
||||||
settings.DevTools?.Presenter,
|
settings.DevTools?.Presenter,
|
||||||
renderWeatherFrame);
|
renderWeatherFrame);
|
||||||
|
IRenderFramePostDiagnosticsPhase postDiagnostics =
|
||||||
|
renderSceneShadowComparison is not null
|
||||||
|
&& lifecycleAutomation is not null
|
||||||
|
? new SerialRenderFramePostDiagnosticsPhase(
|
||||||
|
renderSceneShadowComparison,
|
||||||
|
lifecycleAutomation)
|
||||||
|
: (IRenderFramePostDiagnosticsPhase?)lifecycleAutomation
|
||||||
|
?? NullRenderFramePostDiagnosticsPhase.Instance;
|
||||||
var renderFrame = new RenderFrameOrchestrator(
|
var renderFrame = new RenderFrameOrchestrator(
|
||||||
host.GpuFrameFlights,
|
host.GpuFrameFlights,
|
||||||
new FrameProfilerGpuMeasurement(d.FrameProfiler, d.Gl),
|
new FrameProfilerGpuMeasurement(d.FrameProfiler, d.Gl),
|
||||||
|
|
@ -470,8 +493,7 @@ internal sealed class FrameRootCompositionPhase
|
||||||
worldSceneRenderer,
|
worldSceneRenderer,
|
||||||
privatePresentation,
|
privatePresentation,
|
||||||
live.FrameDiagnostics,
|
live.FrameDiagnostics,
|
||||||
(IRenderFramePostDiagnosticsPhase?)lifecycleAutomation
|
postDiagnostics,
|
||||||
?? NullRenderFramePostDiagnosticsPhase.Instance,
|
|
||||||
(IRenderFrameFailureRecovery?)settings.DevTools?.Presenter
|
(IRenderFrameFailureRecovery?)settings.DevTools?.Presenter
|
||||||
?? NullRenderFrameFailureRecovery.Instance);
|
?? NullRenderFrameFailureRecovery.Instance);
|
||||||
Fault(FrameRootCompositionPoint.RenderRootCreated);
|
Fault(FrameRootCompositionPoint.RenderRootCreated);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ using AcDream.App.Interaction;
|
||||||
using AcDream.App.Physics;
|
using AcDream.App.Physics;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.Rendering.Residency;
|
using AcDream.App.Rendering.Residency;
|
||||||
|
using AcDream.App.Rendering.Scene;
|
||||||
using AcDream.App.Rendering.Selection;
|
using AcDream.App.Rendering.Selection;
|
||||||
using AcDream.App.Rendering.Sky;
|
using AcDream.App.Rendering.Sky;
|
||||||
using AcDream.App.Rendering.Vfx;
|
using AcDream.App.Rendering.Vfx;
|
||||||
|
|
@ -80,6 +81,7 @@ internal sealed record LivePresentationResult(
|
||||||
RetailStaticAnimatingObjectScheduler StaticAnimationScheduler,
|
RetailStaticAnimatingObjectScheduler StaticAnimationScheduler,
|
||||||
WorldGenerationAvailabilityState WorldAvailability,
|
WorldGenerationAvailabilityState WorldAvailability,
|
||||||
GpuWorldState WorldState,
|
GpuWorldState WorldState,
|
||||||
|
RenderSceneShadowRuntime? RenderSceneShadow,
|
||||||
LiveEntityRuntime LiveEntities,
|
LiveEntityRuntime LiveEntities,
|
||||||
ProjectileController ProjectileController,
|
ProjectileController ProjectileController,
|
||||||
LiveEntityProjectionWithdrawalController ProjectionWithdrawal,
|
LiveEntityProjectionWithdrawalController ProjectionWithdrawal,
|
||||||
|
|
@ -196,6 +198,20 @@ internal sealed class LivePresentationCompositionPhase
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
CompositionAcquisitionScope.CompositionAcquisitionLease<
|
||||||
|
RenderSceneShadowRuntime>? renderSceneShadowLease = null;
|
||||||
|
if (interaction.RetainedUi?.Screenshots is not null
|
||||||
|
&& d.Options.AutomationArtifactDirectory is not null)
|
||||||
|
{
|
||||||
|
renderSceneShadowLease = scope.Acquire(
|
||||||
|
"shadow render scene",
|
||||||
|
() => new RenderSceneShadowRuntime(
|
||||||
|
RenderSceneGeneration.FromRaw(1)),
|
||||||
|
static value => value.Dispose());
|
||||||
|
}
|
||||||
|
RenderSceneShadowRuntime? renderSceneShadow =
|
||||||
|
renderSceneShadowLease?.Resource;
|
||||||
|
|
||||||
var componentLifecycle =
|
var componentLifecycle =
|
||||||
new DeferredLiveEntityRuntimeComponentLifecycle();
|
new DeferredLiveEntityRuntimeComponentLifecycle();
|
||||||
var wbSpawnAdapter = new LandblockSpawnAdapter(foundation.MeshAdapter);
|
var wbSpawnAdapter = new LandblockSpawnAdapter(foundation.MeshAdapter);
|
||||||
|
|
@ -333,20 +349,33 @@ internal sealed class LivePresentationCompositionPhase
|
||||||
"developer world-entity source",
|
"developer world-entity source",
|
||||||
devWorldEntities.BindOwned(worldState));
|
devWorldEntities.BindOwned(worldState));
|
||||||
}
|
}
|
||||||
liveEntities = new LiveEntityRuntime(
|
var resourceOwners =
|
||||||
worldState,
|
new List<CompositeLiveEntityResourceLifecycle.Owner>
|
||||||
new CompositeLiveEntityResourceLifecycle(
|
{
|
||||||
new(
|
new(
|
||||||
entity => _ = entitySpawnAdapter.OnCreate(entity),
|
entity => _ = entitySpawnAdapter.OnCreate(entity),
|
||||||
entity => _ = entitySpawnAdapter.OnRemove(entity)),
|
entity => _ = entitySpawnAdapter.OnRemove(entity)),
|
||||||
new(
|
new(
|
||||||
entityScriptActivator.OnCreate,
|
entityScriptActivator.OnCreate,
|
||||||
entityScriptActivator.OnRemove)),
|
entityScriptActivator.OnRemove),
|
||||||
|
};
|
||||||
|
if (renderSceneShadow is not null)
|
||||||
|
{
|
||||||
|
resourceOwners.Add(new(
|
||||||
|
renderSceneShadow.OnLiveResourceRegistered,
|
||||||
|
renderSceneShadow.OnLiveResourceUnregistered));
|
||||||
|
}
|
||||||
|
liveEntities = new LiveEntityRuntime(
|
||||||
|
worldState,
|
||||||
|
new CompositeLiveEntityResourceLifecycle(
|
||||||
|
[.. resourceOwners]),
|
||||||
componentLifecycle);
|
componentLifecycle);
|
||||||
var liveRuntimeLease = scope.Own(
|
var liveRuntimeLease = scope.Own(
|
||||||
"canonical live-entity runtime",
|
"canonical live-entity runtime",
|
||||||
liveEntities,
|
liveEntities,
|
||||||
static runtime => runtime.Clear());
|
static runtime => runtime.Clear());
|
||||||
|
LiveRenderProjectionJournal? liveRenderProjections =
|
||||||
|
renderSceneShadow?.BindLiveRuntime(liveEntities);
|
||||||
Fault(LivePresentationCompositionPoint.CanonicalRuntimeCreated);
|
Fault(LivePresentationCompositionPoint.CanonicalRuntimeCreated);
|
||||||
|
|
||||||
bindings.Adopt(
|
bindings.Adopt(
|
||||||
|
|
@ -376,6 +405,13 @@ internal sealed class LivePresentationCompositionPhase
|
||||||
liveEntities,
|
liveEntities,
|
||||||
wbVisibility,
|
wbVisibility,
|
||||||
"WB projection visibility");
|
"WB projection visibility");
|
||||||
|
if (liveRenderProjections is not null)
|
||||||
|
{
|
||||||
|
bindings.BindProjectionVisibility(
|
||||||
|
liveEntities,
|
||||||
|
liveRenderProjections.OnProjectionVisibilityChanged,
|
||||||
|
"shadow render projection visibility");
|
||||||
|
}
|
||||||
Action<LiveEntityRecord, bool> particleVisibility = (record, visible) =>
|
Action<LiveEntityRecord, bool> particleVisibility = (record, visible) =>
|
||||||
{
|
{
|
||||||
if (record.WorldEntity is { } entity)
|
if (record.WorldEntity is { } entity)
|
||||||
|
|
@ -511,6 +547,15 @@ internal sealed class LivePresentationCompositionPhase
|
||||||
bindings.BindProjectionPoseReady(
|
bindings.BindProjectionPoseReady(
|
||||||
equippedLease.Resource,
|
equippedLease.Resource,
|
||||||
lightsLease.Resource.OnAttachedPoseReady);
|
lightsLease.Resource.OnAttachedPoseReady);
|
||||||
|
if (liveRenderProjections is not null)
|
||||||
|
{
|
||||||
|
bindings.BindProjectionPoseReady(
|
||||||
|
equippedLease.Resource,
|
||||||
|
liveRenderProjections.OnProjectionPoseReady);
|
||||||
|
bindings.BindProjectionRemoved(
|
||||||
|
equippedLease.Resource,
|
||||||
|
liveRenderProjections.OnProjectionRemoved);
|
||||||
|
}
|
||||||
bindings.Adopt(
|
bindings.Adopt(
|
||||||
"entity-effect animation hooks",
|
"entity-effect animation hooks",
|
||||||
content.HookRegistrations.RegisterOwned(entityEffects));
|
content.HookRegistrations.RegisterOwned(entityEffects));
|
||||||
|
|
@ -529,6 +574,8 @@ internal sealed class LivePresentationCompositionPhase
|
||||||
staticAnimationScheduler,
|
staticAnimationScheduler,
|
||||||
worldAvailability,
|
worldAvailability,
|
||||||
worldState,
|
worldState,
|
||||||
|
renderSceneShadow,
|
||||||
|
renderSceneShadowLease,
|
||||||
liveEntities,
|
liveEntities,
|
||||||
projectileController,
|
projectileController,
|
||||||
projectionWithdrawal,
|
projectionWithdrawal,
|
||||||
|
|
@ -574,6 +621,9 @@ internal sealed class LivePresentationCompositionPhase
|
||||||
RetailStaticAnimatingObjectScheduler staticAnimationScheduler,
|
RetailStaticAnimatingObjectScheduler staticAnimationScheduler,
|
||||||
WorldGenerationAvailabilityState worldAvailability,
|
WorldGenerationAvailabilityState worldAvailability,
|
||||||
GpuWorldState worldState,
|
GpuWorldState worldState,
|
||||||
|
RenderSceneShadowRuntime? renderSceneShadow,
|
||||||
|
CompositionAcquisitionScope.CompositionAcquisitionLease<
|
||||||
|
RenderSceneShadowRuntime>? renderSceneShadowLease,
|
||||||
LiveEntityRuntime liveEntities,
|
LiveEntityRuntime liveEntities,
|
||||||
ProjectileController projectileController,
|
ProjectileController projectileController,
|
||||||
LiveEntityProjectionWithdrawalController projectionWithdrawal,
|
LiveEntityProjectionWithdrawalController projectionWithdrawal,
|
||||||
|
|
@ -818,7 +868,8 @@ internal sealed class LivePresentationCompositionPhase
|
||||||
worldState,
|
worldState,
|
||||||
landblockRetirementOwner,
|
landblockRetirementOwner,
|
||||||
landblockLoaded.OnLandblockLoaded,
|
landblockLoaded.OnLandblockLoaded,
|
||||||
landblockRenderPublisher.PrepareAfterRenderPins);
|
landblockRenderPublisher.PrepareAfterRenderPins,
|
||||||
|
renderSceneShadow?.StaticProjections);
|
||||||
Fault(LivePresentationCompositionPoint.LandblockPipelineCreated);
|
Fault(LivePresentationCompositionPoint.LandblockPipelineCreated);
|
||||||
|
|
||||||
var clipFrameLease = scope.Acquire(
|
var clipFrameLease = scope.Acquire(
|
||||||
|
|
@ -965,6 +1016,7 @@ internal sealed class LivePresentationCompositionPhase
|
||||||
staticAnimationScheduler,
|
staticAnimationScheduler,
|
||||||
worldAvailability,
|
worldAvailability,
|
||||||
worldState,
|
worldState,
|
||||||
|
renderSceneShadow,
|
||||||
liveEntities,
|
liveEntities,
|
||||||
projectileController,
|
projectileController,
|
||||||
projectionWithdrawal,
|
projectionWithdrawal,
|
||||||
|
|
@ -1010,6 +1062,7 @@ internal sealed class LivePresentationCompositionPhase
|
||||||
_publication.PublishLivePresentation(result);
|
_publication.PublishLivePresentation(result);
|
||||||
|
|
||||||
liveRuntimeLease.Transfer();
|
liveRuntimeLease.Transfer();
|
||||||
|
renderSceneShadowLease?.Transfer();
|
||||||
lightsLease.Transfer();
|
lightsLease.Transfer();
|
||||||
equippedLease.Transfer();
|
equippedLease.Transfer();
|
||||||
presentationLease.Transfer();
|
presentationLease.Transfer();
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using AcDream.App.Diagnostics;
|
||||||
using AcDream.App.Net;
|
using AcDream.App.Net;
|
||||||
using AcDream.App.Physics;
|
using AcDream.App.Physics;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.Rendering.Scene;
|
||||||
using AcDream.App.Rendering.Vfx;
|
using AcDream.App.Rendering.Vfx;
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
using AcDream.App.Settings;
|
using AcDream.App.Settings;
|
||||||
|
|
@ -454,7 +455,8 @@ internal sealed class SessionPlayerCompositionPhase
|
||||||
new LiveEntityReadyPublisher(
|
new LiveEntityReadyPublisher(
|
||||||
live.LiveEntities,
|
live.LiveEntities,
|
||||||
live.EntityEffects,
|
live.EntityEffects,
|
||||||
live.Presentation),
|
live.Presentation,
|
||||||
|
live.RenderSceneShadow?.LiveProjections),
|
||||||
originCoordinator,
|
originCoordinator,
|
||||||
networkUpdateBridge,
|
networkUpdateBridge,
|
||||||
localPhysicsTimestamps,
|
localPhysicsTimestamps,
|
||||||
|
|
@ -603,7 +605,9 @@ internal sealed class SessionPlayerCompositionPhase
|
||||||
live.EntityEffects,
|
live.EntityEffects,
|
||||||
live.EquippedChildren,
|
live.EquippedChildren,
|
||||||
content.ParticleSink,
|
content.ParticleSink,
|
||||||
live.Lights);
|
live.Lights,
|
||||||
|
live.RenderSceneShadow?.LiveProjections,
|
||||||
|
live.RenderSceneShadow);
|
||||||
var localPlayerAnimation = new LocalPlayerAnimationController(
|
var localPlayerAnimation = new LocalPlayerAnimationController(
|
||||||
live.LiveEntities,
|
live.LiveEntities,
|
||||||
d.PlayerIdentity,
|
d.PlayerIdentity,
|
||||||
|
|
@ -650,7 +654,9 @@ internal sealed class SessionPlayerCompositionPhase
|
||||||
live.AnimationPresenter,
|
live.AnimationPresenter,
|
||||||
d.AnimatedEntities,
|
d.AnimatedEntities,
|
||||||
live.EquippedChildren,
|
live.EquippedChildren,
|
||||||
liveEffectFrame);
|
liveEffectFrame,
|
||||||
|
live.RenderSceneShadow?.LiveProjections,
|
||||||
|
live.RenderSceneShadow?.StaticProjections);
|
||||||
Fault(SessionPlayerCompositionPoint.UpdateLeavesCreated);
|
Fault(SessionPlayerCompositionPoint.UpdateLeavesCreated);
|
||||||
|
|
||||||
var playerMode = new PlayerModeController(
|
var playerMode = new PlayerModeController(
|
||||||
|
|
@ -812,7 +818,8 @@ internal sealed class SessionPlayerCompositionPhase
|
||||||
live.EntityEffects,
|
live.EntityEffects,
|
||||||
content.AnimationHookFrames,
|
content.AnimationHookFrames,
|
||||||
live.Presentation,
|
live.Presentation,
|
||||||
d.RemoteMovementObservations),
|
d.RemoteMovementObservations,
|
||||||
|
live.RenderSceneShadow),
|
||||||
d.Log);
|
d.Log);
|
||||||
LiveSessionHost sessionHost = sessionRuntimeFactory.Create(liveSession);
|
LiveSessionHost sessionHost = sessionRuntimeFactory.Create(liveSession);
|
||||||
Fault(SessionPlayerCompositionPoint.SessionHostCreated);
|
Fault(SessionPlayerCompositionPoint.SessionHostCreated);
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ internal sealed record WorldLifecycleResourceSnapshot(
|
||||||
int LiveEntities,
|
int LiveEntities,
|
||||||
int MaterializedLiveEntities,
|
int MaterializedLiveEntities,
|
||||||
CurrentRenderSceneOracleSnapshot RenderSceneOracle,
|
CurrentRenderSceneOracleSnapshot RenderSceneOracle,
|
||||||
|
RenderSceneShadowComparisonSnapshot RenderSceneShadow,
|
||||||
int PendingLiveTeardowns,
|
int PendingLiveTeardowns,
|
||||||
int PendingLandblockRetirements,
|
int PendingLandblockRetirements,
|
||||||
int ParticleEmitters,
|
int ParticleEmitters,
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,8 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
||||||
private readonly ResidencyManager _residency;
|
private readonly ResidencyManager _residency;
|
||||||
private readonly ICurrentRenderSceneOracleSnapshotSource?
|
private readonly ICurrentRenderSceneOracleSnapshotSource?
|
||||||
_renderSceneOracle;
|
_renderSceneOracle;
|
||||||
|
private readonly IRenderSceneShadowSnapshotSource?
|
||||||
|
_renderSceneShadow;
|
||||||
|
|
||||||
public WorldLifecycleResourceSnapshotSource(
|
public WorldLifecycleResourceSnapshotSource(
|
||||||
GpuWorldState world,
|
GpuWorldState world,
|
||||||
|
|
@ -59,7 +61,8 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
||||||
FrameProfiler frameProfiler,
|
FrameProfiler frameProfiler,
|
||||||
IDatReaderWriter dats,
|
IDatReaderWriter dats,
|
||||||
ResidencyManager residency,
|
ResidencyManager residency,
|
||||||
ICurrentRenderSceneOracleSnapshotSource? renderSceneOracle = null)
|
ICurrentRenderSceneOracleSnapshotSource? renderSceneOracle = null,
|
||||||
|
IRenderSceneShadowSnapshotSource? renderSceneShadow = null)
|
||||||
{
|
{
|
||||||
_world = world ?? throw new ArgumentNullException(nameof(world));
|
_world = world ?? throw new ArgumentNullException(nameof(world));
|
||||||
_animations = animations
|
_animations = animations
|
||||||
|
|
@ -87,6 +90,7 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
||||||
_residency = residency
|
_residency = residency
|
||||||
?? throw new ArgumentNullException(nameof(residency));
|
?? throw new ArgumentNullException(nameof(residency));
|
||||||
_renderSceneOracle = renderSceneOracle;
|
_renderSceneOracle = renderSceneOracle;
|
||||||
|
_renderSceneShadow = renderSceneShadow;
|
||||||
}
|
}
|
||||||
|
|
||||||
public WorldLifecycleResourceSnapshot Capture(RenderFrameOutcome outcome)
|
public WorldLifecycleResourceSnapshot Capture(RenderFrameOutcome outcome)
|
||||||
|
|
@ -129,6 +133,9 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
||||||
MaterializedLiveEntities: _liveEntities.MaterializedCount,
|
MaterializedLiveEntities: _liveEntities.MaterializedCount,
|
||||||
RenderSceneOracle: _renderSceneOracle?.Snapshot
|
RenderSceneOracle: _renderSceneOracle?.Snapshot
|
||||||
?? CurrentRenderSceneOracleSnapshot.Disabled,
|
?? CurrentRenderSceneOracleSnapshot.Disabled,
|
||||||
|
RenderSceneShadow:
|
||||||
|
_renderSceneShadow?.CaptureCheckpointSnapshot()
|
||||||
|
?? RenderSceneShadowComparisonSnapshot.Disabled,
|
||||||
PendingLiveTeardowns: _liveEntities.PendingTeardownCount,
|
PendingLiveTeardowns: _liveEntities.PendingTeardownCount,
|
||||||
PendingLandblockRetirements: _streaming.PendingRetirementCount,
|
PendingLandblockRetirements: _streaming.PendingRetirementCount,
|
||||||
ParticleEmitters: _particles.ActiveEmitterCount,
|
ParticleEmitters: _particles.ActiveEmitterCount,
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ internal sealed class LiveSessionResetBindings
|
||||||
public required Action InboundEventFifo { get; init; }
|
public required Action InboundEventFifo { get; init; }
|
||||||
public required Action LiveLiveness { get; init; }
|
public required Action LiveLiveness { get; init; }
|
||||||
public required Action LiveRuntime { get; init; }
|
public required Action LiveRuntime { get; init; }
|
||||||
|
public required Action RenderSceneProjection { get; init; }
|
||||||
public required Action SessionIdentity { get; init; }
|
public required Action SessionIdentity { get; init; }
|
||||||
public required Action RemoteTeleport { get; init; }
|
public required Action RemoteTeleport { get; init; }
|
||||||
public required Action NetworkEffects { get; init; }
|
public required Action NetworkEffects { get; init; }
|
||||||
|
|
@ -81,6 +82,10 @@ internal static class LiveSessionResetManifest
|
||||||
new("inbound event fifo", bindings.InboundEventFifo),
|
new("inbound event fifo", bindings.InboundEventFifo),
|
||||||
new("live liveness", bindings.LiveLiveness),
|
new("live liveness", bindings.LiveLiveness),
|
||||||
new("live runtime", bindings.LiveRuntime),
|
new("live runtime", bindings.LiveRuntime),
|
||||||
|
// Canonical teardown appends exact projection removals. Drain them
|
||||||
|
// before identity changes so no prior-session scene record or
|
||||||
|
// journal entry can carry into the next character generation.
|
||||||
|
new("render scene projection", bindings.RenderSceneProjection),
|
||||||
// Identity must remain A until live teardown has converged so
|
// Identity must remain A until live teardown has converged so
|
||||||
// player-specific teardown can still classify the old record.
|
// player-specific teardown can still classify the old record.
|
||||||
new("session identity", bindings.SessionIdentity),
|
new("session identity", bindings.SessionIdentity),
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ using AcDream.App.Input;
|
||||||
using AcDream.App.Interaction;
|
using AcDream.App.Interaction;
|
||||||
using AcDream.App.Physics;
|
using AcDream.App.Physics;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.Rendering.Scene;
|
||||||
using AcDream.App.Rendering.Selection;
|
using AcDream.App.Rendering.Selection;
|
||||||
using AcDream.App.Rendering.Vfx;
|
using AcDream.App.Rendering.Vfx;
|
||||||
using AcDream.App.Settings;
|
using AcDream.App.Settings;
|
||||||
|
|
@ -86,7 +87,8 @@ internal sealed record LiveSessionWorldRuntime(
|
||||||
EntityEffectController EntityEffects,
|
EntityEffectController EntityEffects,
|
||||||
AnimationHookFrameQueue AnimationHookFrames,
|
AnimationHookFrameQueue AnimationHookFrames,
|
||||||
LiveEntityPresentationController Presentation,
|
LiveEntityPresentationController Presentation,
|
||||||
RemoteMovementObservationTracker RemoteMovementObservations);
|
RemoteMovementObservationTracker RemoteMovementObservations,
|
||||||
|
RenderSceneShadowRuntime? RenderSceneShadow);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Builds the exact per-generation route/reset graph for the canonical live
|
/// Builds the exact per-generation route/reset graph for the canonical live
|
||||||
|
|
@ -177,6 +179,8 @@ internal sealed class LiveSessionRuntimeFactory
|
||||||
InboundEventFifo = _world.InboundEvents.Clear,
|
InboundEventFifo = _world.InboundEvents.Clear,
|
||||||
LiveLiveness = _world.Liveness.Clear,
|
LiveLiveness = _world.Liveness.Clear,
|
||||||
LiveRuntime = ResetLiveRuntime,
|
LiveRuntime = ResetLiveRuntime,
|
||||||
|
RenderSceneProjection =
|
||||||
|
() => _world.RenderSceneShadow?.DrainUpdateBoundary(),
|
||||||
SessionIdentity = ResetIdentity,
|
SessionIdentity = ResetIdentity,
|
||||||
RemoteTeleport = _world.RemoteTeleport.Clear,
|
RemoteTeleport = _world.RemoteTeleport.Clear,
|
||||||
NetworkEffects = _world.EntityEffects.ClearNetworkState,
|
NetworkEffects = _world.EntityEffects.ClearNetworkState,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using AcDream.Core.Plugins;
|
using AcDream.Core.Plugins;
|
||||||
using AcDream.App.Composition;
|
using AcDream.App.Composition;
|
||||||
using AcDream.App.Physics;
|
using AcDream.App.Physics;
|
||||||
|
using AcDream.App.Rendering.Scene;
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
using AcDream.App.Settings;
|
using AcDream.App.Settings;
|
||||||
using AcDream.App.World;
|
using AcDream.App.World;
|
||||||
|
|
@ -57,6 +58,7 @@ public sealed class GameWindow :
|
||||||
private AcDream.App.Rendering.Wb.EntitySpawnAdapter? _wbEntitySpawnAdapter;
|
private AcDream.App.Rendering.Wb.EntitySpawnAdapter? _wbEntitySpawnAdapter;
|
||||||
private AcDream.App.Rendering.Vfx.EntityScriptActivator? _entityScriptActivator;
|
private AcDream.App.Rendering.Vfx.EntityScriptActivator? _entityScriptActivator;
|
||||||
private RetailStaticAnimatingObjectScheduler? _staticAnimationScheduler;
|
private RetailStaticAnimatingObjectScheduler? _staticAnimationScheduler;
|
||||||
|
private RenderSceneShadowRuntime? _renderSceneShadow;
|
||||||
private AcDream.App.Rendering.Wb.WbDrawDispatcher? _wbDrawDispatcher;
|
private AcDream.App.Rendering.Wb.WbDrawDispatcher? _wbDrawDispatcher;
|
||||||
private AcDream.App.Rendering.Selection.RetailSelectionScene? _retailSelectionScene;
|
private AcDream.App.Rendering.Selection.RetailSelectionScene? _retailSelectionScene;
|
||||||
private AcDream.App.Interaction.WorldSelectionQuery? _worldSelectionQuery;
|
private AcDream.App.Interaction.WorldSelectionQuery? _worldSelectionQuery;
|
||||||
|
|
@ -949,6 +951,7 @@ public sealed class GameWindow :
|
||||||
|| _wbEntitySpawnAdapter is not null
|
|| _wbEntitySpawnAdapter is not null
|
||||||
|| _entityScriptActivator is not null
|
|| _entityScriptActivator is not null
|
||||||
|| _staticAnimationScheduler is not null
|
|| _staticAnimationScheduler is not null
|
||||||
|
|| _renderSceneShadow is not null
|
||||||
|| _wbDrawDispatcher is not null
|
|| _wbDrawDispatcher is not null
|
||||||
|| _retailSelectionScene is not null
|
|| _retailSelectionScene is not null
|
||||||
|| _worldSelectionQuery is not null
|
|| _worldSelectionQuery is not null
|
||||||
|
|
@ -985,6 +988,7 @@ public sealed class GameWindow :
|
||||||
_entityScriptActivator = result.EntityScriptActivator;
|
_entityScriptActivator = result.EntityScriptActivator;
|
||||||
_staticAnimationScheduler = result.StaticAnimationScheduler;
|
_staticAnimationScheduler = result.StaticAnimationScheduler;
|
||||||
_worldState = result.WorldState;
|
_worldState = result.WorldState;
|
||||||
|
_renderSceneShadow = result.RenderSceneShadow;
|
||||||
_liveEntities = result.LiveEntities;
|
_liveEntities = result.LiveEntities;
|
||||||
_projectileController = result.ProjectileController;
|
_projectileController = result.ProjectileController;
|
||||||
_liveEntityProjectionWithdrawal = result.ProjectionWithdrawal;
|
_liveEntityProjectionWithdrawal = result.ProjectionWithdrawal;
|
||||||
|
|
@ -1589,6 +1593,7 @@ public sealed class GameWindow :
|
||||||
_streamer,
|
_streamer,
|
||||||
_equippedChildRenderer,
|
_equippedChildRenderer,
|
||||||
_liveEntities,
|
_liveEntities,
|
||||||
|
_renderSceneShadow,
|
||||||
_livePresentationBindings,
|
_livePresentationBindings,
|
||||||
_entityEffectAdvance,
|
_entityEffectAdvance,
|
||||||
_entityEffects,
|
_entityEffects,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ using AcDream.App.Input;
|
||||||
using AcDream.App.Net;
|
using AcDream.App.Net;
|
||||||
using AcDream.App.Physics;
|
using AcDream.App.Physics;
|
||||||
using AcDream.App.Rendering.Sky;
|
using AcDream.App.Rendering.Sky;
|
||||||
|
using AcDream.App.Rendering.Scene;
|
||||||
using AcDream.App.Rendering.Vfx;
|
using AcDream.App.Rendering.Vfx;
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
using AcDream.App.Settings;
|
using AcDream.App.Settings;
|
||||||
|
|
@ -79,6 +80,7 @@ internal sealed record LiveShutdownRoots(
|
||||||
LandblockStreamer? Streamer,
|
LandblockStreamer? Streamer,
|
||||||
EquippedChildRenderController? EquippedChildren,
|
EquippedChildRenderController? EquippedChildren,
|
||||||
LiveEntityRuntime? LiveEntities,
|
LiveEntityRuntime? LiveEntities,
|
||||||
|
RenderSceneShadowRuntime? RenderSceneShadow,
|
||||||
LivePresentationRuntimeBindings? PresentationBindings,
|
LivePresentationRuntimeBindings? PresentationBindings,
|
||||||
DeferredEntityEffectAdvanceSource EffectAdvance,
|
DeferredEntityEffectAdvanceSource EffectAdvance,
|
||||||
EntityEffectController? EntityEffects,
|
EntityEffectController? EntityEffects,
|
||||||
|
|
@ -377,6 +379,9 @@ internal static class GameWindowShutdownManifest
|
||||||
new ResourceShutdownStage("live entities",
|
new ResourceShutdownStage("live entities",
|
||||||
[
|
[
|
||||||
Hard("live entity runtime", () => live.LiveEntities?.Clear()),
|
Hard("live entity runtime", () => live.LiveEntities?.Clear()),
|
||||||
|
Hard(
|
||||||
|
"shadow render scene",
|
||||||
|
() => live.RenderSceneShadow?.Dispose()),
|
||||||
]),
|
]),
|
||||||
new ResourceShutdownStage("effect dispatch edges",
|
new ResourceShutdownStage("effect dispatch edges",
|
||||||
[
|
[
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,36 @@ internal sealed class NullRenderFramePostDiagnosticsPhase :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fixed-order post-diagnostic fan-out. The shadow referee must run before
|
||||||
|
/// lifecycle checkpoint publication so each artifact observes the comparison
|
||||||
|
/// made against that exact completed render frame.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class SerialRenderFramePostDiagnosticsPhase :
|
||||||
|
IRenderFramePostDiagnosticsPhase
|
||||||
|
{
|
||||||
|
private readonly IRenderFramePostDiagnosticsPhase[] _phases;
|
||||||
|
|
||||||
|
public SerialRenderFramePostDiagnosticsPhase(
|
||||||
|
params IRenderFramePostDiagnosticsPhase[] phases)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(phases);
|
||||||
|
if (phases.Length == 0 || phases.Any(static phase => phase is null))
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
"At least one non-null post-diagnostic phase is required.",
|
||||||
|
nameof(phases));
|
||||||
|
}
|
||||||
|
_phases = [.. phases];
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Process(RenderFrameInput input, RenderFrameOutcome outcome)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _phases.Length; i++)
|
||||||
|
_phases[i].Process(input, outcome);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Closes presentation transactions which may have opened before a later
|
/// Closes presentation transactions which may have opened before a later
|
||||||
/// render phase failed. Recovery is idempotent so failures before the
|
/// render phase failed. Recovery is idempotent so failures before the
|
||||||
|
|
|
||||||
|
|
@ -863,6 +863,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
hash.Add(record.Source.GeometryFingerprint.High);
|
hash.Add(record.Source.GeometryFingerprint.High);
|
||||||
hash.Add(record.Source.AppearanceFingerprint.Low);
|
hash.Add(record.Source.AppearanceFingerprint.Low);
|
||||||
hash.Add(record.Source.AppearanceFingerprint.High);
|
hash.Add(record.Source.AppearanceFingerprint.High);
|
||||||
|
hash.Add(record.Source.CurrentProjectionFlags);
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly record struct ProjectionIdentity(RenderProjectionId Id);
|
private readonly record struct ProjectionIdentity(RenderProjectionId Id);
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,8 @@ internal static class RenderProjectionRecordFactory
|
||||||
ownerLandblockId,
|
ownerLandblockId,
|
||||||
entity);
|
entity);
|
||||||
RenderProjectionFlags flags = RenderProjectionFlags.Selectable;
|
RenderProjectionFlags flags = RenderProjectionFlags.Selectable;
|
||||||
if (spatiallyVisible
|
if (entity.MeshRefs.Count > 0
|
||||||
|
&& spatiallyVisible
|
||||||
&& entity.IsDrawVisible
|
&& entity.IsDrawVisible
|
||||||
&& entity.IsAncestorDrawVisible)
|
&& entity.IsAncestorDrawVisible)
|
||||||
{
|
{
|
||||||
|
|
@ -76,7 +77,8 @@ internal static class RenderProjectionRecordFactory
|
||||||
entity.BuildingShellAnchorCellId ?? 0,
|
entity.BuildingShellAnchorCellId ?? 0,
|
||||||
fingerprint.Transform,
|
fingerprint.Transform,
|
||||||
fingerprint.Geometry,
|
fingerprint.Geometry,
|
||||||
fingerprint.Appearance));
|
fingerprint.Appearance,
|
||||||
|
fingerprint.Flags));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static (Vector3 Minimum, Vector3 Maximum) CalculateBounds(
|
private static (Vector3 Minimum, Vector3 Maximum) CalculateBounds(
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ internal readonly record struct RenderProjectionId
|
||||||
internal static RenderProjectionId FromRaw(ulong value) => new(value);
|
internal static RenderProjectionId FromRaw(ulong value) => new(value);
|
||||||
|
|
||||||
internal ulong RawValue => _value;
|
internal ulong RawValue => _value;
|
||||||
|
internal byte Domain => (byte)(_value >> 56);
|
||||||
internal RenderSortKey ToSortKey() => new(_value);
|
internal RenderSortKey ToSortKey() => new(_value);
|
||||||
|
|
||||||
public int CompareTo(RenderProjectionId other) =>
|
public int CompareTo(RenderProjectionId other) =>
|
||||||
|
|
@ -182,7 +183,8 @@ internal readonly record struct RenderSourceMetadata(
|
||||||
uint BuildingShellAnchorCellId,
|
uint BuildingShellAnchorCellId,
|
||||||
RenderSceneHash128 TransformFingerprint,
|
RenderSceneHash128 TransformFingerprint,
|
||||||
RenderSceneHash128 GeometryFingerprint,
|
RenderSceneHash128 GeometryFingerprint,
|
||||||
RenderSceneHash128 AppearanceFingerprint);
|
RenderSceneHash128 AppearanceFingerprint,
|
||||||
|
uint CurrentProjectionFlags = 0);
|
||||||
|
|
||||||
internal readonly record struct RenderProjectionRecord(
|
internal readonly record struct RenderProjectionRecord(
|
||||||
RenderProjectionId Id,
|
RenderProjectionId Id,
|
||||||
|
|
|
||||||
516
src/AcDream.App/Rendering/Scene/RenderSceneShadowRuntime.cs
Normal file
516
src/AcDream.App/Rendering/Scene/RenderSceneShadowRuntime.cs
Normal file
|
|
@ -0,0 +1,516 @@
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using AcDream.App.Rendering.Scene.Arch;
|
||||||
|
using AcDream.App.World;
|
||||||
|
using AcDream.Core.World;
|
||||||
|
|
||||||
|
namespace AcDream.App.Rendering.Scene;
|
||||||
|
|
||||||
|
internal readonly record struct RenderSceneShadowComparisonSnapshot(
|
||||||
|
bool Enabled,
|
||||||
|
ulong RenderFrameSequence,
|
||||||
|
ulong ComparisonCount,
|
||||||
|
ulong SuccessfulComparisonCount,
|
||||||
|
ulong ComparedOracleFrameSequence,
|
||||||
|
int MatchedProjectionCount,
|
||||||
|
long MismatchCount,
|
||||||
|
string? FirstMismatch,
|
||||||
|
int PendingDeltaCount,
|
||||||
|
ulong DrainSequence,
|
||||||
|
RenderDeltaApplyResult LastApply,
|
||||||
|
RenderDeltaApplyResult CumulativeApply,
|
||||||
|
RenderProjectionCounts SceneCounts,
|
||||||
|
RenderSceneIndexCounts SceneIndexCounts,
|
||||||
|
RenderSceneMemoryAccounting Memory,
|
||||||
|
RenderSceneDigest Digest)
|
||||||
|
{
|
||||||
|
public static RenderSceneShadowComparisonSnapshot Disabled { get; } =
|
||||||
|
new(
|
||||||
|
Enabled: false,
|
||||||
|
RenderFrameSequence: 0,
|
||||||
|
ComparisonCount: 0,
|
||||||
|
SuccessfulComparisonCount: 0,
|
||||||
|
ComparedOracleFrameSequence: 0,
|
||||||
|
MatchedProjectionCount: 0,
|
||||||
|
MismatchCount: 0,
|
||||||
|
FirstMismatch: null,
|
||||||
|
PendingDeltaCount: 0,
|
||||||
|
DrainSequence: 0,
|
||||||
|
LastApply: default,
|
||||||
|
CumulativeApply: default,
|
||||||
|
SceneCounts: default,
|
||||||
|
SceneIndexCounts: default,
|
||||||
|
Memory: default,
|
||||||
|
Digest: default);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface IRenderSceneShadowSnapshotSource
|
||||||
|
{
|
||||||
|
RenderSceneShadowComparisonSnapshot CaptureCheckpointSnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Diagnostic-only owner of Slice F's non-drawing render projection. The
|
||||||
|
/// normal runtime never constructs this type. Accepted world/live lifecycle
|
||||||
|
/// edges append to one ordered journal, and the update thread drains it only
|
||||||
|
/// after current-frame animation, network, and attachment reconciliation.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class RenderSceneShadowRuntime : IDisposable
|
||||||
|
{
|
||||||
|
private readonly ArchRenderScene _scene;
|
||||||
|
private readonly RenderProjectionJournal _journal;
|
||||||
|
private readonly RenderSceneDigestBuffer _digestBuffer = new();
|
||||||
|
private LiveRenderProjectionJournal? _live;
|
||||||
|
private RenderDeltaApplyResult _lastApply;
|
||||||
|
private RenderDeltaApplyResult _cumulativeApply;
|
||||||
|
private ulong _drainSequence;
|
||||||
|
private bool _disposed;
|
||||||
|
|
||||||
|
public RenderSceneShadowRuntime(RenderSceneGeneration generation)
|
||||||
|
{
|
||||||
|
_scene = new ArchRenderScene(generation);
|
||||||
|
_journal = new RenderProjectionJournal(generation);
|
||||||
|
StaticProjections = new StaticRenderProjectionJournal(_journal);
|
||||||
|
}
|
||||||
|
|
||||||
|
public StaticRenderProjectionJournal StaticProjections { get; }
|
||||||
|
|
||||||
|
public ILiveRenderProjectionSink? LiveProjections => _live;
|
||||||
|
|
||||||
|
public int PendingDeltaCount => _journal.Count;
|
||||||
|
|
||||||
|
public ulong DrainSequence => _drainSequence;
|
||||||
|
|
||||||
|
public RenderDeltaApplyResult LastApply => _lastApply;
|
||||||
|
|
||||||
|
public RenderDeltaApplyResult CumulativeApply => _cumulativeApply;
|
||||||
|
|
||||||
|
public RenderProjectionCounts Counts => _scene.Counts;
|
||||||
|
|
||||||
|
internal RenderProjectionJournal Journal => _journal;
|
||||||
|
|
||||||
|
internal RenderSceneQuery Query => _scene.OpenQuery();
|
||||||
|
|
||||||
|
public LiveRenderProjectionJournal BindLiveRuntime(
|
||||||
|
LiveEntityRuntime runtime)
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
ArgumentNullException.ThrowIfNull(runtime);
|
||||||
|
if (_live is not null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"The shadow render scene is already bound to a live runtime.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_live = new LiveRenderProjectionJournal(runtime, _journal);
|
||||||
|
return _live;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnLiveResourceRegistered(WorldEntity entity)
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
ArgumentNullException.ThrowIfNull(entity);
|
||||||
|
// EntityReady is the exact registration edge after all render,
|
||||||
|
// script, effect, and presentation owners have committed.
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnLiveResourceUnregistered(WorldEntity entity)
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
ArgumentNullException.ThrowIfNull(entity);
|
||||||
|
(_live ?? throw new InvalidOperationException(
|
||||||
|
"The shadow render scene was not bound before live teardown."))
|
||||||
|
.OnResourceUnregister(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RenderDeltaApplyResult DrainUpdateBoundary()
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
_lastApply = _journal.DrainTo(_scene);
|
||||||
|
_cumulativeApply = Add(_cumulativeApply, _lastApply);
|
||||||
|
_drainSequence = checked(_drainSequence + 1);
|
||||||
|
return _lastApply;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RenderSceneMemoryAccounting CaptureMemory()
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
RenderSceneMemoryAccounting memory = _scene.Memory;
|
||||||
|
return memory with
|
||||||
|
{
|
||||||
|
EstimatedJournalBufferBytes = checked(
|
||||||
|
(long)_journal.Capacity
|
||||||
|
* Unsafe.SizeOf<RenderProjectionDelta>()),
|
||||||
|
EstimatedSynchronizationSourceBytes = checked(
|
||||||
|
(long)(StaticProjections.ProjectionCount
|
||||||
|
+ (_live?.ProjectionCount ?? 0))
|
||||||
|
* Unsafe.SizeOf<RenderProjectionRecord>()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public RenderSceneDigest BuildDigest()
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
return _scene.BuildDigest(_digestBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearDirty()
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
_scene.ClearDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear(RenderSceneGeneration replacementGeneration)
|
||||||
|
{
|
||||||
|
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||||
|
_live?.ResetTracking();
|
||||||
|
StaticProjections.Clear(replacementGeneration);
|
||||||
|
_scene.Clear(replacementGeneration);
|
||||||
|
_lastApply = default;
|
||||||
|
_cumulativeApply = default;
|
||||||
|
_drainSequence = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
return;
|
||||||
|
_disposed = true;
|
||||||
|
_live?.ResetTracking();
|
||||||
|
_scene.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RenderDeltaApplyResult Add(
|
||||||
|
RenderDeltaApplyResult left,
|
||||||
|
RenderDeltaApplyResult right) =>
|
||||||
|
new(
|
||||||
|
Applied: checked(left.Applied + right.Applied),
|
||||||
|
Registered: checked(left.Registered + right.Registered),
|
||||||
|
Updated: checked(left.Updated + right.Updated),
|
||||||
|
Replaced: checked(left.Replaced + right.Replaced),
|
||||||
|
Unregistered: checked(left.Unregistered + right.Unregistered),
|
||||||
|
RejectedGeneration: checked(
|
||||||
|
left.RejectedGeneration + right.RejectedGeneration),
|
||||||
|
RejectedOutOfOrderSequence: checked(
|
||||||
|
left.RejectedOutOfOrderSequence
|
||||||
|
+ right.RejectedOutOfOrderSequence),
|
||||||
|
RejectedStaleIncarnation: checked(
|
||||||
|
left.RejectedStaleIncarnation
|
||||||
|
+ right.RejectedStaleIncarnation),
|
||||||
|
RejectedMissing: checked(
|
||||||
|
left.RejectedMissing + right.RejectedMissing));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Post-render referee between the exact current partition output and the
|
||||||
|
/// non-drawing incremental scene. It records only the first mismatch, so a
|
||||||
|
/// persistent fault cannot create an unbounded log storm.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class RenderSceneShadowComparisonController :
|
||||||
|
IRenderFramePostDiagnosticsPhase,
|
||||||
|
IRenderSceneShadowSnapshotSource
|
||||||
|
{
|
||||||
|
private const ulong ComparisonCadenceFrames = 30;
|
||||||
|
private const byte StaticProjectionDomain = 1;
|
||||||
|
private const byte EnvCellProjectionDomain = 2;
|
||||||
|
private const byte LiveProjectionDomain = 3;
|
||||||
|
|
||||||
|
private readonly RenderSceneShadowRuntime _shadow;
|
||||||
|
private readonly CurrentRenderSceneOracle _oracle;
|
||||||
|
private readonly Action<string>? _log;
|
||||||
|
private readonly List<RenderProjectionRecord> _sceneRecords = [];
|
||||||
|
private readonly HashSet<RenderProjectionId> _expectedIds = [];
|
||||||
|
private ulong _renderFrameSequence;
|
||||||
|
private ulong _comparisonCount;
|
||||||
|
private ulong _successfulComparisonCount;
|
||||||
|
private long _mismatchCount;
|
||||||
|
private string? _firstMismatch;
|
||||||
|
private int _matchedProjectionCount;
|
||||||
|
private ulong _comparedOracleFrameSequence;
|
||||||
|
|
||||||
|
public RenderSceneShadowComparisonController(
|
||||||
|
RenderSceneShadowRuntime shadow,
|
||||||
|
CurrentRenderSceneOracle oracle,
|
||||||
|
Action<string>? log = null)
|
||||||
|
{
|
||||||
|
_shadow = shadow ?? throw new ArgumentNullException(nameof(shadow));
|
||||||
|
_oracle = oracle ?? throw new ArgumentNullException(nameof(oracle));
|
||||||
|
_log = log;
|
||||||
|
Snapshot = BuildSnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
public RenderSceneShadowComparisonSnapshot Snapshot { get; private set; }
|
||||||
|
|
||||||
|
public void Process(RenderFrameInput input, RenderFrameOutcome outcome)
|
||||||
|
{
|
||||||
|
_renderFrameSequence = checked(_renderFrameSequence + 1);
|
||||||
|
if (outcome.World.NormalWorldDrawn
|
||||||
|
&& _renderFrameSequence % ComparisonCadenceFrames == 0)
|
||||||
|
{
|
||||||
|
Compare(force: false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Snapshot = BuildSnapshot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public RenderSceneShadowComparisonSnapshot CaptureCheckpointSnapshot()
|
||||||
|
{
|
||||||
|
Compare(force: true);
|
||||||
|
return Snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void CompareNow() => Compare(force: true);
|
||||||
|
|
||||||
|
private void Compare(bool force)
|
||||||
|
{
|
||||||
|
CurrentRenderSceneOracleSnapshot current = _oracle.Snapshot;
|
||||||
|
if (current.CompletedFrameSequence == 0
|
||||||
|
|| (!force
|
||||||
|
&& current.CompletedFrameSequence
|
||||||
|
== _comparedOracleFrameSequence))
|
||||||
|
{
|
||||||
|
Snapshot = BuildSnapshot();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_comparisonCount = checked(_comparisonCount + 1);
|
||||||
|
_comparedOracleFrameSequence = current.CompletedFrameSequence;
|
||||||
|
_matchedProjectionCount = 0;
|
||||||
|
string? mismatch = CompareCurrentProjection();
|
||||||
|
if (mismatch is null)
|
||||||
|
{
|
||||||
|
_successfulComparisonCount =
|
||||||
|
checked(_successfulComparisonCount + 1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_mismatchCount = checked(_mismatchCount + 1);
|
||||||
|
if (_firstMismatch is null)
|
||||||
|
{
|
||||||
|
_firstMismatch = mismatch;
|
||||||
|
_log?.Invoke(
|
||||||
|
"[render-scene-shadow] first mismatch: " + mismatch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_shadow.ClearDirty();
|
||||||
|
Snapshot = BuildSnapshot();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? CompareCurrentProjection()
|
||||||
|
{
|
||||||
|
if (_shadow.PendingDeltaCount != 0)
|
||||||
|
{
|
||||||
|
return $"sourceChannel=journal pendingDeltas={_shadow.PendingDeltaCount}";
|
||||||
|
}
|
||||||
|
if (_shadow.CumulativeApply.Rejected != 0)
|
||||||
|
{
|
||||||
|
return "sourceChannel=journal rejectedDeltas="
|
||||||
|
+ _shadow.CumulativeApply.Rejected;
|
||||||
|
}
|
||||||
|
|
||||||
|
RenderSceneQuery query = _shadow.Query;
|
||||||
|
_expectedIds.Clear();
|
||||||
|
IReadOnlyList<CurrentRenderProjectionFingerprint> expected =
|
||||||
|
_oracle.Projections;
|
||||||
|
for (int i = 0; i < expected.Count; i++)
|
||||||
|
{
|
||||||
|
CurrentRenderProjectionFingerprint fingerprint = expected[i];
|
||||||
|
RenderProjectionId id = ExpectedId(in fingerprint);
|
||||||
|
_expectedIds.Add(id);
|
||||||
|
if (!query.TryGet(id, out RenderProjectionRecord actual))
|
||||||
|
{
|
||||||
|
return $"sourceChannel={Channel(id)} id={id} field=presence "
|
||||||
|
+ "expected=present actual=missing";
|
||||||
|
}
|
||||||
|
|
||||||
|
string? mismatch = CompareProjection(
|
||||||
|
in fingerprint,
|
||||||
|
in actual);
|
||||||
|
if (mismatch is not null)
|
||||||
|
return mismatch;
|
||||||
|
_matchedProjectionCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
int total = query.Counts.Total;
|
||||||
|
if (_sceneRecords.Capacity < total)
|
||||||
|
_sceneRecords.Capacity = total;
|
||||||
|
CollectionsMarshal.SetCount(_sceneRecords, total);
|
||||||
|
int copied = query.CopyTo(CollectionsMarshal.AsSpan(_sceneRecords));
|
||||||
|
if (copied != total)
|
||||||
|
{
|
||||||
|
return $"sourceChannel=scene field=count expected={total} "
|
||||||
|
+ $"actual={copied}";
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < copied; i++)
|
||||||
|
{
|
||||||
|
RenderProjectionRecord record = _sceneRecords[i];
|
||||||
|
if (_expectedIds.Contains(record.Id)
|
||||||
|
|| Domain(record.Id) == EnvCellProjectionDomain)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool indoorStatic =
|
||||||
|
record.ProjectionClass
|
||||||
|
is RenderProjectionClass.IndoorCellStatic
|
||||||
|
|| (record.ProjectionClass
|
||||||
|
is RenderProjectionClass.ActiveAnimatedStatic
|
||||||
|
&& InteriorEntityPartition.IsIndoorCellId(
|
||||||
|
record.Source.ParentCellId));
|
||||||
|
if (indoorStatic
|
||||||
|
|| (record.Flags & RenderProjectionFlags.Draw) == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"sourceChannel={Channel(record.Id)} id={record.Id} "
|
||||||
|
+ "field=presence expected=absent actual=drawn";
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? CompareProjection(
|
||||||
|
in CurrentRenderProjectionFingerprint expected,
|
||||||
|
in RenderProjectionRecord actual)
|
||||||
|
{
|
||||||
|
RenderProjectionId id = actual.Id;
|
||||||
|
string channel = Channel(id);
|
||||||
|
string? classMismatch = CompareClass(
|
||||||
|
expected.ProjectionClass,
|
||||||
|
in actual);
|
||||||
|
if (classMismatch is not null)
|
||||||
|
return Prefix(channel, id, "projectionClass", classMismatch);
|
||||||
|
if (actual.Residency.OwnerLandblockId != expected.LandblockId)
|
||||||
|
{
|
||||||
|
return Prefix(
|
||||||
|
channel,
|
||||||
|
id,
|
||||||
|
"landblock",
|
||||||
|
$"{expected.LandblockId:X8}/{actual.Residency.OwnerLandblockId:X8}");
|
||||||
|
}
|
||||||
|
if (actual.Source.LocalEntityId != expected.EntityId)
|
||||||
|
return Values(channel, id, "entityId", expected.EntityId, actual.Source.LocalEntityId);
|
||||||
|
if (actual.Source.ServerGuid != expected.ServerGuid)
|
||||||
|
return Values(channel, id, "serverGuid", expected.ServerGuid, actual.Source.ServerGuid);
|
||||||
|
if (actual.Source.SourceId != expected.SourceId)
|
||||||
|
return Values(channel, id, "sourceId", expected.SourceId, actual.Source.SourceId);
|
||||||
|
if (actual.Source.ParentCellId != expected.ParentCellId)
|
||||||
|
return Values(channel, id, "parentCell", expected.ParentCellId, actual.Source.ParentCellId);
|
||||||
|
if (actual.Source.EffectCellId != expected.EffectCellId)
|
||||||
|
return Values(channel, id, "effectCell", expected.EffectCellId, actual.Source.EffectCellId);
|
||||||
|
if (actual.Source.BuildingShellAnchorCellId
|
||||||
|
!= expected.BuildingShellAnchorCellId)
|
||||||
|
{
|
||||||
|
return Values(
|
||||||
|
channel,
|
||||||
|
id,
|
||||||
|
"buildingShellAnchor",
|
||||||
|
expected.BuildingShellAnchorCellId,
|
||||||
|
actual.Source.BuildingShellAnchorCellId);
|
||||||
|
}
|
||||||
|
if (actual.Source.CurrentProjectionFlags != expected.Flags)
|
||||||
|
return Values(channel, id, "flags", expected.Flags, actual.Source.CurrentProjectionFlags);
|
||||||
|
if (actual.MeshSet.MeshCount != expected.MeshCount)
|
||||||
|
return Values(channel, id, "meshCount", expected.MeshCount, actual.MeshSet.MeshCount);
|
||||||
|
if (actual.Source.TransformFingerprint != expected.Transform)
|
||||||
|
return Prefix(channel, id, "transform", $"{expected.Transform}/{actual.Source.TransformFingerprint}");
|
||||||
|
if (actual.Source.GeometryFingerprint != expected.Geometry)
|
||||||
|
return Prefix(channel, id, "geometry", $"{expected.Geometry}/{actual.Source.GeometryFingerprint}");
|
||||||
|
if (actual.Source.AppearanceFingerprint != expected.Appearance)
|
||||||
|
return Prefix(channel, id, "appearance", $"{expected.Appearance}/{actual.Source.AppearanceFingerprint}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? CompareClass(
|
||||||
|
InteriorEntityPartition.ProjectionClass expected,
|
||||||
|
in RenderProjectionRecord actual)
|
||||||
|
{
|
||||||
|
bool matches = expected switch
|
||||||
|
{
|
||||||
|
InteriorEntityPartition.ProjectionClass.OutdoorStatic =>
|
||||||
|
actual.ProjectionClass
|
||||||
|
is RenderProjectionClass.OutdoorStatic
|
||||||
|
or RenderProjectionClass.ActiveAnimatedStatic
|
||||||
|
&& !InteriorEntityPartition.IsIndoorCellId(
|
||||||
|
actual.Source.ParentCellId),
|
||||||
|
InteriorEntityPartition.ProjectionClass.CellStatic =>
|
||||||
|
actual.ProjectionClass
|
||||||
|
is RenderProjectionClass.IndoorCellStatic
|
||||||
|
or RenderProjectionClass.ActiveAnimatedStatic
|
||||||
|
&& InteriorEntityPartition.IsIndoorCellId(
|
||||||
|
actual.Source.ParentCellId),
|
||||||
|
InteriorEntityPartition.ProjectionClass.Dynamic =>
|
||||||
|
actual.ProjectionClass
|
||||||
|
is RenderProjectionClass.LiveDynamicRoot
|
||||||
|
or RenderProjectionClass.EquippedChild,
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
return matches
|
||||||
|
? null
|
||||||
|
: $"{expected}/{actual.ProjectionClass}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private RenderSceneShadowComparisonSnapshot BuildSnapshot()
|
||||||
|
{
|
||||||
|
RenderSceneQuery query = _shadow.Query;
|
||||||
|
return new RenderSceneShadowComparisonSnapshot(
|
||||||
|
Enabled: true,
|
||||||
|
RenderFrameSequence: _renderFrameSequence,
|
||||||
|
ComparisonCount: _comparisonCount,
|
||||||
|
SuccessfulComparisonCount: _successfulComparisonCount,
|
||||||
|
ComparedOracleFrameSequence: _comparedOracleFrameSequence,
|
||||||
|
MatchedProjectionCount: _matchedProjectionCount,
|
||||||
|
MismatchCount: _mismatchCount,
|
||||||
|
FirstMismatch: _firstMismatch,
|
||||||
|
PendingDeltaCount: _shadow.PendingDeltaCount,
|
||||||
|
DrainSequence: _shadow.DrainSequence,
|
||||||
|
LastApply: _shadow.LastApply,
|
||||||
|
CumulativeApply: _shadow.CumulativeApply,
|
||||||
|
SceneCounts: query.Counts,
|
||||||
|
SceneIndexCounts: query.IndexCounts,
|
||||||
|
Memory: _shadow.CaptureMemory(),
|
||||||
|
Digest: _shadow.BuildDigest());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RenderProjectionId ExpectedId(
|
||||||
|
in CurrentRenderProjectionFingerprint fingerprint) =>
|
||||||
|
fingerprint.ServerGuid == 0
|
||||||
|
? StaticRenderProjectionJournal.StaticEntityId(
|
||||||
|
fingerprint.LandblockId,
|
||||||
|
fingerprint.EntityId)
|
||||||
|
: LiveRenderProjectionJournal.ProjectionId(
|
||||||
|
fingerprint.EntityId);
|
||||||
|
|
||||||
|
private static byte Domain(RenderProjectionId id) =>
|
||||||
|
id.Domain;
|
||||||
|
|
||||||
|
private static string Channel(RenderProjectionId id) =>
|
||||||
|
Domain(id) switch
|
||||||
|
{
|
||||||
|
StaticProjectionDomain => "static",
|
||||||
|
EnvCellProjectionDomain => "envCell",
|
||||||
|
LiveProjectionDomain => "live",
|
||||||
|
byte domain => $"unknown:{domain}",
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string Values<T>(
|
||||||
|
string channel,
|
||||||
|
RenderProjectionId id,
|
||||||
|
string field,
|
||||||
|
T expected,
|
||||||
|
T actual) =>
|
||||||
|
Prefix(channel, id, field, $"{expected}/{actual}");
|
||||||
|
|
||||||
|
private static string Prefix(
|
||||||
|
string channel,
|
||||||
|
RenderProjectionId id,
|
||||||
|
string field,
|
||||||
|
string values) =>
|
||||||
|
$"sourceChannel={channel} id={id} field={field} "
|
||||||
|
+ $"expected/actual={values}";
|
||||||
|
}
|
||||||
|
|
@ -238,13 +238,15 @@ internal sealed class LiveSpatialPresentationReconciler : ILiveSpatialReconcileP
|
||||||
private readonly ParticleHookSink _particleSink;
|
private readonly ParticleHookSink _particleSink;
|
||||||
private readonly LiveEntityLightController _lights;
|
private readonly LiveEntityLightController _lights;
|
||||||
private readonly ILiveRenderProjectionSink? _renderProjections;
|
private readonly ILiveRenderProjectionSink? _renderProjections;
|
||||||
|
private readonly RenderSceneShadowRuntime? _renderSceneShadow;
|
||||||
|
|
||||||
public LiveSpatialPresentationReconciler(
|
public LiveSpatialPresentationReconciler(
|
||||||
EntityEffectController entityEffects,
|
EntityEffectController entityEffects,
|
||||||
EquippedChildRenderController equippedChildren,
|
EquippedChildRenderController equippedChildren,
|
||||||
ParticleHookSink particleSink,
|
ParticleHookSink particleSink,
|
||||||
LiveEntityLightController lights,
|
LiveEntityLightController lights,
|
||||||
ILiveRenderProjectionSink? renderProjections = null)
|
ILiveRenderProjectionSink? renderProjections = null,
|
||||||
|
RenderSceneShadowRuntime? renderSceneShadow = null)
|
||||||
{
|
{
|
||||||
_entityEffects = entityEffects ?? throw new ArgumentNullException(nameof(entityEffects));
|
_entityEffects = entityEffects ?? throw new ArgumentNullException(nameof(entityEffects));
|
||||||
_equippedChildren = equippedChildren
|
_equippedChildren = equippedChildren
|
||||||
|
|
@ -252,6 +254,7 @@ internal sealed class LiveSpatialPresentationReconciler : ILiveSpatialReconcileP
|
||||||
_particleSink = particleSink ?? throw new ArgumentNullException(nameof(particleSink));
|
_particleSink = particleSink ?? throw new ArgumentNullException(nameof(particleSink));
|
||||||
_lights = lights ?? throw new ArgumentNullException(nameof(lights));
|
_lights = lights ?? throw new ArgumentNullException(nameof(lights));
|
||||||
_renderProjections = renderProjections;
|
_renderProjections = renderProjections;
|
||||||
|
_renderSceneShadow = renderSceneShadow;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Reconcile()
|
public void Reconcile()
|
||||||
|
|
@ -261,5 +264,6 @@ internal sealed class LiveSpatialPresentationReconciler : ILiveSpatialReconcileP
|
||||||
_renderProjections?.SynchronizeActiveSources();
|
_renderProjections?.SynchronizeActiveSources();
|
||||||
_particleSink.RefreshAttachedEmitters();
|
_particleSink.RefreshAttachedEmitters();
|
||||||
_lights.Refresh();
|
_lights.Refresh();
|
||||||
|
_renderSceneShadow?.DrainUpdateBoundary();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,12 +38,14 @@ public sealed class FrameRootCompositionTests
|
||||||
AssertAppearsInOrder(
|
AssertAppearsInOrder(
|
||||||
source,
|
source,
|
||||||
"new RenderFrameResourceController(",
|
"new RenderFrameResourceController(",
|
||||||
|
"new RenderSceneShadowComparisonController(",
|
||||||
"new WorldSceneRenderer(",
|
"new WorldSceneRenderer(",
|
||||||
"new WorldLifecycleAutomationController(",
|
"new WorldLifecycleAutomationController(",
|
||||||
"\"world lifecycle automation owner\"",
|
"\"world lifecycle automation owner\"",
|
||||||
"\"world lifecycle automation binding\"",
|
"\"world lifecycle automation binding\"",
|
||||||
|
"new SerialRenderFramePostDiagnosticsPhase(",
|
||||||
"new RenderFrameOrchestrator(",
|
"new RenderFrameOrchestrator(",
|
||||||
"(IRenderFramePostDiagnosticsPhase?)lifecycleAutomation",
|
"postDiagnostics,",
|
||||||
"new RetailLiveFrameCoordinator(",
|
"new RetailLiveFrameCoordinator(",
|
||||||
"new UpdateFrameOrchestrator(",
|
"new UpdateFrameOrchestrator(",
|
||||||
"d.FrameGraphs.PublishOwned(",
|
"d.FrameGraphs.PublishOwned(",
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,15 @@ public sealed class WorldLifecycleAutomationControllerTests
|
||||||
AbortedSelectionFrames: 0,
|
AbortedSelectionFrames: 0,
|
||||||
SelectionPartCount: 7,
|
SelectionPartCount: 7,
|
||||||
SelectionDigest: new RenderSceneHash128(15, 16)),
|
SelectionDigest: new RenderSceneHash128(15, 16)),
|
||||||
|
RenderSceneShadow =
|
||||||
|
RenderSceneShadowComparisonSnapshot.Disabled with
|
||||||
|
{
|
||||||
|
Enabled = true,
|
||||||
|
ComparisonCount = 19,
|
||||||
|
SuccessfulComparisonCount = 19,
|
||||||
|
ComparedOracleFrameSequence = 19,
|
||||||
|
MatchedProjectionCount = 73,
|
||||||
|
},
|
||||||
TrackedGpuBytes = 1234,
|
TrackedGpuBytes = 1234,
|
||||||
Residency = new ResidencySnapshot(
|
Residency = new ResidencySnapshot(
|
||||||
[
|
[
|
||||||
|
|
@ -214,6 +223,20 @@ public sealed class WorldLifecycleAutomationControllerTests
|
||||||
0xFEDCBA9876543210uL,
|
0xFEDCBA9876543210uL,
|
||||||
renderSceneOracle.GetProperty("digest").GetProperty("high")
|
renderSceneOracle.GetProperty("digest").GetProperty("high")
|
||||||
.GetUInt64());
|
.GetUInt64());
|
||||||
|
JsonElement renderSceneShadow = json.RootElement
|
||||||
|
.GetProperty("resources")
|
||||||
|
.GetProperty("renderSceneShadow");
|
||||||
|
Assert.True(renderSceneShadow.GetProperty("enabled").GetBoolean());
|
||||||
|
Assert.Equal(
|
||||||
|
19,
|
||||||
|
renderSceneShadow.GetProperty("comparisonCount").GetInt64());
|
||||||
|
Assert.Equal(
|
||||||
|
73,
|
||||||
|
renderSceneShadow.GetProperty("matchedProjectionCount")
|
||||||
|
.GetInt32());
|
||||||
|
Assert.Equal(
|
||||||
|
0,
|
||||||
|
renderSceneShadow.GetProperty("mismatchCount").GetInt64());
|
||||||
JsonElement residency = json.RootElement
|
JsonElement residency = json.RootElement
|
||||||
.GetProperty("resources")
|
.GetProperty("resources")
|
||||||
.GetProperty("residency");
|
.GetProperty("residency");
|
||||||
|
|
@ -353,6 +376,7 @@ public sealed class WorldLifecycleAutomationControllerTests
|
||||||
|
|
||||||
private static WorldLifecycleResourceSnapshot EmptyResources() => new(
|
private static WorldLifecycleResourceSnapshot EmptyResources() => new(
|
||||||
0, 0, 0, 0, 0, 0, 0, CurrentRenderSceneOracleSnapshot.Disabled,
|
0, 0, 0, 0, 0, 0, 0, CurrentRenderSceneOracleSnapshot.Disabled,
|
||||||
|
RenderSceneShadowComparisonSnapshot.Disabled,
|
||||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||||
0, 0, 0L, 0, 0L, 0L, 0L, 0L, 0, 0, 0, 0, 0, 0, 0, 0L, 0L,
|
0, 0, 0L, 0, 0L, 0L, 0L, 0L, 0, 0, 0, 0, 0, 0, 0, 0L, 0L,
|
||||||
0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L,
|
0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L,
|
||||||
|
|
|
||||||
|
|
@ -269,6 +269,7 @@ public sealed class LiveSessionHostTests
|
||||||
InboundEventFifo = noop,
|
InboundEventFifo = noop,
|
||||||
LiveLiveness = noop,
|
LiveLiveness = noop,
|
||||||
LiveRuntime = noop,
|
LiveRuntime = noop,
|
||||||
|
RenderSceneProjection = noop,
|
||||||
SessionIdentity = noop,
|
SessionIdentity = noop,
|
||||||
RemoteTeleport = noop,
|
RemoteTeleport = noop,
|
||||||
NetworkEffects = noop,
|
NetworkEffects = noop,
|
||||||
|
|
|
||||||
|
|
@ -298,6 +298,7 @@ public sealed class LiveSessionResetPlanTests
|
||||||
LiveRuntime = Stage(
|
LiveRuntime = Stage(
|
||||||
"live runtime",
|
"live runtime",
|
||||||
() => LiveSessionEntityRuntimeReset.ClearAndRequireConvergence(live)),
|
() => LiveSessionEntityRuntimeReset.ClearAndRequireConvergence(live)),
|
||||||
|
RenderSceneProjection = Stage("render scene projection"),
|
||||||
SessionIdentity = Stage("session identity", () =>
|
SessionIdentity = Stage("session identity", () =>
|
||||||
{
|
{
|
||||||
LiveSessionEntityRuntimeReset.RequireConvergence(live);
|
LiveSessionEntityRuntimeReset.RequireConvergence(live);
|
||||||
|
|
@ -378,6 +379,7 @@ public sealed class LiveSessionResetPlanTests
|
||||||
"inbound event fifo",
|
"inbound event fifo",
|
||||||
"live liveness",
|
"live liveness",
|
||||||
"live runtime",
|
"live runtime",
|
||||||
|
"render scene projection",
|
||||||
"session identity",
|
"session identity",
|
||||||
"remote teleport",
|
"remote teleport",
|
||||||
"network effects",
|
"network effects",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,224 @@
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.Rendering.Scene;
|
||||||
|
using AcDream.Core.World;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.Rendering;
|
||||||
|
|
||||||
|
public sealed class RenderSceneShadowRuntimeTests
|
||||||
|
{
|
||||||
|
private const uint Landblock = 0xA9B4FFFF;
|
||||||
|
private const uint Cell = 0xA9B40170;
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ExactCurrentProjection_MatchesAndAcknowledgesDirtyWorkset()
|
||||||
|
{
|
||||||
|
WorldEntity entity = Entity(17, serverGuid: 0, parentCell: null);
|
||||||
|
CurrentRenderSceneOracle oracle = Capture(
|
||||||
|
entity,
|
||||||
|
visibleCells: []);
|
||||||
|
using var shadow = new RenderSceneShadowRuntime(Generation(1));
|
||||||
|
Register(shadow, entity, RenderProjectionClass.OutdoorStatic);
|
||||||
|
Assert.Equal(1, shadow.DrainUpdateBoundary().Registered);
|
||||||
|
|
||||||
|
var controller = new RenderSceneShadowComparisonController(
|
||||||
|
shadow,
|
||||||
|
oracle);
|
||||||
|
RenderSceneShadowComparisonSnapshot snapshot =
|
||||||
|
controller.CaptureCheckpointSnapshot();
|
||||||
|
|
||||||
|
Assert.True(snapshot.Enabled);
|
||||||
|
Assert.Equal(1uL, snapshot.ComparisonCount);
|
||||||
|
Assert.Equal(1uL, snapshot.SuccessfulComparisonCount);
|
||||||
|
Assert.Equal(0, snapshot.MismatchCount);
|
||||||
|
Assert.Equal(1, snapshot.MatchedProjectionCount);
|
||||||
|
Assert.Equal(0, snapshot.PendingDeltaCount);
|
||||||
|
Assert.Equal(0, snapshot.SceneIndexCounts.Dirty);
|
||||||
|
Assert.Equal(1, snapshot.SceneCounts.Total);
|
||||||
|
Assert.True(snapshot.Memory.TotalEstimatedBytes > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void FirstExactMismatch_IsNamedAndLoggedOnlyOnce()
|
||||||
|
{
|
||||||
|
WorldEntity entity = Entity(18, serverGuid: 0, parentCell: null);
|
||||||
|
CurrentRenderSceneOracle oracle = Capture(entity, []);
|
||||||
|
using var shadow = new RenderSceneShadowRuntime(Generation(1));
|
||||||
|
RenderProjectionRecord record = Project(
|
||||||
|
entity,
|
||||||
|
RenderProjectionClass.OutdoorStatic) with
|
||||||
|
{
|
||||||
|
Source = Project(
|
||||||
|
entity,
|
||||||
|
RenderProjectionClass.OutdoorStatic).Source with
|
||||||
|
{
|
||||||
|
TransformFingerprint = new RenderSceneHash128(91, 92),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
shadow.Journal.Register(in record);
|
||||||
|
shadow.DrainUpdateBoundary();
|
||||||
|
var logs = new List<string>();
|
||||||
|
var controller = new RenderSceneShadowComparisonController(
|
||||||
|
shadow,
|
||||||
|
oracle,
|
||||||
|
logs.Add);
|
||||||
|
|
||||||
|
controller.CompareNow();
|
||||||
|
controller.CompareNow();
|
||||||
|
RenderSceneShadowComparisonSnapshot snapshot = controller.Snapshot;
|
||||||
|
|
||||||
|
Assert.Equal(2uL, snapshot.ComparisonCount);
|
||||||
|
Assert.Equal(0uL, snapshot.SuccessfulComparisonCount);
|
||||||
|
Assert.Equal(2, snapshot.MismatchCount);
|
||||||
|
Assert.Contains("sourceChannel=static", snapshot.FirstMismatch);
|
||||||
|
Assert.Contains("field=transform", snapshot.FirstMismatch);
|
||||||
|
Assert.Single(logs);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NonFloodedIndoorStatic_IsAValidRetainedSceneExtra()
|
||||||
|
{
|
||||||
|
WorldEntity entity = Entity(19, serverGuid: 0, parentCell: Cell);
|
||||||
|
CurrentRenderSceneOracle oracle = Capture(entity, []);
|
||||||
|
Assert.Empty(oracle.Projections);
|
||||||
|
using var shadow = new RenderSceneShadowRuntime(Generation(1));
|
||||||
|
Register(shadow, entity, RenderProjectionClass.IndoorCellStatic);
|
||||||
|
shadow.DrainUpdateBoundary();
|
||||||
|
var controller = new RenderSceneShadowComparisonController(
|
||||||
|
shadow,
|
||||||
|
oracle);
|
||||||
|
|
||||||
|
controller.CompareNow();
|
||||||
|
|
||||||
|
Assert.Equal(1uL, controller.Snapshot.SuccessfulComparisonCount);
|
||||||
|
Assert.Equal(0, controller.Snapshot.MismatchCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DrawnOutdoorExtra_IsReportedAsStaleProjection()
|
||||||
|
{
|
||||||
|
WorldEntity expected = Entity(20, serverGuid: 0, parentCell: null);
|
||||||
|
WorldEntity extra = Entity(21, serverGuid: 0, parentCell: null);
|
||||||
|
CurrentRenderSceneOracle oracle = Capture(expected, []);
|
||||||
|
using var shadow = new RenderSceneShadowRuntime(Generation(1));
|
||||||
|
Register(shadow, expected, RenderProjectionClass.OutdoorStatic);
|
||||||
|
Register(shadow, extra, RenderProjectionClass.OutdoorStatic);
|
||||||
|
shadow.DrainUpdateBoundary();
|
||||||
|
var controller = new RenderSceneShadowComparisonController(
|
||||||
|
shadow,
|
||||||
|
oracle);
|
||||||
|
|
||||||
|
controller.CompareNow();
|
||||||
|
|
||||||
|
Assert.Equal(1, controller.Snapshot.MismatchCount);
|
||||||
|
Assert.Contains("expected=absent actual=drawn", controller.Snapshot.FirstMismatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RejectedDelta_IsAComparisonFailureEvenWhenSceneIsEmpty()
|
||||||
|
{
|
||||||
|
CurrentRenderSceneOracle oracle = CaptureNoEntities();
|
||||||
|
using var shadow = new RenderSceneShadowRuntime(Generation(1));
|
||||||
|
WorldEntity missing = Entity(22, serverGuid: 0, parentCell: null);
|
||||||
|
RenderProjectionRecord record = Project(
|
||||||
|
missing,
|
||||||
|
RenderProjectionClass.OutdoorStatic);
|
||||||
|
shadow.Journal.Update(
|
||||||
|
RenderProjectionDeltaKind.UpdateTransform,
|
||||||
|
in record);
|
||||||
|
RenderDeltaApplyResult apply = shadow.DrainUpdateBoundary();
|
||||||
|
Assert.Equal(1, apply.RejectedMissing);
|
||||||
|
var controller = new RenderSceneShadowComparisonController(
|
||||||
|
shadow,
|
||||||
|
oracle);
|
||||||
|
|
||||||
|
controller.CompareNow();
|
||||||
|
|
||||||
|
Assert.Equal(1, controller.Snapshot.MismatchCount);
|
||||||
|
Assert.Contains("rejectedDeltas=1", controller.Snapshot.FirstMismatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Register(
|
||||||
|
RenderSceneShadowRuntime shadow,
|
||||||
|
WorldEntity entity,
|
||||||
|
RenderProjectionClass projectionClass)
|
||||||
|
{
|
||||||
|
RenderProjectionRecord record = Project(entity, projectionClass);
|
||||||
|
shadow.Journal.Register(in record);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RenderProjectionRecord Project(
|
||||||
|
WorldEntity entity,
|
||||||
|
RenderProjectionClass projectionClass) =>
|
||||||
|
RenderProjectionRecordFactory.ProjectEntity(
|
||||||
|
StaticRenderProjectionJournal.StaticEntityId(
|
||||||
|
Landblock,
|
||||||
|
entity.Id),
|
||||||
|
projectionClass,
|
||||||
|
RenderOwnerIncarnation.FromRaw(entity.Id),
|
||||||
|
Landblock,
|
||||||
|
entity.ParentCellId ?? Landblock,
|
||||||
|
entity,
|
||||||
|
spatiallyVisible: true);
|
||||||
|
|
||||||
|
private static CurrentRenderSceneOracle Capture(
|
||||||
|
WorldEntity entity,
|
||||||
|
HashSet<uint> visibleCells)
|
||||||
|
{
|
||||||
|
var oracle = new CurrentRenderSceneOracle();
|
||||||
|
var result = new InteriorEntityPartition.Result();
|
||||||
|
InteriorEntityPartition.Partition(
|
||||||
|
result,
|
||||||
|
visibleCells,
|
||||||
|
[
|
||||||
|
(
|
||||||
|
Landblock,
|
||||||
|
Vector3.Zero,
|
||||||
|
Vector3.One,
|
||||||
|
(IReadOnlyList<WorldEntity>)[entity],
|
||||||
|
(IReadOnlyDictionary<uint, WorldEntity>?)null),
|
||||||
|
],
|
||||||
|
oracle);
|
||||||
|
return oracle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CurrentRenderSceneOracle CaptureNoEntities()
|
||||||
|
{
|
||||||
|
var oracle = new CurrentRenderSceneOracle();
|
||||||
|
var result = new InteriorEntityPartition.Result();
|
||||||
|
InteriorEntityPartition.Partition(
|
||||||
|
result,
|
||||||
|
[],
|
||||||
|
Array.Empty<(
|
||||||
|
uint LandblockId,
|
||||||
|
Vector3 AabbMin,
|
||||||
|
Vector3 AabbMax,
|
||||||
|
IReadOnlyList<WorldEntity> Entities,
|
||||||
|
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)>(),
|
||||||
|
oracle);
|
||||||
|
return oracle;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RenderSceneGeneration Generation(ulong value) =>
|
||||||
|
RenderSceneGeneration.FromRaw(value);
|
||||||
|
|
||||||
|
private static WorldEntity Entity(
|
||||||
|
uint id,
|
||||||
|
uint serverGuid,
|
||||||
|
uint? parentCell) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = id,
|
||||||
|
ServerGuid = serverGuid,
|
||||||
|
SourceGfxObjOrSetupId = 0x02000001u,
|
||||||
|
Position = new Vector3(1, 2, 3),
|
||||||
|
Rotation = Quaternion.Identity,
|
||||||
|
MeshRefs =
|
||||||
|
[
|
||||||
|
new MeshRef(
|
||||||
|
0x01000001u,
|
||||||
|
Matrix4x4.CreateTranslation(0.25f, 0.5f, 0.75f)),
|
||||||
|
],
|
||||||
|
ParentCellId = parentCell,
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue