feat(rendering): index incremental scene queries

Maintain packed render worksets as lifecycle deltas arrive and synchronize active animated statics without scanning the resident world. This keeps Slice F non-drawing while preparing exact spatial and feature queries for shadow comparison.

Release: 8,206 passed, 5 skipped.
This commit is contained in:
Erik 2026-07-24 22:12:26 +02:00
parent 58e7c2eb99
commit ff5d86175f
8 changed files with 745 additions and 16 deletions

View file

@ -253,7 +253,8 @@ public sealed class ArchRenderSceneTests
Assert.True(memory.EstimatedProjectionLookupBytes > 0);
Assert.Equal(
memory.EstimatedChunkPayloadBytes
+ memory.EstimatedProjectionLookupBytes,
+ memory.EstimatedProjectionLookupBytes
+ memory.EstimatedIndexBytes,
memory.TotalEstimatedBytes);
}
@ -359,6 +360,182 @@ public sealed class ArchRenderSceneTests
Assert.Equal(0, scene.Counts.Total);
}
[Fact]
public void IncrementalIndices_PreserveOutdoorCellDynamicAndFeatureMembership()
{
const uint indoorCell = 0x12340100u;
RenderSceneGeneration generation = Generation(16);
using var scene = new ArchRenderScene(generation);
RenderProjectionRecord outdoorStatic = Record(
1,
1,
RenderProjectionClass.OutdoorStatic);
RenderProjectionRecord indoorStatic = Record(
2,
1,
RenderProjectionClass.IndoorCellStatic) with
{
Residency = new RenderSpatialResidency(
Bucket(indoorCell),
0x1234FFFFu,
indoorCell),
Source = new RenderSourceMetadata() with
{
ParentCellId = indoorCell,
},
};
RenderProjectionRecord animatedStatic = Record(
3,
1,
RenderProjectionClass.ActiveAnimatedStatic);
RenderProjectionRecord outdoorDynamic = Record(
4,
1,
RenderProjectionClass.LiveDynamicRoot);
RenderProjectionRecord cellDynamic = Record(
5,
1,
RenderProjectionClass.LiveDynamicRoot) with
{
Residency = new RenderSpatialResidency(
Bucket(indoorCell),
0x1234FFFFu,
indoorCell),
Source = new RenderSourceMetadata() with
{
ParentCellId = indoorCell,
},
};
RenderProjectionRecord equipped = Record(
6,
1,
RenderProjectionClass.EquippedChild) with
{
Flags = RenderProjectionFlags.Draw
| RenderProjectionFlags.Selectable
| RenderProjectionFlags.Translucent
| RenderProjectionFlags.LightCandidate
| RenderProjectionFlags.PortalStraddling,
};
RenderProjectionRecord[] records =
[
outdoorStatic,
indoorStatic,
animatedStatic,
outdoorDynamic,
cellDynamic,
equipped,
];
var deltas = new RenderProjectionDelta[records.Length];
for (int i = 0; i < records.Length; i++)
{
deltas[i] = RenderProjectionDelta.Register(
generation,
(ulong)i + 1,
records[i]);
}
scene.Apply(deltas);
RenderSceneQuery query = scene.OpenQuery();
Assert.Equal(
new RenderSceneIndexCounts(
OutdoorStatic: 2,
IndoorCellStatic: 1,
Dynamic: 3,
OutdoorDynamic: 2,
PortalStraddlingDynamic: 1,
Translucent: 1,
Selectable: 6,
LightCandidate: 1,
Dirty: 6),
query.IndexCounts);
Assert.Equal(1, query.GetCellStaticCount(indoorCell));
Assert.Equal(1, query.GetCellDynamicCount(indoorCell));
var cellRecords = new RenderProjectionRecord[1];
Assert.Equal(1, query.CopyCellStaticsTo(indoorCell, cellRecords));
Assert.Equal(indoorStatic.Id, cellRecords[0].Id);
Assert.Equal(1, query.CopyCellDynamicsTo(indoorCell, cellRecords));
Assert.Equal(cellDynamic.Id, cellRecords[0].Id);
Assert.True(scene.Memory.EstimatedIndexBytes > 0);
}
[Fact]
public void IncrementalIndices_UpdateRebucketReplaceRemoveAndDirtyAcknowledge()
{
const uint indoorCell = 0x22220100u;
RenderSceneGeneration generation = Generation(17);
using var scene = new ArchRenderScene(generation);
RenderProjectionRecord original = Record(
17,
1,
RenderProjectionClass.LiveDynamicRoot);
scene.Apply(
[RenderProjectionDelta.Register(generation, 1, original)]);
scene.ClearDirty();
Assert.Equal(0, scene.OpenQuery().IndexCounts.Dirty);
RenderProjectionRecord flagged = original with
{
Flags = original.Flags
| RenderProjectionFlags.Translucent
| RenderProjectionFlags.LightCandidate
| RenderProjectionFlags.PortalStraddling,
Source = original.Source with { ParentCellId = indoorCell },
};
RenderProjectionRecord rebucketed = flagged with
{
Residency = new RenderSpatialResidency(
Bucket(indoorCell),
0x2222FFFFu,
indoorCell),
};
scene.Apply(
[
RenderProjectionDelta.Update(
RenderProjectionDeltaKind.UpdateFlags,
generation,
2,
flagged),
RenderProjectionDelta.Update(
RenderProjectionDeltaKind.Rebucket,
generation,
3,
rebucketed),
]);
RenderSceneQuery updated = scene.OpenQuery();
Assert.Equal(0, updated.IndexCounts.OutdoorDynamic);
Assert.Equal(1, updated.IndexCounts.PortalStraddlingDynamic);
Assert.Equal(1, updated.IndexCounts.Translucent);
Assert.Equal(1, updated.IndexCounts.LightCandidate);
Assert.Equal(1, updated.IndexCounts.Dirty);
Assert.Equal(1, updated.GetCellDynamicCount(indoorCell));
RenderProjectionRecord replacement = rebucketed with
{
ProjectionClass = RenderProjectionClass.IndoorCellStatic,
OwnerIncarnation = Incarnation(2),
};
scene.Apply(
[RenderProjectionDelta.Register(generation, 4, replacement)]);
RenderSceneQuery replaced = scene.OpenQuery();
Assert.Equal(0, replaced.IndexCounts.Dynamic);
Assert.Equal(1, replaced.IndexCounts.IndoorCellStatic);
Assert.Equal(1, replaced.GetCellStaticCount(indoorCell));
scene.Apply(
[
RenderProjectionDelta.Unregister(
generation,
5,
replacement.Id,
replacement.OwnerIncarnation),
]);
Assert.Equal(default, scene.OpenQuery().IndexCounts);
Assert.True(scene.Memory.EstimatedIndexBytes > 0);
}
private static RenderProjectionRecord Record(
ulong id,
ulong incarnation,

View file

@ -207,6 +207,62 @@ public sealed class StaticRenderProjectionJournalTests
() => statics.Clear(Generation(7)));
}
[Fact]
public void ActiveAnimatedSynchronization_ReclassifiesAndUpdatesOnlyActiveSource()
{
RenderSceneGeneration generation = Generation(8);
var journal = new RenderProjectionJournal(generation);
var statics = new StaticRenderProjectionJournal(journal);
WorldEntity animated = Entity(1);
WorldEntity ordinary = Entity(2);
LandblockBuild build = Build(
LandblockId,
[animated, ordinary],
includeShell: false);
statics.Reconcile(build, Publication(build));
using var scene = new ArchRenderScene(generation);
journal.DrainTo(scene);
RenderProjectionId animatedId =
StaticRenderProjectionJournal.StaticEntityId(
LandblockId,
animated.Id);
Assert.True(scene.OpenQuery().TryGet(animatedId, out var before));
statics.SynchronizeActiveAnimatedSources([animated]);
Assert.Single(journal.Pending.ToArray());
Assert.Equal(
RenderProjectionDeltaKind.Register,
journal.Pending[0].Kind);
Assert.Equal(
RenderProjectionClass.ActiveAnimatedStatic,
journal.Pending[0].Record.ProjectionClass);
Assert.Equal(
before.OwnerIncarnation,
journal.Pending[0].Record.OwnerIncarnation);
journal.DrainTo(scene);
statics.SynchronizeActiveAnimatedSources([animated]);
Assert.Equal(0, journal.Count);
animated.Rotation =
Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 0.75f);
statics.SynchronizeActiveAnimatedSources([animated]);
Assert.Single(journal.Pending.ToArray());
Assert.Equal(
RenderProjectionDeltaKind.UpdateTransform,
journal.Pending[0].Kind);
Assert.True(statics.TryGet(animatedId, out var updated));
Assert.Equal(animated.Rotation, updated.Transform.Rotation);
RenderProjectionId ordinaryId =
StaticRenderProjectionJournal.StaticEntityId(
LandblockId,
ordinary.Id);
Assert.True(statics.TryGet(ordinaryId, out var unchanged));
Assert.Equal(
RenderProjectionClass.OutdoorStatic,
unchanged.ProjectionClass);
}
private static GpuLandblockSpatialPublication Publication(
LandblockBuild build,
bool requiresActivation = true) =>