Reapply "perf(rendering): draw retained frame product"
This reverts commit 2c848d4167.
This commit is contained in:
parent
823936ec31
commit
20f9fadb12
13 changed files with 1333 additions and 267 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<uint> 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<Exception>? 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,
|
||||
|
|
|
|||
|
|
@ -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<uint> 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<WorldEntity> legacyEntities,
|
||||
HashSet<uint>? 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<WorldEntity> _outdoorStaticScratch = new();
|
||||
private readonly List<WorldEntity> _cellStaticScratch = new();
|
||||
|
|
@ -1300,7 +1446,10 @@ public sealed class RetailPViewFrameResult
|
|||
|
||||
public readonly record struct RetailPViewLandscapeSliceContext(
|
||||
ClipViewSlice Slice,
|
||||
IReadOnlyList<WorldEntity> OutdoorEntities);
|
||||
IReadOnlyList<WorldEntity> OutdoorEntities)
|
||||
{
|
||||
internal RenderFrameEntityDrawRequest? EntityDraw { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>#131/#132: the late landscape phase's per-slice payload —
|
||||
/// outside-stage dynamics to mesh-draw, plus the full scene-particle owner
|
||||
|
|
@ -1308,7 +1457,10 @@ public readonly record struct RetailPViewLandscapeSliceContext(
|
|||
public readonly record struct RetailPViewLandscapeLateSliceContext(
|
||||
ClipViewSlice Slice,
|
||||
IReadOnlyList<WorldEntity> Dynamics,
|
||||
IReadOnlyList<WorldEntity> ParticleOwners);
|
||||
IReadOnlyList<WorldEntity> ParticleOwners)
|
||||
{
|
||||
internal RenderFrameEntityDrawRequest? EntityDraw { get; init; }
|
||||
}
|
||||
|
||||
public readonly record struct RetailPViewCellSliceContext(
|
||||
uint CellId,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
/// </summary>
|
||||
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) =>
|
||||
|
|
|
|||
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -113,16 +113,15 @@ internal interface IRenderFrameProductSnapshotSource
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<string>? 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<PortalVisibilityFrame> lookInFrames,
|
||||
HashSet<uint> drawableCells,
|
||||
IRetailPViewCellSource cells,
|
||||
HashSet<uint>? 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<CurrentRenderPViewCandidateFingerprint> 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<RenderFrameCandidateRange> ranges = view.RouteRanges;
|
||||
ReadOnlySpan<RenderProjectionRecord> records = view.RouteCandidates;
|
||||
ReadOnlySpan<RenderFrameCandidateRange> ranges =
|
||||
view.RouteRanges;
|
||||
ReadOnlySpan<RenderProjectionRecord> 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<CurrentRenderDispatcherSubmission> expected =
|
||||
_current.DispatcherSubmissions;
|
||||
Current.DispatcherSubmissions;
|
||||
IReadOnlyList<CurrentRenderDispatcherSubmission> actual =
|
||||
dispatcher.PackedDispatcherSubmissions;
|
||||
RenderSceneHash128 expectedDigest =
|
||||
|
|
@ -426,7 +552,7 @@ internal sealed class RenderScenePViewFrameProductController :
|
|||
}
|
||||
|
||||
IReadOnlyList<CurrentRenderSelectionFingerprint>
|
||||
expectedSelection = _current.SelectionParts;
|
||||
expectedSelection = Current.SelectionParts;
|
||||
IReadOnlyList<CurrentRenderSelectionFingerprint>
|
||||
actualSelection = dispatcher.PackedSelectionParts;
|
||||
BuildSelectionKeys(
|
||||
|
|
@ -628,7 +754,7 @@ internal sealed class RenderScenePViewFrameProductController :
|
|||
_packedActual.Clear();
|
||||
|
||||
IReadOnlyList<CurrentRenderDispatcherFingerprint> 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<RenderProjectionId, int>
|
||||
_outdoorPositions = [];
|
||||
private readonly Dictionary<RenderProjectionId, int>
|
||||
_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<RenderProjectionId, int> positions)
|
||||
{
|
||||
positions.Clear();
|
||||
positions.EnsureCapacity(count);
|
||||
for (int index = 0; index < count; index++)
|
||||
positions.Add(records[index].Id, index);
|
||||
}
|
||||
|
||||
private void BuildOutdoorRoutes(
|
||||
|
|
|
|||
|
|
@ -50,10 +50,12 @@ internal interface IRenderSceneShadowSnapshotSource
|
|||
}
|
||||
|
||||
/// <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.
|
||||
/// 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.
|
||||
/// </summary>
|
||||
internal sealed class RenderSceneShadowRuntime : IDisposable
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<CurrentRenderDispatcherSubmission>
|
||||
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<RenderFrameEntityCandidate> 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<RenderFrameMeshPart> meshParts = view.MeshParts;
|
||||
ReadOnlySpan<RenderProjectionRecord> routeCandidates =
|
||||
view.RouteCandidates;
|
||||
BeginPackedFrameStorage(in view);
|
||||
ReadOnlySpan<RenderFrameCandidateRange> 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<RenderFrameCandidateRange> 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<RenderFrameEntityCandidate> 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<RenderFrameCandidateRange> 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<RenderProjectionRecord> routeCandidates =
|
||||
view.RouteCandidates;
|
||||
ReadOnlySpan<RenderFrameMeshPart> 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<RenderFrameMeshPart> 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,
|
||||
|
|
|
|||
|
|
@ -1483,41 +1483,10 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
HashSet<uint>? 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
|
||||
|
|
@ -2058,11 +2027,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<InstanceGroup> 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,
|
||||
|
|
@ -2075,7 +2104,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,
|
||||
|
|
@ -2084,8 +2113,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,
|
||||
|
|
@ -2202,15 +2231,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,
|
||||
|
|
@ -2421,6 +2450,7 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Phase A8 RR5 (2026-05-26): per-building draw overload. Walks only
|
||||
/// entities whose ParentCellId is in <paramref name="cellIds"/>, plus
|
||||
|
|
@ -2576,6 +2606,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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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();");
|
||||
|
|
@ -504,7 +601,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");
|
||||
|
||||
|
|
@ -557,8 +657,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");
|
||||
|
|
@ -568,6 +701,28 @@ public sealed class RetailPViewPassExecutorTests
|
|||
public void DrawDynamicsParticles(RetailPViewFrameInput frame, IReadOnlyList<WorldEntity> 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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue