perf(rendering): retain packed classification

Retain appearance classification per projection while refreshing transforms, clip slots, lights, selection lighting, and group instance payloads each frame. Incomplete resources and active animation remain on the live classification path, and only an exact full packed comparison acknowledges scene dirtiness.
This commit is contained in:
Erik 2026-07-25 02:46:40 +02:00
parent f10dec58ba
commit 54d17eb446
7 changed files with 731 additions and 30 deletions

View file

@ -30,6 +30,8 @@ public sealed unsafe partial class WbDrawDispatcher
private readonly List<CurrentRenderSelectionFingerprint>
_packedSelectionParts = [];
private readonly HashSet<PackedSelectionKey> _packedSelectionKeys = [];
private readonly PackedProjectionClassificationCache
_packedClassificationCache = new();
private long _packedGroupFrame;
private long _nextPackedGroupRegistration = 1;
private int _nextPackedInstanceSubmissionOrder;
@ -40,6 +42,10 @@ public sealed unsafe partial class WbDrawDispatcher
internal IReadOnlyList<CurrentRenderSelectionFingerprint>
PackedSelectionParts => _packedSelectionParts;
internal PackedClassificationCacheSnapshot
PackedClassificationSnapshot =>
_packedClassificationCache.Snapshot;
internal void BuildPackedDispatcherOracle(
in RenderFrameView view,
uint tupleLandblockId,
@ -60,6 +66,7 @@ public sealed unsafe partial class WbDrawDispatcher
_packedSubmissions.Clear();
_packedSelectionParts.Clear();
_packedSelectionKeys.Clear();
_packedClassificationCache.BeginFrame(view.Generation);
ReadOnlySpan<RenderFrameEntityCandidate> entities =
view.EntityCandidates;
@ -132,6 +139,7 @@ public sealed unsafe partial class WbDrawDispatcher
in source,
tupleLandblockId);
ClassifyPackedEntity(
in projection,
in candidate,
meshParts.Slice(
source.MeshPartOffset,
@ -164,9 +172,12 @@ public sealed unsafe partial class WbDrawDispatcher
cameraWorldPosition,
_packedAlphaFingerprintScratch));
}
_packedClassificationCache.EndFrame();
}
private void ClassifyPackedEntity(
in RenderProjectionRecord projection,
in RenderInstanceCandidate entity,
ReadOnlySpan<RenderFrameMeshPart> meshParts,
ref uint anyVao)
@ -194,6 +205,45 @@ public sealed unsafe partial class WbDrawDispatcher
lighting.Luminosity,
lighting.Diffuse)
: new Vector2(0f, 1f);
PackedProjectionClassificationEntry? cacheEntry = null;
if (!entity.Animated)
{
PackedClassificationIdentity identity =
PackedClassificationIdentity.From(in projection);
if (_packedClassificationCache.TryGetReusable(
entity.ProjectionId,
in identity,
projection.DirtyMask,
out cacheEntry))
{
if (anyVao == 0 && !meshParts.IsEmpty)
{
ObjectRenderData? firstRenderData =
_meshAdapter.TryGetRenderData(
meshParts[0].MeshRef.GfxObjId);
if (firstRenderData is not null)
anyVao = firstRenderData.VAO;
}
ReplayPackedClassification(
cacheEntry!,
in entity,
slot,
lights,
indoor,
selectionLighting);
return;
}
cacheEntry = _packedClassificationCache.BeginRebuild(
entity.ProjectionId,
in identity);
}
else
{
_packedClassificationCache.RecordAnimatedClassification();
}
PaletteCompositeIdentity paletteIdentity = default;
if (entity.PaletteOverride is not null)
{
@ -201,6 +251,7 @@ public sealed unsafe partial class WbDrawDispatcher
entity.PaletteOverride);
}
bool reusableAcrossFrames = !entity.Animated;
for (int meshIndex = 0;
meshIndex < meshParts.Length;
meshIndex++)
@ -219,7 +270,10 @@ public sealed unsafe partial class WbDrawDispatcher
ObjectRenderData? renderData =
_meshAdapter.TryGetRenderData(meshRef.GfxObjId);
if (renderData is null)
{
reusableAcrossFrames = false;
continue;
}
if (anyVao == 0)
anyVao = renderData.VAO;
@ -235,20 +289,26 @@ public sealed unsafe partial class WbDrawDispatcher
ObjectRenderData? partData =
_meshAdapter.TryGetRenderData(gfxObjId);
if (partData is null)
{
reusableAcrossFrames = false;
continue;
}
float opacity = PackedPartOpacity(
entity.LocalEntityId,
(uint)setupPartIndex);
if (opacity < 1f)
reusableAcrossFrames = false;
if (opacity <= 0f)
continue;
Matrix4x4 model = ComposePartWorldMatrix(
entity.RootWorld,
meshRef.PartTransform,
partTransform);
ClassifyPackedBatches(
Matrix4x4 restPose =
partTransform * meshRef.PartTransform;
Matrix4x4 model =
restPose * entity.RootWorld;
if (!ClassifyPackedBatches(
partData,
restPose,
model,
in entity,
meshRef,
@ -257,12 +317,22 @@ public sealed unsafe partial class WbDrawDispatcher
lights,
indoor,
selectionLighting,
opacity);
opacity,
cacheEntry))
{
reusableAcrossFrames = false;
}
int selectionPartIndex = unchecked(
(partIndex << 16)
| (setupPartIndex & 0xFFFF));
cacheEntry?.SelectionParts.Add(
new PackedClassifiedSelectionPart(
selectionPartIndex,
(uint)gfxObjId,
restPose));
AddPackedSelectionPart(
in entity,
unchecked(
(partIndex << 16)
| (setupPartIndex & 0xFFFF)),
selectionPartIndex,
(uint)gfxObjId,
model);
}
@ -272,13 +342,16 @@ public sealed unsafe partial class WbDrawDispatcher
float opacity = PackedPartOpacity(
entity.LocalEntityId,
0u);
if (opacity < 1f)
reusableAcrossFrames = false;
if (opacity <= 0f)
continue;
Matrix4x4 model =
meshRef.PartTransform * entity.RootWorld;
ClassifyPackedBatches(
Matrix4x4 restPose = meshRef.PartTransform;
Matrix4x4 model = restPose * entity.RootWorld;
if (!ClassifyPackedBatches(
renderData,
restPose,
model,
in entity,
meshRef,
@ -287,7 +360,16 @@ public sealed unsafe partial class WbDrawDispatcher
lights,
indoor,
selectionLighting,
opacity);
opacity,
cacheEntry))
{
reusableAcrossFrames = false;
}
cacheEntry?.SelectionParts.Add(
new PackedClassifiedSelectionPart(
partIndex,
meshRef.GfxObjId,
restPose));
AddPackedSelectionPart(
in entity,
partIndex,
@ -295,6 +377,13 @@ public sealed unsafe partial class WbDrawDispatcher
model);
}
}
if (cacheEntry is not null)
{
_packedClassificationCache.CompleteRebuild(
cacheEntry,
reusableAcrossFrames);
}
}
/// <summary>
@ -324,8 +413,9 @@ public sealed unsafe partial class WbDrawDispatcher
: 1f - translucency;
}
private void ClassifyPackedBatches(
private bool ClassifyPackedBatches(
ObjectRenderData renderData,
Matrix4x4 restPose,
Matrix4x4 model,
in RenderInstanceCandidate entity,
MeshRef meshRef,
@ -334,8 +424,10 @@ public sealed unsafe partial class WbDrawDispatcher
InstanceLightSet lights,
bool indoor,
Vector2 selectionLighting,
float opacity)
float opacity,
PackedProjectionClassificationEntry? cacheEntry)
{
bool reusableAcrossFrames = true;
for (int batchIndex = 0;
batchIndex < renderData.Batches.Count;
batchIndex++)
@ -351,7 +443,9 @@ public sealed unsafe partial class WbDrawDispatcher
meshRef,
batch,
paletteIdentity,
out _);
out bool compositePending);
if (compositePending)
reusableAcrossFrames = false;
if (texture.Handle == 0)
continue;
@ -363,17 +457,78 @@ public sealed unsafe partial class WbDrawDispatcher
texture.Layer,
translucency,
batch.CullMode);
InstanceGroup group = GetOrCreatePackedGroup(key);
group.Matrices.Add(model);
group.LocalSortCenters.Add(renderData.SortCenter);
group.SubmissionOrders.Add(
_nextPackedInstanceSubmissionOrder++);
group.Slots.Add(slot);
group.LightSets.Add(lights);
group.IndoorFlags.Add(indoor ? 1u : 0u);
group.Opacities.Add(opacity);
group.SelectionLighting.Add(selectionLighting);
var classified = new PackedClassifiedBatch(
key,
restPose,
renderData.SortCenter);
cacheEntry?.Batches.Add(classified);
AppendPackedClassification(
in classified,
model,
slot,
lights,
indoor,
opacity,
selectionLighting);
}
return reusableAcrossFrames;
}
private void ReplayPackedClassification(
PackedProjectionClassificationEntry entry,
in RenderInstanceCandidate entity,
uint slot,
InstanceLightSet lights,
bool indoor,
Vector2 selectionLighting)
{
for (int i = 0; i < entry.Batches.Count; i++)
{
PackedClassifiedBatch classified = entry.Batches[i];
AppendPackedClassification(
in classified,
classified.RestPose * entity.RootWorld,
slot,
lights,
indoor,
opacity: 1f,
selectionLighting);
}
for (int i = 0; i < entry.SelectionParts.Count; i++)
{
PackedClassifiedSelectionPart part =
entry.SelectionParts[i];
AddPackedSelectionPart(
in entity,
part.PartIndex,
part.GfxObjId,
part.RestPose * entity.RootWorld);
}
}
private void AppendPackedClassification(
in PackedClassifiedBatch classified,
Matrix4x4 model,
uint slot,
InstanceLightSet lights,
bool indoor,
float opacity,
Vector2 selectionLighting)
{
InstanceGroup group =
GetOrCreatePackedGroup(classified.Key);
group.Matrices.Add(model);
group.LocalSortCenters.Add(
classified.LocalSortCenter);
group.SubmissionOrders.Add(
_nextPackedInstanceSubmissionOrder++);
group.Slots.Add(slot);
group.LightSets.Add(lights);
group.IndoorFlags.Add(indoor ? 1u : 0u);
group.Opacities.Add(opacity);
group.SelectionLighting.Add(selectionLighting);
}
private InstanceGroup GetOrCreatePackedGroup(GroupKey key)