feat(rendering): pack scene dispatcher inputs

Capture immutable presentation payloads in the render scene, flatten unique entity and mesh-part records into the borrowed frame arena, and compare every routed dispatcher input tuple without changing the production draw source.
This commit is contained in:
Erik 2026-07-25 00:44:58 +02:00
parent 720578592b
commit d8d9897376
13 changed files with 593 additions and 24 deletions

View file

@ -1,5 +1,6 @@
using System.Numerics;
using AcDream.Core.Selection;
using AcDream.Core.World;
namespace AcDream.App.Rendering.Scene;
@ -36,6 +37,17 @@ internal readonly record struct RenderFrameTransformRecord(
int PartIndex,
Matrix4x4 LocalToWorld);
internal readonly record struct RenderFrameEntityCandidate(
RenderProjectionRecord Projection,
int MeshPartOffset,
int MeshPartCount,
bool Animated);
internal readonly record struct RenderFrameMeshPart(
RenderProjectionId ProjectionId,
int PartIndex,
MeshRef MeshRef);
internal readonly record struct RenderFrameClassificationRecord(
RenderProjectionId ProjectionId,
int CandidateIndex,
@ -77,7 +89,9 @@ internal readonly record struct RenderFrameDiagnosticCounts(
int AlphaClassificationCount,
int LightSetCount,
int SelectionPartCount,
int RouteCandidateCount);
int RouteCandidateCount,
int EntityCandidateCount,
int MeshPartCount);
/// <summary>
/// A validated borrowed view over one reusable frame arena. Copies are safe:
@ -133,6 +147,12 @@ internal readonly struct RenderFrameView
public ReadOnlySpan<RenderFrameTransformRecord> Transforms =>
Arena.Transforms;
public ReadOnlySpan<RenderFrameEntityCandidate> EntityCandidates =>
Arena.EntityCandidates;
public ReadOnlySpan<RenderFrameMeshPart> MeshParts =>
Arena.MeshParts;
public ReadOnlySpan<RenderFrameClassificationRecord> Classifications =>
Arena.Classifications;
@ -192,6 +212,8 @@ internal sealed class RenderFrameArena
private RenderFrameCellRange[] _cellRanges = [];
private RenderProjectionRecord[] _dynamics = [];
private RenderFrameTransformRecord[] _transforms = [];
private RenderFrameEntityCandidate[] _entityCandidates = [];
private RenderFrameMeshPart[] _meshParts = [];
private RenderFrameClassificationRecord[] _classifications = [];
private RenderFrameObjectLightSet[] _lightSets = [];
private RenderFrameSelectionPart[] _selectionParts = [];
@ -202,6 +224,8 @@ internal sealed class RenderFrameArena
private int _cellRangeCount;
private int _dynamicCount;
private int _transformCount;
private int _entityCandidateCount;
private int _meshPartCount;
private int _classificationCount;
private int _lightSetCount;
private int _selectionPartCount;
@ -234,7 +258,9 @@ internal sealed class RenderFrameArena
_alphaClassificationCount,
_lightSetCount,
_selectionPartCount,
_routeCandidateCount);
_routeCandidateCount,
_entityCandidateCount,
_meshPartCount);
internal ReadOnlySpan<RenderProjectionRecord> OutdoorStaticCandidates =>
_outdoor.AsSpan(0, _outdoorCount);
@ -251,6 +277,12 @@ internal sealed class RenderFrameArena
internal ReadOnlySpan<RenderFrameTransformRecord> Transforms =>
_transforms.AsSpan(0, _transformCount);
internal ReadOnlySpan<RenderFrameEntityCandidate> EntityCandidates =>
_entityCandidates.AsSpan(0, _entityCandidateCount);
internal ReadOnlySpan<RenderFrameMeshPart> MeshParts =>
_meshParts.AsSpan(0, _meshPartCount);
internal ReadOnlySpan<RenderFrameClassificationRecord> Classifications =>
_classifications.AsSpan(0, _classificationCount);
@ -284,6 +316,8 @@ internal sealed class RenderFrameArena
_cellRangeCount = 0;
_dynamicCount = 0;
_transformCount = 0;
_entityCandidateCount = 0;
_meshPartCount = 0;
_classificationCount = 0;
_lightSetCount = 0;
_selectionPartCount = 0;
@ -397,6 +431,46 @@ internal sealed class RenderFrameArena
_transforms[_transformCount++] = transform;
}
internal void AddEntityCandidate(
ulong epoch,
in RenderProjectionRecord projection,
bool animated)
{
EnsureBuilding(epoch);
IReadOnlyList<MeshRef>? meshRefs =
projection.EntityPayload.MeshRefs;
int meshPartCount = meshRefs?.Count ?? 0;
if (meshPartCount != projection.MeshSet.MeshCount)
{
throw new InvalidOperationException(
$"Projection {projection.Id} declares "
+ $"{projection.MeshSet.MeshCount} mesh parts but its "
+ $"presentation payload contains {meshPartCount}.");
}
int meshPartOffset = _meshPartCount;
EnsureCapacity(
ref _meshParts,
checked(meshPartOffset + meshPartCount));
for (int partIndex = 0; partIndex < meshPartCount; partIndex++)
{
_meshParts[_meshPartCount++] = new RenderFrameMeshPart(
projection.Id,
partIndex,
meshRefs![partIndex]);
}
EnsureCapacity(
ref _entityCandidates,
_entityCandidateCount + 1);
_entityCandidates[_entityCandidateCount++] =
new RenderFrameEntityCandidate(
projection,
meshPartOffset,
meshPartCount,
animated);
}
internal void AddClassification(
ulong epoch,
in RenderFrameClassificationRecord classification)
@ -529,6 +603,10 @@ internal sealed class RenderFrameArena
{
if (_selectionPartCount > 0)
Array.Clear(_selectionParts, 0, _selectionPartCount);
if (_entityCandidateCount > 0)
Array.Clear(_entityCandidates, 0, _entityCandidateCount);
if (_meshPartCount > 0)
Array.Clear(_meshParts, 0, _meshPartCount);
}
private static void EnsureCapacity<T>(ref T[] values, int required)
@ -603,6 +681,11 @@ internal readonly struct RenderFrameWriter
public void AddTransform(in RenderFrameTransformRecord transform) =>
Arena.AddTransform(_epoch, in transform);
public void AddEntityCandidate(
in RenderProjectionRecord projection,
bool animated) =>
Arena.AddEntityCandidate(_epoch, in projection, animated);
public void AddClassification(
in RenderFrameClassificationRecord classification) =>
Arena.AddClassification(_epoch, in classification);