perf(rendering): retire production entity partition

Route production meshes and attached particles from the exact retained PView frame ranges, while keeping the former WorldEntity partition only for standalone tests and explicit diagnostic/oracle probes. Copy retained source/count facts before arena release so world diagnostics no longer require the old partition. Record the accepted G4 visual gate and open the exact G5 production matrix.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-25 10:50:57 +02:00
parent 14b48cf553
commit 10ccce3f2d
11 changed files with 452 additions and 125 deletions

View file

@ -25,6 +25,8 @@ public sealed class RetailPViewRenderer
private static readonly ClipViewSlice NoClipSlice =
new(0, new Vector4(-1f, -1f, 1f, 1f), Array.Empty<Vector4>());
private static readonly ClipViewSlice[] NoClipSlices = { NoClipSlice };
private static readonly IReadOnlySet<uint> NoParticleOwners =
new HashSet<uint>();
private readonly HashSet<uint> _oneCell = new(1);
// Shell-batch scratch: all of a pass's cells collected for ONE batched
@ -50,7 +52,9 @@ public sealed class RetailPViewRenderer
// #131/#132: the late landscape phase's scene-particle owner survivors
// (statics + outside-stage dynamics passing the slice cone).
private readonly List<WorldEntity> _lateParticleOwnerScratch = new();
private readonly HashSet<uint> _lateParticleOwnerScratch = new();
private readonly HashSet<uint> _cellParticleOwnerScratch = new();
private readonly HashSet<uint> _dynamicParticleOwnerScratch = new();
// MP-Alloc (2026-07-05): the frame's entity partition (ByCell/OutdoorStatic/
// Dynamics), reused across frames instead of `new`ing a Result (a Dictionary
@ -58,6 +62,8 @@ public sealed class RetailPViewRenderer
// call. See InteriorEntityPartition.Partition(Result, ...) — clears in
// place and reuses each cell's list across frames when the cell stays
// visible.
// Slice G4: this is now a diagnostic/fallback oracle only. Normal
// production consumes the retained RenderFrameView routes directly.
private readonly InteriorEntityPartition.Result _partitionResult = new();
// MP-Alloc (2026-07-05): DrawInside's drawable-cell set, reused across
@ -174,20 +180,6 @@ public sealed class RetailPViewRenderer
passes.PrepareCellBatches(ctx, prepareCells);
InteriorEntityPartition.Partition(
_partitionResult,
prepareCells,
ctx.LandblockEntries,
_partitionObserver);
var partition = _partitionResult;
RetailPViewFrameResult result = _frameResultScratch.Reset(
pvFrame,
clipAssembly,
drawableCells,
partition);
passes.EmitDiagnostics(ctx, result);
// T1 (fused BR-2/3): retail's frame order — static world, then the
// aperture depth writes, then interior cells WHOLE far→near, then
// per-cell statics, then ALL dynamics last (retail draws objects after
@ -218,6 +210,51 @@ public sealed class RetailPViewRenderer
_candidateObserver?.BeginPViewFrame();
try
{
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;
}
// The retained scene product is the production object source.
// Rebuild the former WorldEntity partition only for the standalone
// fallback and explicitly enabled comparison/probe paths.
InteriorEntityPartition.Result? partition = null;
if (_sceneFrameProduct is null || LegacyPartitionDiagnosticsEnabled)
{
InteriorEntityPartition.Partition(
_partitionResult,
prepareCells,
ctx.LandblockEntries,
_partitionObserver);
partition = _partitionResult;
}
RenderFrameDiagnosticCounts counts = frameViewBorrowed
? frameView.DiagnosticCounts
: LegacyDiagnosticCounts(partition!);
RenderProjectionCounts sourceCounts = frameViewBorrowed
? frameView.SourceDigest.Counts
: LegacySourceCounts(partition!);
RetailPViewFrameResult result = _frameResultScratch.Reset(
pvFrame,
clipAssembly,
drawableCells,
counts,
sourceCounts,
partition);
passes.EmitDiagnostics(ctx, result);
// #118: stage assignment for dynamics under an INTERIOR root. Retail
// draws the OUTSIDE world's objects inside the landscape stage —
// PView::DrawCells runs LScape::draw FIRST (pc:432719), then the gated
@ -235,7 +272,7 @@ public sealed class RetailPViewRenderer
// z-buffered equivalent of retail's painter-ordered outdoor pass (the
// BR-2 punch-after-dynamics lesson, reverted 88be519).
_outsideStageDynamics.Clear();
if (!ctx.RootCell.IsOutdoorNode)
if (partition is not null && !ctx.RootCell.IsOutdoorNode)
{
foreach (var e in partition.Dynamics)
{
@ -245,22 +282,6 @@ public sealed class RetailPViewRenderer
}
}
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,
@ -441,7 +462,7 @@ public sealed class RetailPViewRenderer
RetailPViewFrameInput ctx,
IRetailPViewPassExecutor passes,
ClipFrameAssembly clipAssembly,
InteriorEntityPartition.Result partition,
InteriorEntityPartition.Result? partition,
IRenderFrameEntityPassExecutor? frameEntityPasses,
in RenderFrameView frameView)
{
@ -466,7 +487,7 @@ public sealed class RetailPViewRenderer
0,
new Vector4(poly.MinX, poly.MinY, poly.MaxX, poly.MaxY),
cps.PlaneArray),
Array.Empty<WorldEntity>()));
NoParticleOwners));
}
}
@ -494,8 +515,11 @@ public sealed class RetailPViewRenderer
passes.DrawTransparentCellShells(_oneCell);
_cellStaticScratch.Clear();
if (partition.ByCell.TryGetValue(cellId, out var bucket))
if (partition is not null
&& partition.ByCell.TryGetValue(cellId, out var bucket))
{
_cellStaticScratch.AddRange(bucket);
}
// #131 ROOT CAUSE: DYNAMICS living in a look-in cell (the
// Holtburg hall-porch PORTAL, pCell 0xA9B4017A) draw NOWHERE
@ -510,11 +534,31 @@ public sealed class RetailPViewRenderer
// cell is absent from the main cone), and their emitters ride
// the DrawCellParticles call below, not DrawDynamicsParticles
// (which only sees dynamics-last cone survivors).
foreach (var e in partition.Dynamics)
if (e.ParentCellId == cellId)
_cellStaticScratch.Add(e);
if (partition is not null)
{
foreach (var e in partition.Dynamics)
if (e.ParentCellId == cellId)
_cellStaticScratch.Add(e);
}
if (_cellStaticScratch.Count > 0)
if (frameEntityPasses is not null)
{
RenderFrameRouteOwnerSelector.Replace(
_cellParticleOwnerScratch,
in frameView,
RenderFrameCandidateRoute.LookInObject,
i,
cellId);
}
else
{
ReplaceOwnerIds(
_cellParticleOwnerScratch,
_cellStaticScratch);
}
if (frameEntityPasses is not null
|| _cellStaticScratch.Count > 0)
{
_candidateObserver?.ObservePViewBucket(
CurrentRenderPViewRoute.LookInObject,
@ -536,7 +580,7 @@ public sealed class RetailPViewRenderer
// nested DrawCells draws objects WITH their emitters.
foreach (var slice in GetCellSlicesOrNoClip(clipAssembly, cellId))
passes.DrawCellParticles(ctx, new RetailPViewCellSliceContext(
cellId, slice, _cellStaticScratch));
cellId, slice, _cellParticleOwnerScratch));
}
}
}
@ -546,7 +590,7 @@ public sealed class RetailPViewRenderer
RetailPViewFrameInput ctx,
IRetailPViewPassExecutor passes,
ClipFrameAssembly clipAssembly,
InteriorEntityPartition.Result partition,
InteriorEntityPartition.Result? partition,
ViewconeCuller viewcone,
IRenderFrameEntityPassExecutor? frameEntityPasses,
in RenderFrameView frameView)
@ -580,11 +624,19 @@ public sealed class RetailPViewRenderer
passes.EmitClipRouteProbe(clipAssembly, slice, probeSliceIndex);
_outdoorStaticScratch.Clear();
foreach (var e in partition.OutdoorStatic)
if (partition is not null)
{
EntitySphere(e, out var c, out float r);
if (viewcone.SphereVisibleInOutsideSlice(probeSliceIndex, c, r))
_outdoorStaticScratch.Add(e);
foreach (var e in partition.OutdoorStatic)
{
EntitySphere(e, out var c, out float r);
if (viewcone.SphereVisibleInOutsideSlice(
probeSliceIndex,
c,
r))
{
_outdoorStaticScratch.Add(e);
}
}
}
_candidateObserver?.ObservePViewBucket(
CurrentRenderPViewRoute.LandscapeOutdoorStatic,
@ -636,21 +688,27 @@ public sealed class RetailPViewRenderer
_outdoorStaticScratch.Clear(); // late: dynamics survivors
_lateParticleOwnerScratch.Clear(); // late: statics + dynamics survivors
foreach (var e in partition.OutdoorStatic)
if (partition is not null)
{
EntitySphere(e, out var c, out float r);
bool ownerPass = viewcone.SphereVisibleInOutsideSlice(probeSliceIndex, c, r);
if (ownerPass)
_lateParticleOwnerScratch.Add(e);
foreach (var e in partition.OutdoorStatic)
{
EntitySphere(e, out var c, out float r);
bool ownerPass = viewcone.SphereVisibleInOutsideSlice(
probeSliceIndex,
c,
r);
if (ownerPass)
_lateParticleOwnerScratch.Add(e.Id);
// #131 owner watchlist (throwaway): ACDREAM_DUMP_ENTITY ids
// double as an ENTITY-id watchlist here — one line per watched
// outdoor-static owner per CHANGE of its cone verdict.
passes.EmitOutStageOwner(
e,
c,
r,
probeSliceIndex,
ownerPass);
passes.EmitOutStageOwner(
e,
c,
r,
probeSliceIndex,
ownerPass);
}
}
foreach (var e in _outsideStageDynamics)
{
@ -658,9 +716,24 @@ public sealed class RetailPViewRenderer
if (viewcone.SphereVisibleInOutsideSlice(probeSliceIndex, c, r))
{
_outdoorStaticScratch.Add(e);
_lateParticleOwnerScratch.Add(e);
_lateParticleOwnerScratch.Add(e.Id);
}
}
if (frameEntityPasses is not null)
{
RenderFrameRouteOwnerSelector.Replace(
_lateParticleOwnerScratch,
in frameView,
RenderFrameCandidateRoute.LandscapeOutdoorStatic,
probeSliceIndex,
0);
RenderFrameRouteOwnerSelector.Union(
_lateParticleOwnerScratch,
in frameView,
RenderFrameCandidateRoute.LandscapeOutsideDynamic,
probeSliceIndex,
0);
}
passes.EmitOutStageRouting(
probeSliceIndex,
_outsideStageDynamics,
@ -735,7 +808,10 @@ public sealed class RetailPViewRenderer
foreach (var slice in GetCellSlicesOrNoClip(clipAssembly, cellId))
passes.DrawExitPortalMask(
ctx,
new RetailPViewCellSliceContext(cellId, slice, Array.Empty<WorldEntity>()));
new RetailPViewCellSliceContext(
cellId,
slice,
NoParticleOwners));
}
}
@ -795,13 +871,51 @@ public sealed class RetailPViewRenderer
private void DrawDynamicsLast(
RetailPViewFrameInput ctx,
IRetailPViewPassExecutor passes,
InteriorEntityPartition.Result partition,
InteriorEntityPartition.Result? partition,
ViewconeCuller viewcone,
bool rootIsOutdoor,
IRenderFrameEntityPassExecutor? frameEntityPasses,
in RenderFrameView frameView)
{
if (partition.Dynamics.Count == 0)
if (partition is null)
{
RenderFrameRouteOwnerSelector.Replace(
_dynamicParticleOwnerScratch,
in frameView,
RenderFrameCandidateRoute.DynamicLast,
0,
0);
passes.UseIndoorMembershipOnlyRouting();
DrawEntityRouteOrLegacy(
ctx,
passes,
frameEntityPasses,
in frameView,
RenderFrameCandidateRoute.DynamicLast,
0,
0,
Array.Empty<WorldEntity>(),
visibleCellIds: null);
// An owner routed through any pre-clear outside slice already had
// its alpha particles drawn there. Meshes may be submitted in both
// stages, but particles must be emitted exactly once.
RenderFrameRouteOwnerSelector.ExceptRoute(
_dynamicParticleOwnerScratch,
in frameView,
RenderFrameCandidateRoute.LandscapeOutsideDynamic);
if (_dynamicParticleOwnerScratch.Count > 0)
{
passes.DrawDynamicsParticles(
ctx,
_dynamicParticleOwnerScratch);
}
return;
}
if (partition.Dynamics.Count == 0
&& frameEntityPasses is null)
return;
_dynamicsScratch.Clear();
@ -837,14 +951,18 @@ public sealed class RetailPViewRenderer
_dynamicsScratch.Add(e);
}
if (_dynamicsScratch.Count == 0)
if (_dynamicsScratch.Count == 0
&& frameEntityPasses is null)
return;
_candidateObserver?.ObservePViewBucket(
CurrentRenderPViewRoute.DynamicLast,
0,
0,
_dynamicsScratch);
if (_dynamicsScratch.Count > 0)
{
_candidateObserver?.ObservePViewBucket(
CurrentRenderPViewRoute.DynamicLast,
0,
0,
_dynamicsScratch);
}
passes.UseIndoorMembershipOnlyRouting();
DrawEntityRouteOrLegacy(
ctx,
@ -867,12 +985,28 @@ public sealed class RetailPViewRenderer
// portals went invisible. Outside-stage dynamics are excluded here:
// their emitters already drew in the landscape slice (alpha-blended
// particles must not double-draw, unlike the depth-idempotent meshes).
_dynamicsParticleScratch.Clear();
foreach (var e in _dynamicsScratch)
if (!_outsideStageDynamics.Contains(e))
_dynamicsParticleScratch.Add(e);
if (_dynamicsParticleScratch.Count > 0)
passes.DrawDynamicsParticles(ctx, _dynamicsParticleScratch);
if (frameEntityPasses is not null)
{
RenderFrameRouteOwnerSelector.Replace(
_dynamicParticleOwnerScratch,
in frameView,
RenderFrameCandidateRoute.DynamicLast,
0,
0);
RenderFrameRouteOwnerSelector.ExceptRoute(
_dynamicParticleOwnerScratch,
in frameView,
RenderFrameCandidateRoute.LandscapeOutsideDynamic);
}
else
{
_dynamicParticleOwnerScratch.Clear();
foreach (var e in _dynamicsScratch)
if (!_outsideStageDynamics.Contains(e))
_dynamicParticleOwnerScratch.Add(e.Id);
}
if (_dynamicParticleOwnerScratch.Count > 0)
passes.DrawDynamicsParticles(ctx, _dynamicParticleOwnerScratch);
}
private void DrawCellObjectLists(
@ -881,11 +1015,40 @@ public sealed class RetailPViewRenderer
PortalVisibilityFrame pvFrame,
ClipFrameAssembly clipAssembly,
HashSet<uint> drawableCells,
InteriorEntityPartition.Result partition,
InteriorEntityPartition.Result? partition,
ViewconeCuller viewcone,
IRenderFrameEntityPassExecutor? frameEntityPasses,
in RenderFrameView frameView)
{
if (partition is null)
{
RenderFrameRouteOwnerSelector.Replace(
_cellParticleOwnerScratch,
in frameView,
RenderFrameCandidateRoute.CellStatic,
0,
0);
passes.UseIndoorMembershipOnlyRouting();
DrawEntityRouteOrLegacy(
ctx,
passes,
frameEntityPasses,
in frameView,
RenderFrameCandidateRoute.CellStatic,
0,
0,
Array.Empty<WorldEntity>(),
visibleCellIds: null);
passes.DrawCellParticles(
ctx,
new RetailPViewCellSliceContext(
0u,
NoClipSlice,
_cellParticleOwnerScratch));
return;
}
// T1: per-cell STATIC object lists only (dat-baked 0x40 statics) —
// dynamics moved to DrawDynamicsLast. Far→near with the cells, after
// the shells (retail DrawCells epilogue: PortalList = cell's views →
@ -940,7 +1103,8 @@ public sealed class RetailPViewRenderer
// draw in DrawDynamicsLast. T3 (BR-5): each static was sphere-tested against
// ITS cell's views above (the statics-through-walls fix is preserved by the
// cull; only the draw is batched).
if (_allCellStatics.Count > 0)
if (frameEntityPasses is not null
|| _allCellStatics.Count > 0)
{
_candidateObserver?.ObservePViewBucket(
CurrentRenderPViewRoute.CellStatic,
@ -974,10 +1138,31 @@ public sealed class RetailPViewRenderer
// the buffer (the statics-before-particles order). cellId/slice are unused
// by the particle pass — pass NoClipSlice + the union owner list. This also
// drops the per-cell BuildDrawList allocations (N → 1).
if (_allCellStatics.Count > 0)
if (frameEntityPasses is not null
|| _allCellStatics.Count > 0)
{
if (frameEntityPasses is not null)
{
RenderFrameRouteOwnerSelector.Replace(
_cellParticleOwnerScratch,
in frameView,
RenderFrameCandidateRoute.CellStatic,
0,
0);
}
else
{
ReplaceOwnerIds(
_cellParticleOwnerScratch,
_allCellStatics);
}
passes.DrawCellParticles(
ctx,
new RetailPViewCellSliceContext(0u, NoClipSlice, _allCellStatics));
new RetailPViewCellSliceContext(
0u,
NoClipSlice,
_cellParticleOwnerScratch));
}
}
private static void DrawEntityRouteOrLegacy(
@ -1016,9 +1201,6 @@ public sealed class RetailPViewRenderer
// #118: dynamics assigned to the OUTSIDE stage this frame (interior roots
// only) — outdoor-classified + exit-portal straddlers. Cleared per frame.
private readonly List<WorldEntity> _outsideStageDynamics = new();
// #121: cone-surviving dynamics whose emitters draw in the dynamics
// particle pass (survivors minus outside-stage). Cleared per use.
private readonly List<WorldEntity> _dynamicsParticleScratch = new();
// Dense-town FPS iteration-1 (cellobject batching): all visible cells'
// viewcone-surviving statics accumulated for ONE batched DrawEntityBucket,
// plus the union of their cell ids for the dispatcher's visibleCellIds gate.
@ -1026,6 +1208,70 @@ public sealed class RetailPViewRenderer
private readonly List<WorldEntity> _allCellStatics = new();
private readonly HashSet<uint> _cellObjCells = new();
private bool LegacyPartitionDiagnosticsEnabled =>
_partitionObserver is not null
|| AcDream.Core.Rendering.RenderingDiagnostics.ProbeOutStageEnabled
|| AcDream.Core.Rendering.RenderingDiagnostics.ProbePhantomEnabled
|| AcDream.Core.Rendering.RenderingDiagnostics.ProbeFlapEnabled
|| AcDream.App.Streaming.EntityVanishProbe.Enabled;
internal static RenderFrameDiagnosticCounts LegacyDiagnosticCounts(
InteriorEntityPartition.Result partition)
{
int cellStaticCount = 0;
foreach (List<WorldEntity> bucket in partition.ByCell.Values)
cellStaticCount = checked(cellStaticCount + bucket.Count);
return new RenderFrameDiagnosticCounts(
partition.OutdoorStatic.Count,
cellStaticCount,
partition.Dynamics.Count,
TransformCount: 0,
OpaqueClassificationCount: 0,
AlphaClassificationCount: 0,
LightSetCount: 0,
SelectionPartCount: 0,
RouteCandidateCount:
checked(
partition.OutdoorStatic.Count
+ cellStaticCount
+ partition.Dynamics.Count),
EntityCandidateCount: 0,
MeshPartCount: 0);
}
internal static RenderProjectionCounts LegacySourceCounts(
InteriorEntityPartition.Result partition)
{
int cellStaticCount = 0;
foreach (List<WorldEntity> bucket in partition.ByCell.Values)
cellStaticCount = checked(cellStaticCount + bucket.Count);
int total = checked(
partition.OutdoorStatic.Count
+ cellStaticCount
+ partition.Dynamics.Count);
return new RenderProjectionCounts(
total,
partition.OutdoorStatic.Count,
cellStaticCount,
partition.Dynamics.Count,
ActiveAnimatedStatic: 0,
EquippedChild: 0);
}
private static void ReplaceOwnerIds(
HashSet<uint> destination,
IReadOnlyList<WorldEntity> entities)
{
destination.Clear();
for (int index = 0; index < entities.Count; index++)
{
uint localEntityId = entities[index].Id;
if (localEntityId != 0)
destination.Add(localEntityId);
}
}
/// <summary>
/// #118 stage assignment for a dynamic under an INTERIOR root: does it draw
/// in the OUTSIDE (landscape) stage — before the gated depth clear and the
@ -1153,7 +1399,7 @@ public interface IRetailPViewPassExecutor
void DrawUnattachedSceneParticles(RetailPViewFrameInput frame);
void FlushLandscapeAlpha();
void DrawCellParticles(RetailPViewFrameInput frame, RetailPViewCellSliceContext context);
void DrawDynamicsParticles(RetailPViewFrameInput frame, IReadOnlyList<WorldEntity> survivors);
void DrawDynamicsParticles(RetailPViewFrameInput frame, IReadOnlySet<uint> ownerIds);
void EmitDiagnostics(RetailPViewFrameInput frame, RetailPViewFrameResult result);
}
@ -1428,20 +1674,42 @@ public sealed class RetailPViewFrameResult
public PortalVisibilityFrame PortalFrame { get; private set; } = null!;
public ClipFrameAssembly ClipAssembly { get; private set; } = null!;
public HashSet<uint> DrawableCells { get; private set; } = null!;
public InteriorEntityPartition.Result Partition { get; private set; } = null!;
internal RenderFrameDiagnosticCounts DiagnosticCounts { get; private set; }
internal RenderProjectionCounts SourceCounts { get; private set; }
internal InteriorEntityPartition.Result? DiagnosticPartition
{ get; private set; }
internal RetailPViewFrameResult Reset(
PortalVisibilityFrame portalFrame,
ClipFrameAssembly clipAssembly,
HashSet<uint> drawableCells,
InteriorEntityPartition.Result partition)
RenderFrameDiagnosticCounts diagnosticCounts,
RenderProjectionCounts sourceCounts,
InteriorEntityPartition.Result? diagnosticPartition)
{
PortalFrame = portalFrame;
ClipAssembly = clipAssembly;
DrawableCells = drawableCells;
Partition = partition;
DiagnosticCounts = diagnosticCounts;
SourceCounts = sourceCounts;
DiagnosticPartition = diagnosticPartition;
return this;
}
internal RetailPViewFrameResult Reset(
PortalVisibilityFrame portalFrame,
ClipFrameAssembly clipAssembly,
HashSet<uint> drawableCells,
InteriorEntityPartition.Result diagnosticPartition) =>
Reset(
portalFrame,
clipAssembly,
drawableCells,
RetailPViewRenderer.LegacyDiagnosticCounts(
diagnosticPartition),
RetailPViewRenderer.LegacySourceCounts(
diagnosticPartition),
diagnosticPartition);
}
public readonly record struct RetailPViewLandscapeSliceContext(
@ -1457,7 +1725,7 @@ public readonly record struct RetailPViewLandscapeSliceContext(
public readonly record struct RetailPViewLandscapeLateSliceContext(
ClipViewSlice Slice,
IReadOnlyList<WorldEntity> Dynamics,
IReadOnlyList<WorldEntity> ParticleOwners)
IReadOnlySet<uint> ParticleOwnerIds)
{
internal RenderFrameEntityDrawRequest? EntityDraw { get; init; }
}
@ -1465,4 +1733,4 @@ public readonly record struct RetailPViewLandscapeLateSliceContext(
public readonly record struct RetailPViewCellSliceContext(
uint CellId,
ClipViewSlice Slice,
IReadOnlyList<WorldEntity> CellEntities);
IReadOnlySet<uint> ParticleOwnerIds);