perf(rendering): draw retained frame product
Make the incremental render scene the production entity source at the existing retail PView stages while retaining the accepted dispatcher upload and draw executor. Keep diagnostics consumer-gated, retain ordered indices across unchanged frames, refresh only dirty records, and preserve exact mesh-load, selection, alpha, lighting, and route lifecycle semantics.
This commit is contained in:
parent
c7d7848dd2
commit
ef1d263337
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();
|
||||
|
|
@ -1241,7 +1387,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
|
||||
|
|
@ -1249,7 +1398,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,
|
||||
|
|
|
|||
|
|
@ -1466,41 +1466,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
|
||||
|
|
@ -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<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,
|
||||
|
|
@ -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
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Phase A8 RR5 (2026-05-26): per-building draw overload. Walks only
|
||||
/// entities whose ParentCellId is in <paramref name="cellIds"/>, 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,
|
||||
|
|
|
|||
|
|
@ -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();");
|
||||
|
|
@ -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<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