diff --git a/src/AcDream.App/Composition/FrameRootComposition.cs b/src/AcDream.App/Composition/FrameRootComposition.cs index d7c8bad1..5bf6f4d8 100644 --- a/src/AcDream.App/Composition/FrameRootComposition.cs +++ b/src/AcDream.App/Composition/FrameRootComposition.cs @@ -374,12 +374,6 @@ internal sealed class FrameRootCompositionPhase && d.Options.AutomationArtifactDirectory is not null ? new CurrentRenderSceneOracle() : 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 @@ -390,18 +384,18 @@ internal sealed class FrameRootCompositionPhase acknowledgeDirty: false) : null; RenderScenePViewFrameProductController? renderFrameProduct = - currentRenderSceneOracle is not null - && live.RenderSceneShadow is not null + live.RenderSceneShadow is not null ? new RenderScenePViewFrameProductController( live.RenderSceneShadow, currentRenderSceneOracle, message => d.Log("[UI-PROBE] " + message), live.DrawDispatcher) : null; - live.DrawDispatcher.SetCurrentRenderSceneObserver( - currentRenderSceneOracle); - live.SelectionScene.SetCurrentRenderSceneObserver( - currentRenderSceneOracle); + // After G4 the retained product is the production object source. + // The old dispatcher/selection observer is intentionally detached; + // automation still compares the independently built PView route list. + live.DrawDispatcher.SetCurrentRenderSceneObserver(null); + live.SelectionScene.SetCurrentRenderSceneObserver(null); var worldSceneRenderer = new WorldSceneRenderer( renderFrameResources, renderLoginState, diff --git a/src/AcDream.App/Composition/LivePresentationComposition.cs b/src/AcDream.App/Composition/LivePresentationComposition.cs index 38f8bb02..23881f65 100644 --- a/src/AcDream.App/Composition/LivePresentationComposition.cs +++ b/src/AcDream.App/Composition/LivePresentationComposition.cs @@ -200,15 +200,11 @@ internal sealed class LivePresentationCompositionPhase { 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()); - } + renderSceneShadowLease = scope.Acquire( + "render scene", + () => new RenderSceneShadowRuntime( + RenderSceneGeneration.FromRaw(1)), + static value => value.Dispose()); RenderSceneShadowRuntime? renderSceneShadow = renderSceneShadowLease?.Resource; diff --git a/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs b/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs index 0f6c73f9..ea873c97 100644 --- a/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs +++ b/src/AcDream.App/Rendering/RetailPViewPassExecutor.cs @@ -1,4 +1,5 @@ using System.Numerics; +using AcDream.App.Rendering.Scene; using AcDream.App.Rendering.Sky; using AcDream.App.Rendering.Wb; using AcDream.Core.Rendering; @@ -79,8 +80,33 @@ internal interface IOutdoorSceneParticleOwnerSource IReadOnlySet OutdoorSceneParticleEntityIds { get; } } +internal readonly record struct RenderFrameEntityDrawRequest( + RenderFrameView View, + RenderFrameCandidateRoute Route, + int RouteIndex, + uint CellId, + uint TupleLandblockId); + +internal interface IRenderFrameEntityPassExecutor +{ + void BeginEntityFrame(in RenderFrameView view); + + bool DrawEntityRoute( + ICamera camera, + in RenderFrameView view, + RenderFrameCandidateRoute route, + int routeIndex, + uint cellId, + uint tupleLandblockId); + + void CompleteEntityFrame(in RenderFrameView view); + + void AbortEntityFrame(); +} + internal sealed class RetailPViewPassExecutor : IRetailPViewPassExecutor, + IRenderFrameEntityPassExecutor, IOutdoorSceneParticleOwnerSource { private readonly GL _gl; @@ -147,6 +173,30 @@ internal sealed class RetailPViewPassExecutor : _particleClassifications.BeginFrame(); } + public void BeginEntityFrame(in RenderFrameView view) => + _entities.BeginPackedProductionFrame(in view); + + public bool DrawEntityRoute( + ICamera camera, + in RenderFrameView view, + RenderFrameCandidateRoute route, + int routeIndex, + uint cellId, + uint tupleLandblockId) => + _entities.DrawPackedProductionRoute( + camera, + in view, + route, + routeIndex, + cellId, + tupleLandblockId); + + public void CompleteEntityFrame(in RenderFrameView view) => + _entities.CompletePackedProductionFrame(in view); + + public void AbortEntityFrame() => + _entities.AbortPackedProductionFrame(); + public void AbortFrame() { List? failures = null; @@ -332,7 +382,18 @@ internal sealed class RetailPViewPassExecutor : _terrainDiagnostics.Complete(); DisableClipDistances(); - if (context.OutdoorEntities.Count > 0) + if (context.EntityDraw is RenderFrameEntityDrawRequest request) + { + RenderFrameView drawView = request.View; + _entities.DrawPackedProductionRoute( + frame.Camera, + in drawView, + request.Route, + request.RouteIndex, + request.CellId, + request.TupleLandblockId); + } + else if (context.OutdoorEntities.Count > 0) { var sceneryEntry = ( frame.PlayerLandblockId ?? 0u, @@ -363,7 +424,18 @@ internal sealed class RetailPViewPassExecutor : _clipFrame.BindTerrainClip(_gl); DisableClipDistances(); - if (context.Dynamics.Count > 0) + if (context.EntityDraw is RenderFrameEntityDrawRequest request) + { + RenderFrameView drawView = request.View; + _entities.DrawPackedProductionRoute( + frame.Camera, + in drawView, + request.Route, + request.RouteIndex, + request.CellId, + request.TupleLandblockId); + } + else if (context.Dynamics.Count > 0) { var dynamicsEntry = ( frame.PlayerLandblockId ?? 0u, diff --git a/src/AcDream.App/Rendering/RetailPViewRenderer.cs b/src/AcDream.App/Rendering/RetailPViewRenderer.cs index 17bca20e..2e9904be 100644 --- a/src/AcDream.App/Rendering/RetailPViewRenderer.cs +++ b/src/AcDream.App/Rendering/RetailPViewRenderer.cs @@ -76,13 +76,6 @@ public sealed class RetailPViewRenderer InteriorEntityPartition.IObserver? partitionObserver, RenderScenePViewFrameProductController? sceneFrameProduct = null) { - if (sceneFrameProduct is not null - && partitionObserver is not ICurrentRenderPViewObserver) - { - throw new ArgumentException( - "The scene frame-product referee requires the current PView observer.", - nameof(partitionObserver)); - } _partitionObserver = partitionObserver; _candidateObserver = partitionObserver as ICurrentRenderPViewObserver; _sceneFrameProduct = sceneFrameProduct; @@ -212,6 +205,16 @@ public sealed class RetailPViewRenderer ctx.ViewProjection, _viewconeScratch); + IRenderFrameEntityPassExecutor? frameEntityPasses = null; + if (_sceneFrameProduct is not null) + { + frameEntityPasses = passes as IRenderFrameEntityPassExecutor + ?? throw new InvalidOperationException( + "The production frame product requires a packed entity-pass executor."); + } + RenderFrameView frameView = default; + bool frameViewBorrowed = false; + bool entityFrameOpen = false; _candidateObserver?.BeginPViewFrame(); try { @@ -242,32 +245,73 @@ public sealed class RetailPViewRenderer } } - DrawLandscapeThroughOutsideView(ctx, passes, clipAssembly, partition, viewcone); + if (_sceneFrameProduct is not null) + { + frameView = _sceneFrameProduct.BuildAndBorrow( + pvFrame, + clipAssembly, + viewcone, + _lookInFrames, + drawableCells, + ctx.Cells, + ctx.AnimatedEntityIds, + ctx.RootCell.IsOutdoorNode); + frameViewBorrowed = true; + frameEntityPasses!.BeginEntityFrame(in frameView); + entityFrameOpen = true; + } + + DrawLandscapeThroughOutsideView( + ctx, + passes, + clipAssembly, + partition, + viewcone, + frameEntityPasses, + in frameView); passes.UseIndoorMembershipOnlyRouting(); DrawExitPortalMasks(ctx, passes, pvFrame, clipAssembly, drawableCells); DrawEnvCellShells(passes, pvFrame); - DrawCellObjectLists(ctx, passes, pvFrame, clipAssembly, drawableCells, partition, viewcone); - DrawDynamicsLast(ctx, passes, partition, viewcone, ctx.RootCell.IsOutdoorNode); - - _candidateObserver?.CompletePViewFrame(); - _sceneFrameProduct?.BuildAndCompare( + DrawCellObjectLists( + ctx, + passes, pvFrame, clipAssembly, - viewcone, - _lookInFrames, drawableCells, - ctx.Cells, - ctx.AnimatedEntityIds, - ctx.PlayerLandblockId ?? 0, + partition, + viewcone, + frameEntityPasses, + in frameView); + DrawDynamicsLast( + ctx, + passes, + partition, + viewcone, ctx.RootCell.IsOutdoorNode, - ctx.CameraWorldPosition); + frameEntityPasses, + in frameView); + + if (entityFrameOpen) + { + frameEntityPasses!.CompleteEntityFrame(in frameView); + entityFrameOpen = false; + } + _candidateObserver?.CompletePViewFrame(); + _sceneFrameProduct?.CompleteProduction(in frameView); return result; } catch { + if (entityFrameOpen) + frameEntityPasses!.AbortEntityFrame(); _candidateObserver?.AbortPViewFrame(); throw; } + finally + { + if (frameViewBorrowed) + _sceneFrameProduct!.Release(in frameView); + } } // R-A2: group the nearby building cells by BuildingId and run one per-building flood per group @@ -397,7 +441,9 @@ public sealed class RetailPViewRenderer RetailPViewFrameInput ctx, IRetailPViewPassExecutor passes, ClipFrameAssembly clipAssembly, - InteriorEntityPartition.Result partition) + InteriorEntityPartition.Result partition, + IRenderFrameEntityPassExecutor? frameEntityPasses, + in RenderFrameView frameView) { if (_lookInFrames.Count == 0) return; @@ -475,7 +521,16 @@ public sealed class RetailPViewRenderer i, cellId, _cellStaticScratch); - passes.DrawEntityBucket(ctx, _cellStaticScratch, _oneCell); + DrawEntityRouteOrLegacy( + ctx, + passes, + frameEntityPasses, + in frameView, + RenderFrameCandidateRoute.LookInObject, + i, + cellId, + _cellStaticScratch, + _oneCell); // The cell-particles pass for look-in cells — retail's // nested DrawCells draws objects WITH their emitters. @@ -492,7 +547,9 @@ public sealed class RetailPViewRenderer IRetailPViewPassExecutor passes, ClipFrameAssembly clipAssembly, InteriorEntityPartition.Result partition, - ViewconeCuller viewcone) + ViewconeCuller viewcone, + IRenderFrameEntityPassExecutor? frameEntityPasses, + in RenderFrameView frameView) { if (clipAssembly.OutsideViewSlices.Length == 0) return; @@ -534,15 +591,37 @@ public sealed class RetailPViewRenderer probeSliceIndex, 0, _outdoorStaticScratch); + RenderFrameEntityDrawRequest? entityDraw = + frameEntityPasses is null + ? null + : new RenderFrameEntityDrawRequest( + frameView, + RenderFrameCandidateRoute.LandscapeOutdoorStatic, + probeSliceIndex, + 0, + ctx.PlayerLandblockId ?? 0); probeSliceIndex++; - passes.DrawLandscapeSlice(ctx, new RetailPViewLandscapeSliceContext(slice, _outdoorStaticScratch)); + passes.DrawLandscapeSlice( + ctx, + new RetailPViewLandscapeSliceContext( + slice, + _outdoorStaticScratch) + { + EntityDraw = entityDraw, + }); } // #124: far-building look-ins draw HERE — still inside the landscape // stage (their punches mark against the terrain/exterior depth just // drawn), strictly BEFORE the depth clear + seals below, matching // retail's LScape::draw placement (DrawCells pc:432719 vs 432732/432785). - DrawBuildingLookIns(ctx, passes, clipAssembly, partition); + DrawBuildingLookIns( + ctx, + passes, + clipAssembly, + partition, + frameEntityPasses, + in frameView); // LATE phase (per slice): outside-stage dynamics' meshes (#118 — drawn // pre-clear so the seal protects their aperture pixels; AFTER the @@ -591,9 +670,25 @@ public sealed class RetailPViewRenderer probeSliceIndex, 0, _outdoorStaticScratch); + RenderFrameEntityDrawRequest? entityDraw = + frameEntityPasses is null + ? null + : new RenderFrameEntityDrawRequest( + frameView, + RenderFrameCandidateRoute.LandscapeOutsideDynamic, + probeSliceIndex, + 0, + ctx.PlayerLandblockId ?? 0); probeSliceIndex++; - passes.DrawLandscapeSliceLate(ctx, new RetailPViewLandscapeLateSliceContext( - slice, _outdoorStaticScratch, _lateParticleOwnerScratch)); + passes.DrawLandscapeSliceLate( + ctx, + new RetailPViewLandscapeLateSliceContext( + slice, + _outdoorStaticScratch, + _lateParticleOwnerScratch) + { + EntityDraw = entityDraw, + }); } // #131: UNATTACHED emitters (AttachedObjectId == 0 — portal swirls, @@ -702,7 +797,9 @@ public sealed class RetailPViewRenderer IRetailPViewPassExecutor passes, InteriorEntityPartition.Result partition, ViewconeCuller viewcone, - bool rootIsOutdoor) + bool rootIsOutdoor, + IRenderFrameEntityPassExecutor? frameEntityPasses, + in RenderFrameView frameView) { if (partition.Dynamics.Count == 0) return; @@ -749,7 +846,16 @@ public sealed class RetailPViewRenderer 0, _dynamicsScratch); passes.UseIndoorMembershipOnlyRouting(); - passes.DrawEntityBucket(ctx, _dynamicsScratch, visibleCellIds: null); + DrawEntityRouteOrLegacy( + ctx, + passes, + frameEntityPasses, + in frameView, + RenderFrameCandidateRoute.DynamicLast, + 0, + 0, + _dynamicsScratch, + visibleCellIds: null); // #121: dynamics' attached emitters (portal swirls, creature effects) // gate through the SAME cone-surviving owner set as their meshes — @@ -776,7 +882,9 @@ public sealed class RetailPViewRenderer ClipFrameAssembly clipAssembly, HashSet drawableCells, InteriorEntityPartition.Result partition, - ViewconeCuller viewcone) + ViewconeCuller viewcone, + IRenderFrameEntityPassExecutor? frameEntityPasses, + in RenderFrameView frameView) { // T1: per-cell STATIC object lists only (dat-baked 0x40 statics) — // dynamics moved to DrawDynamicsLast. Far→near with the cells, after @@ -840,7 +948,16 @@ public sealed class RetailPViewRenderer 0, _allCellStatics); passes.UseIndoorMembershipOnlyRouting(); - passes.DrawEntityBucket(ctx, _allCellStatics, _cellObjCells); + DrawEntityRouteOrLegacy( + ctx, + passes, + frameEntityPasses, + in frameView, + RenderFrameCandidateRoute.CellStatic, + 0, + 0, + _allCellStatics, + _cellObjCells); } // Cell-particle pass — consolidated across ALL visible cells into ONE @@ -863,6 +980,35 @@ public sealed class RetailPViewRenderer new RetailPViewCellSliceContext(0u, NoClipSlice, _allCellStatics)); } + private static void DrawEntityRouteOrLegacy( + RetailPViewFrameInput frame, + IRetailPViewPassExecutor passes, + IRenderFrameEntityPassExecutor? frameEntityPasses, + in RenderFrameView frameView, + RenderFrameCandidateRoute route, + int routeIndex, + uint cellId, + IReadOnlyList legacyEntities, + HashSet? visibleCellIds) + { + if (frameEntityPasses is not null) + { + frameEntityPasses.DrawEntityRoute( + frame.Camera, + in frameView, + route, + routeIndex, + cellId, + frame.PlayerLandblockId ?? 0); + return; + } + + passes.DrawEntityBucket( + frame, + legacyEntities, + visibleCellIds); + } + // T3 scratch lists (render thread only; cleared per use). private readonly List _outdoorStaticScratch = new(); private readonly List _cellStaticScratch = new(); @@ -1241,7 +1387,10 @@ public sealed class RetailPViewFrameResult public readonly record struct RetailPViewLandscapeSliceContext( ClipViewSlice Slice, - IReadOnlyList OutdoorEntities); + IReadOnlyList OutdoorEntities) +{ + internal RenderFrameEntityDrawRequest? EntityDraw { get; init; } +} /// #131/#132: the late landscape phase's per-slice payload — /// outside-stage dynamics to mesh-draw, plus the full scene-particle owner @@ -1249,7 +1398,10 @@ public readonly record struct RetailPViewLandscapeSliceContext( public readonly record struct RetailPViewLandscapeLateSliceContext( ClipViewSlice Slice, IReadOnlyList Dynamics, - IReadOnlyList ParticleOwners); + IReadOnlyList ParticleOwners) +{ + internal RenderFrameEntityDrawRequest? EntityDraw { get; init; } +} public readonly record struct RetailPViewCellSliceContext( uint CellId, diff --git a/src/AcDream.App/Rendering/Scene/Arch/ArchRenderScene.cs b/src/AcDream.App/Rendering/Scene/Arch/ArchRenderScene.cs index 48ef28d2..07f7f3a2 100644 --- a/src/AcDream.App/Rendering/Scene/Arch/ArchRenderScene.cs +++ b/src/AcDream.App/Rendering/Scene/Arch/ArchRenderScene.cs @@ -10,7 +10,8 @@ namespace AcDream.App.Rendering.Scene.Arch; /// projection. Arch identities never leave this namespace; callers only see /// generation-checked acdream value types. /// -/// Slice F1 deliberately leaves this owner unbound and non-drawing. +/// Slice F introduced this owner in non-drawing shadow mode; Slice G supplies +/// its borrowed frame product to the production renderer. /// internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource { @@ -35,6 +36,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource private ArchWorld _world; private RenderProjectionCounts _counts; private ulong _lastAppliedJournalSequence; + private ulong _indexRevision = 1; private bool _disposed; public ArchRenderScene(RenderSceneGeneration initialGeneration) @@ -225,6 +227,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource _counts = default; _lastAppliedJournalSequence = 0; Generation = replacementGeneration; + AdvanceIndexRevision(); } public void Dispose() @@ -274,6 +277,13 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource _dirty.Count); } + ulong IRenderSceneQuerySource.GetIndexRevision( + RenderSceneGeneration generation) + { + EnsureQueryGeneration(generation); + return _indexRevision; + } + bool IRenderSceneQuerySource.TryGet( RenderSceneGeneration generation, RenderProjectionId id, @@ -591,6 +601,12 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource in RenderProjectionRecord prior, in RenderProjectionRecord current) { + if (IndexMembershipEquals(in prior, in current)) + { + SynchronizeDirtyIndex(in current); + return; + } + RemoveFromIndices(in prior); AddToIndices(in current); } @@ -635,6 +651,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource _lightCandidates.Add(record.Id); if (record.DirtyMask != RenderDirtyMask.None) _dirty.Add(record.Id); + AdvanceIndexRevision(); } private void RemoveFromIndices(in RenderProjectionRecord record) @@ -650,6 +667,46 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource _dirty.Remove(record.Id); RemoveCell(_cellStatics, record.Residency.FullCellId, record.Id); RemoveCell(_cellDynamics, record.Residency.FullCellId, record.Id); + AdvanceIndexRevision(); + } + + private static bool IndexMembershipEquals( + in RenderProjectionRecord left, + in RenderProjectionRecord right) + { + const RenderProjectionFlags indexedFlags = + RenderProjectionFlags.Translucent + | RenderProjectionFlags.Selectable + | RenderProjectionFlags.LightCandidate + | RenderProjectionFlags.PortalStraddling + | RenderProjectionFlags.SpatiallyResident; + + return left.ProjectionClass == right.ProjectionClass + && left.Source.ParentCellId == right.Source.ParentCellId + && left.Residency.FullCellId == right.Residency.FullCellId + && (left.Flags & indexedFlags) == (right.Flags & indexedFlags) + && left.MeshSet.MeshCount == right.MeshSet.MeshCount + && left.SortKey == right.SortKey; + } + + private void SynchronizeDirtyIndex( + in RenderProjectionRecord record) + { + if (record.DirtyMask == RenderDirtyMask.None) + _dirty.Remove(record.Id); + else + _dirty.Add(record.Id); + } + + private void AdvanceIndexRevision() + { + if (_indexRevision == ulong.MaxValue) + { + throw new InvalidOperationException( + "Render-scene index revision space was exhausted."); + } + + _indexRevision++; } private static bool IsDynamic(RenderProjectionClass projectionClass) => diff --git a/src/AcDream.App/Rendering/Scene/RenderSceneContracts.cs b/src/AcDream.App/Rendering/Scene/RenderSceneContracts.cs index abd4ce67..057d1fe7 100644 --- a/src/AcDream.App/Rendering/Scene/RenderSceneContracts.cs +++ b/src/AcDream.App/Rendering/Scene/RenderSceneContracts.cs @@ -455,6 +455,7 @@ internal interface IRenderSceneQuerySource { RenderProjectionCounts GetCounts(RenderSceneGeneration generation); RenderSceneIndexCounts GetIndexCounts(RenderSceneGeneration generation); + ulong GetIndexRevision(RenderSceneGeneration generation); bool TryGet( RenderSceneGeneration generation, @@ -503,6 +504,9 @@ internal readonly struct RenderSceneQuery public RenderSceneIndexCounts IndexCounts => Source.GetIndexCounts(Generation); + public ulong IndexRevision => + Source.GetIndexRevision(Generation); + public bool TryGet( RenderProjectionId id, out RenderProjectionRecord record) => diff --git a/src/AcDream.App/Rendering/Scene/RenderScenePViewFrameProduct.cs b/src/AcDream.App/Rendering/Scene/RenderScenePViewFrameProduct.cs index e573bc09..bbd368bd 100644 --- a/src/AcDream.App/Rendering/Scene/RenderScenePViewFrameProduct.cs +++ b/src/AcDream.App/Rendering/Scene/RenderScenePViewFrameProduct.cs @@ -113,16 +113,15 @@ internal interface IRenderFrameProductSnapshotSource } /// -/// G1 compare-only owner. It builds the candidate frame product directly from -/// the incremental scene after the accepted PView traversal, borrows it in the -/// same render frame, and compares route membership against the current-path -/// observer. It never supplies a production draw input. +/// Owns the same-frame PView product. Diagnostic composition may compare it +/// with the retired current-path observer; production borrows the exact same +/// arena and supplies its ordered ranges directly to the dispatcher. /// internal sealed class RenderScenePViewFrameProductController : IRenderFrameProductSnapshotSource { private readonly RenderSceneShadowRuntime _shadow; - private readonly CurrentRenderSceneOracle _current; + private readonly CurrentRenderSceneOracle? _current; private readonly WbDrawDispatcher? _dispatcher; private readonly RenderScenePViewFrameBuilder _builder = new(); private readonly RenderFrameExchange _exchange = new(); @@ -155,12 +154,12 @@ internal sealed class RenderScenePViewFrameProductController : public RenderScenePViewFrameProductController( RenderSceneShadowRuntime shadow, - CurrentRenderSceneOracle current, + CurrentRenderSceneOracle? current = null, Action? log = null, WbDrawDispatcher? dispatcher = null) { _shadow = shadow ?? throw new ArgumentNullException(nameof(shadow)); - _current = current ?? throw new ArgumentNullException(nameof(current)); + _current = current; _log = log; _dispatcher = dispatcher; Snapshot = RenderFrameProductComparisonSnapshot.Disabled with @@ -171,6 +170,56 @@ internal sealed class RenderScenePViewFrameProductController : public RenderFrameProductComparisonSnapshot Snapshot { get; private set; } + private CurrentRenderSceneOracle Current => + _current + ?? throw new InvalidOperationException( + "Current-path comparison was requested without a referee."); + + public RenderFrameView BuildAndBorrow( + PortalVisibilityFrame portalFrame, + ClipFrameAssembly clipAssembly, + ViewconeCuller viewcone, + IReadOnlyList lookInFrames, + HashSet drawableCells, + IRetailPViewCellSource cells, + HashSet? animatedEntityIds, + bool rootIsOutdoor) + { + ArgumentNullException.ThrowIfNull(portalFrame); + ArgumentNullException.ThrowIfNull(clipAssembly); + ArgumentNullException.ThrowIfNull(viewcone); + ArgumentNullException.ThrowIfNull(lookInFrames); + ArgumentNullException.ThrowIfNull(drawableCells); + ArgumentNullException.ThrowIfNull(cells); + + ulong frameSequence = checked(++_productFrameSequence); + RenderSceneQuery scene = _shadow.Query; + RenderSceneDigest sourceDigest = _current is null + ? new RenderSceneDigest( + scene.Generation, + scene.Counts, + default) + : _shadow.BuildDigest(); + var input = new RenderScenePViewBuildInput( + scene, + sourceDigest, + portalFrame, + clipAssembly, + viewcone, + lookInFrames, + drawableCells, + cells, + animatedEntityIds, + rootIsOutdoor); + _builder.Build(_exchange, frameSequence, in input); + return _exchange.BorrowLatest( + scene.Generation, + frameSequence); + } + + public void Release(in RenderFrameView view) => + _exchange.Release(in view); + public void BuildAndCompare( PortalVisibilityFrame portalFrame, ClipFrameAssembly clipAssembly, @@ -183,18 +232,7 @@ internal sealed class RenderScenePViewFrameProductController : bool rootIsOutdoor, Vector3 cameraWorldPosition = default) { - ArgumentNullException.ThrowIfNull(portalFrame); - ArgumentNullException.ThrowIfNull(clipAssembly); - ArgumentNullException.ThrowIfNull(viewcone); - ArgumentNullException.ThrowIfNull(lookInFrames); - ArgumentNullException.ThrowIfNull(drawableCells); - ArgumentNullException.ThrowIfNull(cells); - - ulong frameSequence = checked(++_productFrameSequence); - RenderSceneQuery scene = _shadow.Query; - var input = new RenderScenePViewBuildInput( - scene, - _shadow.BuildDigest(), + RenderFrameView view = BuildAndBorrow( portalFrame, clipAssembly, viewcone, @@ -203,10 +241,6 @@ internal sealed class RenderScenePViewFrameProductController : cells, animatedEntityIds, rootIsOutdoor); - _builder.Build(_exchange, frameSequence, in input); - RenderFrameView view = _exchange.BorrowLatest( - scene.Generation, - frameSequence); try { Compare( @@ -216,26 +250,92 @@ internal sealed class RenderScenePViewFrameProductController : } finally { - _exchange.Release(in view); + Release(in view); } } - private void Compare( - in RenderFrameView view, - uint tupleLandblockId, - Vector3 cameraWorldPosition) + public void CompleteProduction(in RenderFrameView view) + { + _packedClassification = + _dispatcher?.PackedClassificationSnapshot ?? default; + if (_current is null) + { + _shadow.ClearDirty(); + Snapshot = Snapshot with + { + Enabled = true, + ProductFrameSequence = view.FrameSequence, + ActualCandidateCount = view.RouteCandidates.Length, + PackedClassificationProjectionCount = + _packedClassification.ProjectionCount, + PackedClassificationStaticRebuildCount = + _packedClassification.StaticRebuildCount, + PackedClassificationSameFrameReuseCount = + _packedClassification.SameFrameReuseCount, + PackedClassificationCrossFrameReuseCount = + _packedClassification.CrossFrameReuseCount, + PackedClassificationAnimatedCount = + _packedClassification.AnimatedClassificationCount, + PackedClassificationRetiredCount = + _packedClassification.RetiredProjectionCount, + PackedClassificationRetainedPayloadBytes = + _packedClassification.RetainedPayloadBytes, + ProductCounts = view.DiagnosticCounts, + }; + return; + } + + CandidateComparison candidates = + CompareCandidateMembership(in view); + if (candidates.Successful) + _shadow.ClearDirty(); + Snapshot = Snapshot with + { + Enabled = true, + ProductFrameSequence = view.FrameSequence, + CurrentPViewFrameSequence = + Current.Snapshot.CompletedPViewFrameSequence, + ComparisonCount = _comparisonCount, + SuccessfulComparisonCount = _successfulComparisonCount, + ExpectedCandidateCount = _expected.Count, + ActualCandidateCount = _actual.Count, + MismatchCount = _mismatchCount, + FirstMismatch = _firstMismatch, + ExpectedDigest = candidates.ExpectedDigest, + ActualDigest = candidates.ActualDigest, + PackedClassificationProjectionCount = + _packedClassification.ProjectionCount, + PackedClassificationStaticRebuildCount = + _packedClassification.StaticRebuildCount, + PackedClassificationSameFrameReuseCount = + _packedClassification.SameFrameReuseCount, + PackedClassificationCrossFrameReuseCount = + _packedClassification.CrossFrameReuseCount, + PackedClassificationAnimatedCount = + _packedClassification.AnimatedClassificationCount, + PackedClassificationRetiredCount = + _packedClassification.RetiredProjectionCount, + PackedClassificationRetainedPayloadBytes = + _packedClassification.RetainedPayloadBytes, + ProductCounts = view.DiagnosticCounts, + }; + } + + private CandidateComparison CompareCandidateMembership( + in RenderFrameView view) { _comparisonCount = checked(_comparisonCount + 1); _expected.Clear(); _actual.Clear(); IReadOnlyList expected = - _current.PViewCandidates; + Current.PViewCandidates; if (_expected.Capacity < expected.Count) _expected.Capacity = expected.Count; - for (int i = 0; i < expected.Count; i++) + for (int index = 0; index < expected.Count; index++) { - CurrentRenderPViewCandidateFingerprint candidate = expected[i]; + CurrentRenderPViewCandidateFingerprint candidate = + expected[index]; CurrentRenderProjectionFingerprint projection = candidate.Projection; _expected.Add(new CandidateKey( @@ -245,11 +345,15 @@ internal sealed class RenderScenePViewFrameProductController : ExpectedId(in projection))); } - ReadOnlySpan ranges = view.RouteRanges; - ReadOnlySpan records = view.RouteCandidates; + ReadOnlySpan ranges = + view.RouteRanges; + ReadOnlySpan records = + view.RouteCandidates; if (_actual.Capacity < records.Length) _actual.Capacity = records.Length; - for (int rangeIndex = 0; rangeIndex < ranges.Length; rangeIndex++) + for (int rangeIndex = 0; + rangeIndex < ranges.Length; + rangeIndex++) { RenderFrameCandidateRange range = ranges[rangeIndex]; int end = checked(range.Offset + range.Count); @@ -285,10 +389,32 @@ internal sealed class RenderScenePViewFrameProductController : { _firstMismatch = mismatch; _log?.Invoke( - "[render-frame-product] first mismatch: " + mismatch); + "[render-frame-product] first mismatch: " + + mismatch); } } + return new CandidateComparison( + mismatch is null, + expectedDigest, + actualDigest); + } + + private void Compare( + in RenderFrameView view, + uint tupleLandblockId, + Vector3 cameraWorldPosition) + { + CandidateComparison candidates = + CompareCandidateMembership(in view); + RenderSceneHash128 expectedDigest = + candidates.ExpectedDigest; + RenderSceneHash128 actualDigest = + candidates.ActualDigest; + string? mismatch = candidates.Successful + ? null + : _firstMismatch; + PackedInputComparison packed = ComparePackedInput(in view, tupleLandblockId); ClassifiedOutputComparison classified = @@ -312,7 +438,7 @@ internal sealed class RenderScenePViewFrameProductController : Enabled: true, ProductFrameSequence: view.FrameSequence, CurrentPViewFrameSequence: - _current.Snapshot.CompletedPViewFrameSequence, + Current.Snapshot.CompletedPViewFrameSequence, ComparisonCount: _comparisonCount, SuccessfulComparisonCount: _successfulComparisonCount, ExpectedCandidateCount: _expected.Count, @@ -395,7 +521,7 @@ internal sealed class RenderScenePViewFrameProductController : _packedClassification = dispatcher.PackedClassificationSnapshot; IReadOnlyList expected = - _current.DispatcherSubmissions; + Current.DispatcherSubmissions; IReadOnlyList actual = dispatcher.PackedDispatcherSubmissions; RenderSceneHash128 expectedDigest = @@ -426,7 +552,7 @@ internal sealed class RenderScenePViewFrameProductController : } IReadOnlyList - expectedSelection = _current.SelectionParts; + expectedSelection = Current.SelectionParts; IReadOnlyList actualSelection = dispatcher.PackedSelectionParts; BuildSelectionKeys( @@ -628,7 +754,7 @@ internal sealed class RenderScenePViewFrameProductController : _packedActual.Clear(); IReadOnlyList expected = - _current.DispatcherCandidates; + Current.DispatcherCandidates; if (_packedExpected.Capacity < expected.Count) _packedExpected.Capacity = expected.Count; for (int index = 0; index < expected.Count; index++) @@ -750,10 +876,10 @@ internal sealed class RenderScenePViewFrameProductController : private string? ComparePackedInputKeys(int actualDrawCount) { - if (_current.Snapshot.DispatcherDrawCount != actualDrawCount) + if (Current.Snapshot.DispatcherDrawCount != actualDrawCount) { return $"field=drawCount expected=" - + $"{_current.Snapshot.DispatcherDrawCount} " + + $"{Current.Snapshot.DispatcherDrawCount} " + $"actual={actualDrawCount}"; } if (_packedExpected.Count != _packedActual.Count) @@ -891,6 +1017,11 @@ internal sealed class RenderScenePViewFrameProductController : RenderSceneHash128 ExpectedDigest, RenderSceneHash128 ActualDigest); + private readonly record struct CandidateComparison( + bool Successful, + RenderSceneHash128 ExpectedDigest, + RenderSceneHash128 ActualDigest); + private readonly record struct ClassifiedOutputComparison( bool Successful, int ExpectedDrawCount, @@ -993,11 +1124,19 @@ internal sealed class RenderScenePViewFrameBuilder private RenderProjectionRecord[] _outdoor = []; private RenderProjectionRecord[] _dynamics = []; private RenderProjectionRecord[] _cell = []; + private RenderProjectionRecord[] _dirty = []; private RenderProjectionRecord[] _survivors = []; private RenderProjectionRecord[] _cellRoute = []; + private readonly Dictionary + _outdoorPositions = []; + private readonly Dictionary + _dynamicPositions = []; private int _outdoorCount; private int _dynamicCount; private int _cellRouteCount; + private int _dirtyCount; + private RenderSceneGeneration _indexGeneration; + private ulong _indexRevision; public void Build( RenderFrameExchange exchange, @@ -1023,6 +1162,7 @@ internal sealed class RenderScenePViewFrameBuilder BuildCellStaticRoute(writer, in input); BuildDynamicLastRoute(writer, in input); writer.Publish(); + AcknowledgeCachedDirtyRecords(); } catch { @@ -1034,21 +1174,97 @@ internal sealed class RenderScenePViewFrameBuilder private void LoadSceneIndices(RenderSceneQuery scene) { RenderSceneIndexCounts counts = scene.IndexCounts; - EnsureCapacity(ref _outdoor, counts.OutdoorStatic); - _outdoorCount = scene.CopyIndexTo( - RenderSceneIndex.OutdoorStatic, - _outdoor); - _outdoorCount = CompactAndSort( - _outdoor, - _outdoorCount); + ulong revision = scene.IndexRevision; + if (scene.Generation != _indexGeneration + || revision != _indexRevision) + { + EnsureCapacity(ref _outdoor, counts.OutdoorStatic); + _outdoorCount = scene.CopyIndexTo( + RenderSceneIndex.OutdoorStatic, + _outdoor); + _outdoorCount = CompactAndSort( + _outdoor, + _outdoorCount); + BuildPositionIndex( + _outdoor, + _outdoorCount, + _outdoorPositions); - EnsureCapacity(ref _dynamics, counts.Dynamic); - _dynamicCount = scene.CopyIndexTo( - RenderSceneIndex.Dynamic, - _dynamics); - _dynamicCount = CompactAndSort( - _dynamics, - _dynamicCount); + EnsureCapacity(ref _dynamics, counts.Dynamic); + _dynamicCount = scene.CopyIndexTo( + RenderSceneIndex.Dynamic, + _dynamics); + _dynamicCount = CompactAndSort( + _dynamics, + _dynamicCount); + BuildPositionIndex( + _dynamics, + _dynamicCount, + _dynamicPositions); + + _indexGeneration = scene.Generation; + _indexRevision = revision; + } + + EnsureCapacity(ref _dirty, counts.Dirty); + _dirtyCount = scene.CopyIndexTo( + RenderSceneIndex.Dirty, + _dirty); + for (int index = 0; index < _dirtyCount; index++) + { + RenderProjectionRecord record = _dirty[index]; + if (_outdoorPositions.TryGetValue( + record.Id, + out int outdoorPosition)) + { + _outdoor[outdoorPosition] = record; + } + if (_dynamicPositions.TryGetValue( + record.Id, + out int dynamicPosition)) + { + _dynamics[dynamicPosition] = record; + } + } + } + + private void AcknowledgeCachedDirtyRecords() + { + for (int index = 0; index < _dirtyCount; index++) + { + RenderProjectionId id = _dirty[index].Id; + if (_outdoorPositions.TryGetValue( + id, + out int outdoorPosition)) + { + _outdoor[outdoorPosition] = + _outdoor[outdoorPosition] with + { + DirtyMask = RenderDirtyMask.None, + }; + } + if (_dynamicPositions.TryGetValue( + id, + out int dynamicPosition)) + { + _dynamics[dynamicPosition] = + _dynamics[dynamicPosition] with + { + DirtyMask = RenderDirtyMask.None, + }; + } + } + } + + private static void BuildPositionIndex( + RenderProjectionRecord[] records, + int count, + Dictionary positions) + { + positions.Clear(); + positions.EnsureCapacity(count); + for (int index = 0; index < count; index++) + positions.Add(records[index].Id, index); } private void BuildOutdoorRoutes( diff --git a/src/AcDream.App/Rendering/Scene/RenderSceneShadowRuntime.cs b/src/AcDream.App/Rendering/Scene/RenderSceneShadowRuntime.cs index 25ab889f..aad7adbd 100644 --- a/src/AcDream.App/Rendering/Scene/RenderSceneShadowRuntime.cs +++ b/src/AcDream.App/Rendering/Scene/RenderSceneShadowRuntime.cs @@ -50,10 +50,12 @@ internal interface IRenderSceneShadowSnapshotSource } /// -/// 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. +/// Owner of the derived render projection. Slice F introduced it as a +/// diagnostic shadow; Slice G keeps the same generation/incarnation journal +/// boundary while lending its frame product to production drawing. 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. /// internal sealed class RenderSceneShadowRuntime : IDisposable { diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.PackedOracle.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.PackedOracle.cs index 00b19924..b04df854 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.PackedOracle.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.PackedOracle.cs @@ -1,4 +1,5 @@ using System.Numerics; +using AcDream.Core.Rendering; using AcDream.App.Rendering.Scene; using AcDream.App.Rendering.Selection; using AcDream.Core.Lighting; @@ -10,10 +11,10 @@ using DatReaderWriter.Enums; namespace AcDream.App.Rendering.Wb; /// -/// G2 compare-only classification of the scene-built frame product. This file -/// deliberately shares the production dispatcher's exact mesh, texture, -/// light, translucency, selection, grouping, and ordering owners while keeping -/// its group storage separate. It never uploads or draws. +/// Scene-frame classification and production submission. The retained group +/// storage is also consumed by the G2/G3 compare oracle, while production +/// routes reuse the dispatcher's exact mesh, texture, light, translucency, +/// selection, upload, and draw owners. /// public sealed unsafe partial class WbDrawDispatcher { @@ -35,6 +36,10 @@ public sealed unsafe partial class WbDrawDispatcher private long _packedGroupFrame; private long _nextPackedGroupRegistration = 1; private int _nextPackedInstanceSubmissionOrder; + private bool _packedProductionFrameOpen; + private RenderSceneGeneration _packedProductionGeneration; + private ulong _packedProductionFrameSequence; + private int _packedProductionNextRange; internal IReadOnlyList PackedDispatcherSubmissions => _packedSubmissions; @@ -51,104 +56,28 @@ public sealed unsafe partial class WbDrawDispatcher uint tupleLandblockId, Vector3 cameraWorldPosition) { - if (_packedGroupFrame == long.MaxValue) + if (_packedProductionFrameOpen) { throw new InvalidOperationException( - "Packed dispatcher frame identity was exhausted."); + "The packed dispatcher oracle cannot replace an active production frame."); } - _packedGroupFrame++; - PruneInstanceGroupsUnusedBeforeFrame( - _packedGroups, - _packedRetiredGroupKeys, - _packedGroupFrame - 1); - _packedEntityById.Clear(); - _packedSubmissions.Clear(); - _packedSelectionParts.Clear(); - _packedSelectionKeys.Clear(); - _packedClassificationCache.BeginFrame(view.Generation); - - ReadOnlySpan entities = - view.EntityCandidates; - for (int i = 0; i < entities.Length; i++) - { - if (!_packedEntityById.TryAdd( - entities[i].Projection.Id, - entities[i])) - { - throw new InvalidOperationException( - $"Packed dispatcher received duplicate projection " - + $"{entities[i].Projection.Id}."); - } - } - - ReadOnlySpan meshParts = view.MeshParts; - ReadOnlySpan routeCandidates = - view.RouteCandidates; + BeginPackedFrameStorage(in view); ReadOnlySpan ranges = view.RouteRanges; for (int rangeIndex = 0; rangeIndex < ranges.Length; rangeIndex++) { - _nextPackedInstanceSubmissionOrder = 0; - uint anyVao = 0; - foreach (InstanceGroup group in _packedGroups.Values) - group.ClearPerInstanceData(); - - RenderFrameCandidateRange range = ranges[rangeIndex]; - int end = checked(range.Offset + range.Count); - if ((uint)range.Offset > (uint)routeCandidates.Length - || (uint)end > (uint)routeCandidates.Length) - { - throw new InvalidOperationException( - $"Packed dispatcher route {rangeIndex} exceeds " - + "candidate storage."); - } - - for (int routeIndex = range.Offset; - routeIndex < end; - routeIndex++) - { - RenderProjectionRecord projection = - routeCandidates[routeIndex]; - if ((projection.Flags & RenderProjectionFlags.Draw) == 0) - continue; - if (!_packedEntityById.TryGetValue( - projection.Id, - out RenderFrameEntityCandidate source)) - { - throw new InvalidOperationException( - $"Packed route references absent entity " - + $"{projection.Id}."); - } - - int meshEnd = checked( - source.MeshPartOffset + source.MeshPartCount); - if ((uint)source.MeshPartOffset - > (uint)meshParts.Length - || (uint)meshEnd > (uint)meshParts.Length) - { - throw new InvalidOperationException( - $"Packed entity {projection.Id} exceeds mesh " - + "part storage."); - } - - RenderInstanceCandidate candidate = - RenderInstanceCandidate.FromFrame( - in source, - tupleLandblockId); - ClassifyPackedEntity( - in projection, - in candidate, - meshParts.Slice( - source.MeshPartOffset, - source.MeshPartCount), - ref anyVao); - } + PackedRangeClassification classified = + ClassifyPackedRange( + in view, + rangeIndex, + tupleLandblockId, + publishSelection: false); bool deferTransparent = ShouldDeferPackedTransparent( - anyVao, + classified.AnyVao, _alphaQueue?.IsCollecting == true); InstanceLayoutCounts counts = PartitionInstanceGroups( _packedGroups.Values, @@ -176,11 +105,236 @@ public sealed unsafe partial class WbDrawDispatcher _packedClassificationCache.EndFrame(); } + internal void BeginPackedProductionFrame( + in RenderFrameView view) + { + if (_packedProductionFrameOpen) + { + throw new InvalidOperationException( + "The packed dispatcher cannot begin a second production frame."); + } + + BeginPackedFrameStorage(in view); + _packedProductionFrameOpen = true; + _packedProductionGeneration = view.Generation; + _packedProductionFrameSequence = view.FrameSequence; + _packedProductionNextRange = 0; + } + + internal bool DrawPackedProductionRoute( + ICamera camera, + in RenderFrameView view, + RenderFrameCandidateRoute route, + int routeIndex, + uint cellId, + uint tupleLandblockId) + { + ValidatePackedProductionView(in view); + ReadOnlySpan ranges = + view.RouteRanges; + if (_packedProductionNextRange >= ranges.Length) + return false; + + RenderFrameCandidateRange range = + ranges[_packedProductionNextRange]; + if (range.Route != route + || range.RouteIndex != routeIndex + || range.CellId != cellId) + { + return false; + } + + int rangePosition = _packedProductionNextRange++; + bool diagnosticsEnabled = BeginEntityDispatch( + camera, + out Matrix4x4 viewProjection, + out Vector3 cameraWorldPosition); + PackedRangeClassification classified = + ClassifyPackedRange( + in view, + rangePosition, + tupleLandblockId, + publishSelection: true); + ExecuteClassifiedGroups( + viewProjection, + cameraWorldPosition, + classified.AnyVao, + _packedGroups.Values, + EntitySet.All, + classified.CandidateCount, + classified.MeshPartCount, + diagnosticsEnabled, + observeCurrentPath: false); + return true; + } + + internal void CompletePackedProductionFrame( + in RenderFrameView view) + { + ValidatePackedProductionView(in view); + int expected = view.RouteRanges.Length; + if (_packedProductionNextRange != expected) + { + throw new InvalidOperationException( + "The packed dispatcher did not consume the complete route stream: " + + $"consumed={_packedProductionNextRange} expected={expected}."); + } + + _packedClassificationCache.EndFrame(); + ResetPackedProductionFrame(); + } + + internal void AbortPackedProductionFrame() + { + if (!_packedProductionFrameOpen) + return; + + _packedClassificationCache.EndFrame(); + ResetPackedProductionFrame(); + } + + private void BeginPackedFrameStorage( + in RenderFrameView view) + { + if (_packedGroupFrame == long.MaxValue) + { + throw new InvalidOperationException( + "Packed dispatcher frame identity was exhausted."); + } + + _packedGroupFrame++; + PruneInstanceGroupsUnusedBeforeFrame( + _packedGroups, + _packedRetiredGroupKeys, + _packedGroupFrame - 1); + _packedEntityById.Clear(); + _packedSubmissions.Clear(); + _packedSelectionParts.Clear(); + _packedSelectionKeys.Clear(); + _packedClassificationCache.BeginFrame(view.Generation); + + ReadOnlySpan entities = + view.EntityCandidates; + for (int index = 0; index < entities.Length; index++) + { + if (!_packedEntityById.TryAdd( + entities[index].Projection.Id, + entities[index])) + { + throw new InvalidOperationException( + "Packed dispatcher received duplicate projection " + + $"{entities[index].Projection.Id}."); + } + } + } + + private PackedRangeClassification ClassifyPackedRange( + in RenderFrameView view, + int rangeIndex, + uint tupleLandblockId, + bool publishSelection) + { + ReadOnlySpan ranges = + view.RouteRanges; + if ((uint)rangeIndex >= (uint)ranges.Length) + throw new ArgumentOutOfRangeException(nameof(rangeIndex)); + + _nextPackedInstanceSubmissionOrder = 0; + foreach (InstanceGroup group in _packedGroups.Values) + group.ClearPerInstanceData(); + + uint anyVao = 0; + int meshPartCount = 0; + RenderFrameCandidateRange range = ranges[rangeIndex]; + ReadOnlySpan routeCandidates = + view.RouteCandidates; + ReadOnlySpan meshParts = view.MeshParts; + int end = checked(range.Offset + range.Count); + if ((uint)range.Offset > (uint)routeCandidates.Length + || (uint)end > (uint)routeCandidates.Length) + { + throw new InvalidOperationException( + $"Packed dispatcher route {rangeIndex} exceeds candidate storage."); + } + + for (int candidateIndex = range.Offset; + candidateIndex < end; + candidateIndex++) + { + RenderProjectionRecord projection = + routeCandidates[candidateIndex]; + if ((projection.Flags & RenderProjectionFlags.Draw) == 0) + continue; + if (!_packedEntityById.TryGetValue( + projection.Id, + out RenderFrameEntityCandidate source)) + { + throw new InvalidOperationException( + $"Packed route references absent entity {projection.Id}."); + } + + int meshEnd = checked( + source.MeshPartOffset + source.MeshPartCount); + if ((uint)source.MeshPartOffset > (uint)meshParts.Length + || (uint)meshEnd > (uint)meshParts.Length) + { + throw new InvalidOperationException( + $"Packed entity {projection.Id} exceeds mesh part storage."); + } + + meshPartCount = checked( + meshPartCount + source.MeshPartCount); + RenderInstanceCandidate candidate = + RenderInstanceCandidate.FromFrame( + in source, + tupleLandblockId); + ClassifyPackedEntity( + in projection, + in candidate, + meshParts.Slice( + source.MeshPartOffset, + source.MeshPartCount), + ref anyVao, + publishSelection); + } + + return new PackedRangeClassification( + anyVao, + range.Count, + meshPartCount); + } + + private void ValidatePackedProductionView( + in RenderFrameView view) + { + if (!_packedProductionFrameOpen + || view.Generation != _packedProductionGeneration + || view.FrameSequence != _packedProductionFrameSequence) + { + throw new InvalidOperationException( + "The packed dispatcher received a stale or foreign production frame."); + } + } + + private void ResetPackedProductionFrame() + { + _packedProductionFrameOpen = false; + _packedProductionGeneration = default; + _packedProductionFrameSequence = 0; + _packedProductionNextRange = 0; + } + + private readonly record struct PackedRangeClassification( + uint AnyVao, + int CandidateCount, + int MeshPartCount); + private void ClassifyPackedEntity( in RenderProjectionRecord projection, in RenderInstanceCandidate entity, ReadOnlySpan meshParts, - ref uint anyVao) + ref uint anyVao, + bool publishSelection) { (uint slot, bool culled) = ResolveSlotForFrame( _clipRoutingActive, @@ -231,7 +385,8 @@ public sealed unsafe partial class WbDrawDispatcher slot, lights, indoor, - selectionLighting); + selectionLighting, + publishSelection); return; } @@ -272,6 +427,8 @@ public sealed unsafe partial class WbDrawDispatcher if (renderData is null) { reusableAcrossFrames = false; + if (_missRequested.Add(meshRef.GfxObjId)) + _meshAdapter.EnsureLoaded(meshRef.GfxObjId); continue; } if (anyVao == 0) @@ -291,6 +448,8 @@ public sealed unsafe partial class WbDrawDispatcher if (partData is null) { reusableAcrossFrames = false; + if (_missRequested.Add(gfxObjId)) + _meshAdapter.EnsureLoaded(gfxObjId); continue; } @@ -334,7 +493,8 @@ public sealed unsafe partial class WbDrawDispatcher in entity, selectionPartIndex, (uint)gfxObjId, - model); + model, + publishSelection); } } else @@ -374,7 +534,8 @@ public sealed unsafe partial class WbDrawDispatcher in entity, partIndex, meshRef.GfxObjId, - model); + model, + publishSelection); } } @@ -481,7 +642,8 @@ public sealed unsafe partial class WbDrawDispatcher uint slot, InstanceLightSet lights, bool indoor, - Vector2 selectionLighting) + Vector2 selectionLighting, + bool publishSelection) { for (int i = 0; i < entry.Batches.Count; i++) { @@ -504,7 +666,8 @@ public sealed unsafe partial class WbDrawDispatcher in entity, part.PartIndex, part.GfxObjId, - part.RestPose * entity.RootWorld); + part.RestPose * entity.RootWorld, + publishSelection); } } @@ -593,13 +756,29 @@ public sealed unsafe partial class WbDrawDispatcher in RenderInstanceCandidate entity, int partIndex, uint gfxObjId, - Matrix4x4 localToWorld) + Matrix4x4 localToWorld, + bool publishSelection) { - if (_selectionSink is not IRetailSelectionRenderOracle oracle - || !_packedSelectionKeys.Add(new PackedSelectionKey( + if (!_packedSelectionKeys.Add(new PackedSelectionKey( entity.LocalEntityId, partIndex, - gfxObjId)) + gfxObjId))) + { + return; + } + + if (publishSelection) + { + _selectionSink?.AddVisiblePart( + entity.ServerGuid, + entity.LocalEntityId, + partIndex, + gfxObjId, + localToWorld); + return; + } + + if (_selectionSink is not IRetailSelectionRenderOracle oracle || !oracle.TryCreateVisiblePart( entity.ServerGuid, entity.LocalEntityId, diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs index 27604cbc..1eca6cd5 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs @@ -1466,41 +1466,10 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable HashSet? animatedEntityIds = null, EntitySet set = EntitySet.All) { - _shader.Use(); - _selectionLighting?.TickLighting(); - _indoorProbeFrameCounter++; - var vp = camera.View * camera.Projection; - _shader.SetMatrix4("uViewProjection", vp); - // A7 Fix D D-3/D-4: object path — plain Lambert points + sun. MUST set - // explicitly (shared GL uniform; EnvCellRenderer sets it to 1). - _shader.SetInt("uLightingMode", 0); - // #176 stripe-hunt isolation (ACDREAM_LIGHT_DEBUG) — throwaway diagnostic. - _shader.SetInt("uLightDebug", AcDream.Core.Rendering.RenderingDiagnostics.LightDebugMode); - - // #128 self-heal: fresh re-request dedup per Draw pass. - _missRequested.Clear(); - - bool diag = string.Equals(Environment.GetEnvironmentVariable("ACDREAM_WB_DIAG"), "1", StringComparison.Ordinal); - - if (diag && !_gpuQueriesInitialized) - { - for (int i = 0; i < GpuQueryRingDepth; i++) - { - _gpuQueryOpaque[i] = _gl.GenQuery(); - _gpuQueryTransparent[i] = _gl.GenQuery(); - } - _gpuQueriesInitialized = true; - } - - // Always run the CPU stopwatch — cheap; only logged under diag. - _cpuStopwatch.Restart(); - - // Camera world-space position for front-to-back sort (perf #2). The view - // matrix is the inverse of the camera's world transform, so the world - // translation lives in the inverse's translation row. - Vector3 camPos = Vector3.Zero; - if (Matrix4x4.Invert(camera.View, out var invView)) - camPos = invView.Translation; + bool diag = BeginEntityDispatch( + camera, + out Matrix4x4 vp, + out Vector3 camPos); // ── Phase 1: clear groups, walk entities, build groups ────────────── // Draw is invoked several times per frame (landscape slices, late @@ -2041,11 +2010,71 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable if (RenderingDiagnostics.ProbeClipRouteEnabled && _clipRoutingActive) EmitClipRouteDispatchProbe(probeCulledEntities); + ExecuteClassifiedGroups( + vp, + camPos, + anyVao, + _groups.Values, + set, + walkResult.EntitiesWalked, + _walkScratch.Count, + diag, + observeCurrentPath: true); + } + + private bool BeginEntityDispatch( + ICamera camera, + out Matrix4x4 viewProjection, + out Vector3 cameraWorldPosition) + { + _shader.Use(); + _selectionLighting?.TickLighting(); + _indoorProbeFrameCounter++; + viewProjection = camera.View * camera.Projection; + _shader.SetMatrix4("uViewProjection", viewProjection); + _shader.SetInt("uLightingMode", 0); + _shader.SetInt( + "uLightDebug", + RenderingDiagnostics.LightDebugMode); + _missRequested.Clear(); + + bool diagnosticsEnabled = string.Equals( + Environment.GetEnvironmentVariable("ACDREAM_WB_DIAG"), + "1", + StringComparison.Ordinal); + if (diagnosticsEnabled && !_gpuQueriesInitialized) + { + for (int index = 0; index < GpuQueryRingDepth; index++) + { + _gpuQueryOpaque[index] = _gl.GenQuery(); + _gpuQueryTransparent[index] = _gl.GenQuery(); + } + _gpuQueriesInitialized = true; + } + + _cpuStopwatch.Restart(); + cameraWorldPosition = Vector3.Zero; + if (Matrix4x4.Invert(camera.View, out Matrix4x4 inverseView)) + cameraWorldPosition = inverseView.Translation; + return diagnosticsEnabled; + } + + private void ExecuteClassifiedGroups( + Matrix4x4 vp, + Vector3 camPos, + uint anyVao, + IEnumerable groups, + EntitySet set, + int entitiesWalked, + int tupleCount, + bool diag, + bool observeCurrentPath) + { // Nothing visible — skip the GL pass entirely. if (anyVao == 0) { - LastDrawStats = new DrawStats(set, walkResult.EntitiesWalked, _walkScratch.Count, 0, 0, 0, 0, 0, 0); - ObserveCurrentDispatcherSubmission( + LastDrawStats = new DrawStats(set, entitiesWalked, tupleCount, 0, 0, 0, 0, 0, 0); + ObserveClassifiedDispatcherSubmission(observeCurrentPath, visibleInstanceCount: 0, immediateInstanceCount: 0, deferTransparent: false, @@ -2058,7 +2087,7 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable // ── Phase 3: assign FirstInstance per group, lay matrices contiguously, sort opaque ── bool deferTransparent = _alphaQueue?.IsCollecting == true; var instanceCounts = PartitionInstanceGroups( - _groups.Values, + groups, deferTransparent, camPos, _opaqueDraws, @@ -2067,8 +2096,8 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable int immediateInstances = instanceCounts.ImmediateInstances; if (totalInstances == 0) { - LastDrawStats = new DrawStats(set, walkResult.EntitiesWalked, _walkScratch.Count, 0, 0, 0, 0, 0, 0); - ObserveCurrentDispatcherSubmission( + LastDrawStats = new DrawStats(set, entitiesWalked, tupleCount, 0, 0, 0, 0, 0, 0); + ObserveClassifiedDispatcherSubmission(observeCurrentPath, visibleInstanceCount: 0, immediateInstanceCount: 0, deferTransparent, @@ -2185,15 +2214,15 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable _transparentByteOffset = layout.TransparentByteOffset; LastDrawStats = new DrawStats( set, - walkResult.EntitiesWalked, - _walkScratch.Count, + entitiesWalked, + tupleCount, totalInstances, totalDraws, cullRuns, _opaqueDrawCount, _transparentDrawCount, totalTriangles); - ObserveCurrentDispatcherSubmission( + ObserveClassifiedDispatcherSubmission(observeCurrentPath, totalInstances, immediateInstances, deferTransparent, @@ -2404,6 +2433,7 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable } } + /// /// Phase A8 RR5 (2026-05-26): per-building draw overload. Walks only /// entities whose ParentCellId is in , plus @@ -2559,6 +2589,23 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable observer.ObserveDispatcherSubmission(in submission); } + private void ObserveClassifiedDispatcherSubmission( + bool observeCurrentPath, + int visibleInstanceCount, + int immediateInstanceCount, + bool deferTransparent, + Vector3 cameraWorldPosition) + { + if (observeCurrentPath) + { + ObserveCurrentDispatcherSubmission( + visibleInstanceCount, + immediateInstanceCount, + deferTransparent, + cameraWorldPosition); + } + } + internal static CurrentRenderDispatcherSubmission CreateDispatcherSubmission( int visibleInstanceCount, diff --git a/tests/AcDream.App.Tests/Rendering/ArchRenderSceneTests.cs b/tests/AcDream.App.Tests/Rendering/ArchRenderSceneTests.cs index abdf712c..94750bdb 100644 --- a/tests/AcDream.App.Tests/Rendering/ArchRenderSceneTests.cs +++ b/tests/AcDream.App.Tests/Rendering/ArchRenderSceneTests.cs @@ -575,6 +575,62 @@ public sealed class ArchRenderSceneTests Assert.True(scene.Memory.EstimatedIndexBytes > 0); } + [Fact] + public void IndexRevision_AdvancesOnlyWhenOrderedMembershipCanChange() + { + RenderSceneGeneration generation = Generation(18); + using var scene = new ArchRenderScene(generation); + RenderProjectionRecord original = Record( + 18, + 1, + RenderProjectionClass.OutdoorStatic); + scene.Apply( + [RenderProjectionDelta.Register(generation, 1, original)]); + scene.ClearDirty(); + ulong registeredRevision = scene.OpenQuery().IndexRevision; + + Matrix4x4 movedTransform = + Matrix4x4.CreateTranslation(4, 5, 6); + RenderProjectionRecord moved = original with + { + Transform = new RenderTransform(movedTransform), + PreviousTransform = + new PreviousRenderTransform( + original.Transform.LocalToWorld), + Bounds = new RenderWorldBounds( + new Vector3(3, 4, 5), + new Vector3(5, 6, 7)), + }; + scene.Apply( + [ + RenderProjectionDelta.Update( + RenderProjectionDeltaKind.UpdateTransform, + generation, + 2, + moved), + ]); + + RenderSceneQuery movedQuery = scene.OpenQuery(); + Assert.Equal(registeredRevision, movedQuery.IndexRevision); + Assert.Equal(1, movedQuery.IndexCounts.Dirty); + + RenderProjectionRecord reordered = moved with + { + SortKey = new RenderSortKey(moved.SortKey.Value + 1), + }; + scene.Apply( + [ + RenderProjectionDelta.Update( + RenderProjectionDeltaKind.UpdateTransform, + generation, + 3, + reordered), + ]); + + Assert.True( + scene.OpenQuery().IndexRevision > registeredRevision); + } + private static RenderProjectionRecord Record( ulong id, ulong incarnation, diff --git a/tests/AcDream.App.Tests/Rendering/RenderScenePViewFrameProductTests.cs b/tests/AcDream.App.Tests/Rendering/RenderScenePViewFrameProductTests.cs index 6bb4d3ff..9a5b3f5c 100644 --- a/tests/AcDream.App.Tests/Rendering/RenderScenePViewFrameProductTests.cs +++ b/tests/AcDream.App.Tests/Rendering/RenderScenePViewFrameProductTests.cs @@ -133,6 +133,142 @@ public sealed class RenderScenePViewFrameProductTests } } + [Fact] + public void Builder_ReusesOrderedIndicesWhileRefreshingDirtyRecords() + { + using var scene = new ArchRenderScene(Generation); + RenderProjectionRecord later = Record( + 0x0100_0000_0000_0012, + RenderProjectionClass.OutdoorStatic); + RenderProjectionRecord earlier = Record( + 0x0100_0000_0000_0011, + RenderProjectionClass.OutdoorStatic); + scene.Apply( + [ + RenderProjectionDelta.Register(Generation, 1, later), + RenderProjectionDelta.Register(Generation, 2, earlier), + ]); + + PortalVisibilityFrame portal = Portal(Cell); + ClipFrameAssembly clip = FullScreenClip(Cell); + ViewconeCuller viewcone = + ViewconeCuller.Build(clip, Matrix4x4.Identity); + var exchange = new RenderFrameExchange(); + var builder = new RenderScenePViewFrameBuilder(); + + Build(frameSequence: 1); + RenderFrameView first = exchange.BorrowLatest(Generation, 1); + try + { + Assert.Equal( + [earlier.Id, later.Id], + first.OutdoorStaticCandidates.ToArray() + .Select(static record => record.Id)); + } + finally + { + exchange.Release(in first); + } + + scene.ClearDirty(); + ulong retainedRevision = scene.OpenQuery().IndexRevision; + Matrix4x4 movedTransform = + Matrix4x4.CreateTranslation(0.25f, 0, 0); + RenderProjectionRecord moved = later with + { + Transform = new RenderTransform(movedTransform), + PreviousTransform = + new PreviousRenderTransform( + later.Transform.LocalToWorld), + Bounds = new RenderWorldBounds( + new Vector3(-0.75f, -1, -1), + new Vector3(1.25f, 1, 1)), + }; + scene.Apply( + [ + RenderProjectionDelta.Update( + RenderProjectionDeltaKind.UpdateTransform, + Generation, + 3, + moved), + ]); + Assert.Equal( + retainedRevision, + scene.OpenQuery().IndexRevision); + + Build(frameSequence: 2); + RenderFrameView second = exchange.BorrowLatest(Generation, 2); + try + { + RenderProjectionRecord projected = + second.EntityCandidates.ToArray() + .Single(candidate => + candidate.Projection.Id == later.Id) + .Projection; + Assert.Equal( + movedTransform, + projected.Transform.LocalToWorld); + Assert.Equal( + [earlier.Id, later.Id], + second.OutdoorStaticCandidates.ToArray() + .Select(static record => record.Id)); + } + finally + { + exchange.Release(in second); + } + + scene.ClearDirty(); + RenderProjectionRecord reordered = moved with + { + SortKey = new RenderSortKey(0), + }; + scene.Apply( + [ + RenderProjectionDelta.Update( + RenderProjectionDeltaKind.UpdateTransform, + Generation, + 4, + reordered), + ]); + Assert.True( + scene.OpenQuery().IndexRevision > retainedRevision); + + Build(frameSequence: 3); + RenderFrameView third = exchange.BorrowLatest(Generation, 3); + try + { + Assert.Equal( + [later.Id, earlier.Id], + third.OutdoorStaticCandidates.ToArray() + .Select(static record => record.Id)); + } + finally + { + exchange.Release(in third); + } + + void Build(ulong frameSequence) + { + RenderSceneQuery query = scene.OpenQuery(); + var input = new RenderScenePViewBuildInput( + query, + new RenderSceneDigest( + query.Generation, + query.Counts, + default), + portal, + clip, + viewcone, + [], + [Cell], + EmptyCellSource.Instance, + [], + RootIsOutdoor: true); + builder.Build(exchange, frameSequence, in input); + } + } + [Fact] public void Controller_MatchesCurrentPViewThenNamesAWithdrawnCandidate() { diff --git a/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs b/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs index fcb705d3..cc5fc6af 100644 --- a/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs +++ b/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs @@ -158,6 +158,103 @@ public sealed class RetailPViewPassExecutorTests }); } + [Fact] + public void DrawInside_production_product_supplies_the_ordered_entity_routes() + { + LoadedCell interior = InteriorWithExit(0xA9B40100u); + WorldEntity cellStatic = Entity( + 30u, + parentCellId: interior.CellId); + WorldEntity dynamic = Entity( + 31u, + serverGuid: 0x80000031u, + parentCellId: interior.CellId); + uint landblockId = interior.CellId & 0xFFFF0000u; + var oracle = new CurrentRenderSceneOracle(); + using var shadow = new RenderSceneShadowRuntime( + RenderSceneGeneration.FromRaw(1)); + RenderProjectionRecord staticRecord = + RenderProjectionRecordFactory.ProjectEntity( + StaticRenderProjectionJournal.StaticEntityId( + landblockId, + cellStatic.Id), + RenderProjectionClass.IndoorCellStatic, + RenderOwnerIncarnation.FromRaw(cellStatic.Id), + landblockId, + interior.CellId, + cellStatic, + spatiallyVisible: true); + RenderProjectionRecord dynamicRecord = + RenderProjectionRecordFactory.ProjectEntity( + LiveRenderProjectionJournal.ProjectionId(dynamic.Id), + RenderProjectionClass.LiveDynamicRoot, + RenderOwnerIncarnation.FromRaw(dynamic.Id), + landblockId, + interior.CellId, + dynamic, + spatiallyVisible: true); + shadow.Journal.Register(in staticRecord); + shadow.Journal.Register(in dynamicRecord); + shadow.DrainUpdateBoundary(); + var product = new RenderScenePViewFrameProductController( + shadow, + oracle); + var renderer = new RetailPViewRenderer(oracle, product); + using var executor = new RecordingExecutor(); + + renderer.DrawInside( + Frame(interior, [cellStatic, dynamic]), + executor); + + AssertAppearsInOrder( + string.Join('|', executor.Operations), + "entity-frame-begin", + "entity-route:CellStatic:0:00000000", + "entity-route:DynamicLast:0:00000000", + "entity-frame-complete"); + Assert.Equal(0, product.Snapshot.MismatchCount); + Assert.Equal( + product.Snapshot.ExpectedDigest, + product.Snapshot.ActualDigest); + } + + [Fact] + public void DrawInside_production_product_does_not_require_compare_only_referee() + { + LoadedCell interior = InteriorWithExit(0xA9B40100u); + WorldEntity dynamic = Entity( + 32u, + serverGuid: 0x80000032u, + parentCellId: interior.CellId); + uint landblockId = interior.CellId & 0xFFFF0000u; + using var shadow = new RenderSceneShadowRuntime( + RenderSceneGeneration.FromRaw(1)); + RenderProjectionRecord dynamicRecord = + RenderProjectionRecordFactory.ProjectEntity( + LiveRenderProjectionJournal.ProjectionId(dynamic.Id), + RenderProjectionClass.LiveDynamicRoot, + RenderOwnerIncarnation.FromRaw(dynamic.Id), + landblockId, + interior.CellId, + dynamic, + spatiallyVisible: true); + shadow.Journal.Register(in dynamicRecord); + shadow.DrainUpdateBoundary(); + var product = new RenderScenePViewFrameProductController(shadow); + var renderer = new RetailPViewRenderer(null, product); + using var executor = new RecordingExecutor(); + + renderer.DrawInside(Frame(interior, [dynamic]), executor); + + AssertAppearsInOrder( + string.Join('|', executor.Operations), + "entity-frame-begin", + "entity-route:DynamicLast:0:00000000", + "entity-frame-complete"); + Assert.Equal(0uL, product.Snapshot.ComparisonCount); + Assert.Equal(1, product.Snapshot.ActualCandidateCount); + } + [Fact] public void DrawInside_interior_root_executes_the_nearby_building_look_in_punch() { @@ -186,11 +283,11 @@ public sealed class RetailPViewPassExecutorTests drawInside, "passes.BeginFrame();", "passes.EmitDiagnostics(ctx, result);", - "DrawLandscapeThroughOutsideView(ctx, passes", + "DrawLandscapeThroughOutsideView(", "DrawExitPortalMasks(ctx, passes", "DrawEnvCellShells(passes, pvFrame);", - "DrawCellObjectLists(ctx, passes", - "DrawDynamicsLast(ctx, passes"); + "DrawCellObjectLists(", + "DrawDynamicsLast("); string landscape = MethodBody( source, @@ -198,9 +295,9 @@ public sealed class RetailPViewPassExecutorTests AssertAppearsInOrder( landscape, "passes.SetTerrainClip(slice.Planes);", - "passes.DrawLandscapeSlice(ctx", - "DrawBuildingLookIns(ctx, passes", - "passes.DrawLandscapeSliceLate(ctx", + "passes.DrawLandscapeSlice(", + "DrawBuildingLookIns(", + "passes.DrawLandscapeSliceLate(", "passes.DrawUnattachedSceneParticles(ctx);", "passes.FlushLandscapeAlpha();", "passes.ClearInteriorDepth();"); @@ -506,7 +603,10 @@ public sealed class RetailPViewPassExecutorTests public float Aspect { get; set; } = 1f; } - private sealed class RecordingExecutor : IRetailPViewPassExecutor, IDisposable + private sealed class RecordingExecutor : + IRetailPViewPassExecutor, + IRenderFrameEntityPassExecutor, + IDisposable { public void AbortFrame() => Operations.Add("abort"); @@ -559,8 +659,41 @@ public sealed class RetailPViewPassExecutorTests ViewconeCuller viewcone) => Operations.Add("outstage-routing"); public void EmitPhantomObjects(uint cellId, int survivorCount) => Operations.Add("phantom-objects"); - public void DrawLandscapeSlice(RetailPViewFrameInput frame, RetailPViewLandscapeSliceContext context) => Operations.Add("landscape-early"); - public void DrawLandscapeSliceLate(RetailPViewFrameInput frame, RetailPViewLandscapeLateSliceContext context) => Operations.Add("landscape-late"); + public void DrawLandscapeSlice( + RetailPViewFrameInput frame, + RetailPViewLandscapeSliceContext context) + { + Operations.Add("landscape-early"); + if (context.EntityDraw is RenderFrameEntityDrawRequest request) + { + RenderFrameView view = request.View; + DrawEntityRoute( + frame.Camera, + in view, + request.Route, + request.RouteIndex, + request.CellId, + request.TupleLandblockId); + } + } + + public void DrawLandscapeSliceLate( + RetailPViewFrameInput frame, + RetailPViewLandscapeLateSliceContext context) + { + Operations.Add("landscape-late"); + if (context.EntityDraw is RenderFrameEntityDrawRequest request) + { + RenderFrameView view = request.View; + DrawEntityRoute( + frame.Camera, + in view, + request.Route, + request.RouteIndex, + request.CellId, + request.TupleLandblockId); + } + } public void ClearInteriorDepth() => Operations.Add("interior-depth-clear"); public void DrawExitPortalMask(RetailPViewFrameInput frame, RetailPViewCellSliceContext context) => Operations.Add("exit-mask"); public void DrawLookInPortalPunch(RetailPViewFrameInput frame, RetailPViewCellSliceContext context) => Operations.Add("look-in-punch"); @@ -570,6 +703,28 @@ public sealed class RetailPViewPassExecutorTests public void DrawDynamicsParticles(RetailPViewFrameInput frame, IReadOnlyList survivors) => Operations.Add("dynamics-particles"); public void EmitDiagnostics(RetailPViewFrameInput frame, RetailPViewFrameResult result) => Operations.Add("diagnostics"); + public void BeginEntityFrame(in RenderFrameView view) => + Operations.Add("entity-frame-begin"); + + public bool DrawEntityRoute( + ICamera camera, + in RenderFrameView view, + RenderFrameCandidateRoute route, + int routeIndex, + uint cellId, + uint tupleLandblockId) + { + Operations.Add( + $"entity-route:{route}:{routeIndex}:{cellId:X8}"); + return true; + } + + public void CompleteEntityFrame(in RenderFrameView view) => + Operations.Add("entity-frame-complete"); + + public void AbortEntityFrame() => + Operations.Add("entity-frame-abort"); + public void Dispose() => _clipFrame.Dispose(); } }