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,4 +1,5 @@
using System.Numerics;
using AcDream.App.Rendering.Wb;
namespace AcDream.App.Rendering.Scene;
@ -11,6 +12,7 @@ internal readonly record struct RenderScenePViewBuildInput(
IReadOnlyList<PortalVisibilityFrame> LookInFrames,
HashSet<uint> DrawableCells,
IRetailPViewCellSource Cells,
HashSet<uint>? AnimatedEntityIds,
bool RootIsOutdoor);
internal readonly record struct RenderFrameProductComparisonSnapshot(
@ -25,6 +27,14 @@ internal readonly record struct RenderFrameProductComparisonSnapshot(
string? FirstMismatch,
RenderSceneHash128 ExpectedDigest,
RenderSceneHash128 ActualDigest,
ulong PackedInputComparisonCount,
ulong PackedInputSuccessfulComparisonCount,
int PackedInputExpectedCount,
int PackedInputActualCount,
long PackedInputMismatchCount,
string? PackedInputFirstMismatch,
RenderSceneHash128 PackedInputExpectedDigest,
RenderSceneHash128 PackedInputActualDigest,
RenderFrameDiagnosticCounts ProductCounts)
{
public static RenderFrameProductComparisonSnapshot Disabled { get; } =
@ -40,6 +50,14 @@ internal readonly record struct RenderFrameProductComparisonSnapshot(
FirstMismatch: null,
ExpectedDigest: RenderSceneHash128.Empty,
ActualDigest: RenderSceneHash128.Empty,
PackedInputComparisonCount: 0,
PackedInputSuccessfulComparisonCount: 0,
PackedInputExpectedCount: 0,
PackedInputActualCount: 0,
PackedInputMismatchCount: 0,
PackedInputFirstMismatch: null,
PackedInputExpectedDigest: RenderSceneHash128.Empty,
PackedInputActualDigest: RenderSceneHash128.Empty,
ProductCounts: default);
}
@ -63,12 +81,18 @@ internal sealed class RenderScenePViewFrameProductController :
private readonly RenderFrameExchange _exchange = new();
private readonly List<CandidateKey> _expected = [];
private readonly List<CandidateKey> _actual = [];
private readonly List<PackedInputKey> _packedExpected = [];
private readonly List<PackedInputKey> _packedActual = [];
private readonly Action<string>? _log;
private ulong _productFrameSequence;
private ulong _comparisonCount;
private ulong _successfulComparisonCount;
private long _mismatchCount;
private string? _firstMismatch;
private ulong _packedInputComparisonCount;
private ulong _packedInputSuccessfulComparisonCount;
private long _packedInputMismatchCount;
private string? _packedInputFirstMismatch;
public RenderScenePViewFrameProductController(
RenderSceneShadowRuntime shadow,
@ -93,6 +117,8 @@ internal sealed class RenderScenePViewFrameProductController :
IReadOnlyList<PortalVisibilityFrame> lookInFrames,
HashSet<uint> drawableCells,
IRetailPViewCellSource cells,
HashSet<uint>? animatedEntityIds,
uint tupleLandblockId,
bool rootIsOutdoor)
{
ArgumentNullException.ThrowIfNull(portalFrame);
@ -113,6 +139,7 @@ internal sealed class RenderScenePViewFrameProductController :
lookInFrames,
drawableCells,
cells,
animatedEntityIds,
rootIsOutdoor);
_builder.Build(_exchange, frameSequence, in input);
RenderFrameView view = _exchange.BorrowLatest(
@ -120,7 +147,7 @@ internal sealed class RenderScenePViewFrameProductController :
frameSequence);
try
{
Compare(in view);
Compare(in view, tupleLandblockId);
}
finally
{
@ -128,7 +155,7 @@ internal sealed class RenderScenePViewFrameProductController :
}
}
private void Compare(in RenderFrameView view)
private void Compare(in RenderFrameView view, uint tupleLandblockId)
{
_comparisonCount = checked(_comparisonCount + 1);
_expected.Clear();
@ -196,6 +223,9 @@ internal sealed class RenderScenePViewFrameProductController :
}
}
PackedInputComparison packed =
ComparePackedInput(in view, tupleLandblockId);
Snapshot = new RenderFrameProductComparisonSnapshot(
Enabled: true,
ProductFrameSequence: view.FrameSequence,
@ -209,9 +239,184 @@ internal sealed class RenderScenePViewFrameProductController :
FirstMismatch: _firstMismatch,
ExpectedDigest: expectedDigest,
ActualDigest: actualDigest,
PackedInputComparisonCount: _packedInputComparisonCount,
PackedInputSuccessfulComparisonCount:
_packedInputSuccessfulComparisonCount,
PackedInputExpectedCount: _packedExpected.Count,
PackedInputActualCount: _packedActual.Count,
PackedInputMismatchCount: _packedInputMismatchCount,
PackedInputFirstMismatch: _packedInputFirstMismatch,
PackedInputExpectedDigest: packed.ExpectedDigest,
PackedInputActualDigest: packed.ActualDigest,
ProductCounts: view.DiagnosticCounts);
}
private PackedInputComparison ComparePackedInput(
in RenderFrameView view,
uint tupleLandblockId)
{
_packedInputComparisonCount =
checked(_packedInputComparisonCount + 1);
_packedExpected.Clear();
_packedActual.Clear();
IReadOnlyList<CurrentRenderDispatcherFingerprint> expected =
_current.DispatcherCandidates;
if (_packedExpected.Capacity < expected.Count)
_packedExpected.Capacity = expected.Count;
for (int index = 0; index < expected.Count; index++)
{
CurrentRenderDispatcherFingerprint candidate = expected[index];
CurrentRenderProjectionFingerprint projection =
candidate.Projection;
_packedExpected.Add(new PackedInputKey(
candidate.DrawSequence,
candidate.Set,
ExpectedId(in projection),
candidate.MeshRefIndex,
candidate.TupleLandblockId,
candidate.CacheLandblockId,
projection.EntityId,
projection.ServerGuid,
projection.SourceId,
projection.ParentCellId,
projection.EffectCellId,
projection.Flags,
projection.Transform,
projection.Geometry,
projection.Appearance,
candidate.GfxObjId,
candidate.PartTransform,
candidate.SurfaceOverrides));
}
ReadOnlySpan<RenderFrameCandidateRange> ranges = view.RouteRanges;
ReadOnlySpan<RenderProjectionRecord> routeCandidates =
view.RouteCandidates;
if (_packedActual.Capacity < view.MeshParts.Length)
_packedActual.Capacity = view.MeshParts.Length;
for (int drawSequence = 0;
drawSequence < ranges.Length;
drawSequence++)
{
RenderFrameCandidateRange range = ranges[drawSequence];
int end = checked(range.Offset + range.Count);
for (int candidateIndex = range.Offset;
candidateIndex < end;
candidateIndex++)
{
RenderProjectionRecord projection =
routeCandidates[candidateIndex];
if ((projection.Flags & RenderProjectionFlags.Draw) == 0)
continue;
IReadOnlyList<AcDream.Core.World.MeshRef>? meshRefs =
projection.EntityPayload.MeshRefs;
int meshCount = meshRefs?.Count ?? 0;
for (int partIndex = 0;
partIndex < meshCount;
partIndex++)
{
AcDream.Core.World.MeshRef meshRef =
meshRefs![partIndex];
_packedActual.Add(new PackedInputKey(
drawSequence,
WbDrawDispatcher.EntitySet.All,
projection.Id,
partIndex,
tupleLandblockId,
DispatcherCacheLandblock(
in projection,
tupleLandblockId),
projection.Source.LocalEntityId,
projection.Source.ServerGuid,
projection.Source.SourceId,
projection.Source.ParentCellId,
projection.Source.EffectCellId,
projection.Source.CurrentProjectionFlags,
projection.Source.TransformFingerprint,
projection.Source.GeometryFingerprint,
projection.Source.AppearanceFingerprint,
meshRef.GfxObjId,
meshRef.PartTransform,
CurrentRenderSceneOracle
.CreateSurfaceOverrideFingerprint(
meshRef.SurfaceOverrides)));
}
}
}
_packedExpected.Sort(PackedInputKeyComparer.Instance);
_packedActual.Sort(PackedInputKeyComparer.Instance);
RenderSceneHash128 expectedDigest =
BuildPackedInputDigest(_packedExpected);
RenderSceneHash128 actualDigest =
BuildPackedInputDigest(_packedActual);
string? mismatch = ComparePackedInputKeys(ranges.Length);
if (mismatch is null)
{
_packedInputSuccessfulComparisonCount =
checked(_packedInputSuccessfulComparisonCount + 1);
}
else
{
_packedInputMismatchCount =
checked(_packedInputMismatchCount + 1);
if (_packedInputFirstMismatch is null)
{
_packedInputFirstMismatch = mismatch;
_log?.Invoke(
"[render-packed-input] first mismatch: " + mismatch);
}
}
return new PackedInputComparison(expectedDigest, actualDigest);
}
private static uint DispatcherCacheLandblock(
in RenderProjectionRecord projection,
uint tupleLandblockId) =>
projection.Source.ParentCellId != 0
? (projection.Source.ParentCellId & 0xFFFF0000u) | 0xFFFFu
: tupleLandblockId;
private string? ComparePackedInputKeys(int actualDrawCount)
{
if (_current.Snapshot.DispatcherDrawCount != actualDrawCount)
{
return $"field=drawCount expected="
+ $"{_current.Snapshot.DispatcherDrawCount} "
+ $"actual={actualDrawCount}";
}
if (_packedExpected.Count != _packedActual.Count)
{
return $"field=count expected={_packedExpected.Count} "
+ $"actual={_packedActual.Count}";
}
for (int index = 0; index < _packedExpected.Count; index++)
{
if (_packedExpected[index] != _packedActual[index])
{
return $"field=meshPart index={index} "
+ $"expected={_packedExpected[index]} "
+ $"actual={_packedActual[index]}";
}
}
return null;
}
private static RenderSceneHash128 BuildPackedInputDigest(
List<PackedInputKey> candidates)
{
StableRenderHash128 hash = StableRenderHash128.Create();
hash.Add(candidates.Count);
for (int index = 0; index < candidates.Count; index++)
candidates[index].AddTo(ref hash);
return hash.Finish();
}
private string? CompareKeys()
{
if (_expected.Count != _actual.Count)
@ -283,6 +488,57 @@ internal sealed class RenderScenePViewFrameProductController :
uint CellId,
RenderProjectionId ProjectionId);
private readonly record struct PackedInputComparison(
RenderSceneHash128 ExpectedDigest,
RenderSceneHash128 ActualDigest);
private readonly record struct PackedInputKey(
int DrawSequence,
WbDrawDispatcher.EntitySet Set,
RenderProjectionId ProjectionId,
int MeshRefIndex,
uint TupleLandblockId,
uint CacheLandblockId,
uint LocalEntityId,
uint ServerGuid,
uint SourceId,
uint ParentCellId,
uint EffectCellId,
uint SourceFlags,
RenderSceneHash128 Transform,
RenderSceneHash128 Geometry,
RenderSceneHash128 Appearance,
uint GfxObjId,
Matrix4x4 PartTransform,
RenderSceneHash128 SurfaceOverrides)
{
public void AddTo(ref StableRenderHash128 hash)
{
hash.Add(DrawSequence);
hash.Add((int)Set);
ProjectionId.AddTo(ref hash);
hash.Add(MeshRefIndex);
hash.Add(TupleLandblockId);
hash.Add(CacheLandblockId);
hash.Add(LocalEntityId);
hash.Add(ServerGuid);
hash.Add(SourceId);
hash.Add(ParentCellId);
hash.Add(EffectCellId);
hash.Add(SourceFlags);
hash.Add(Transform.Low);
hash.Add(Transform.High);
hash.Add(Geometry.Low);
hash.Add(Geometry.High);
hash.Add(Appearance.Low);
hash.Add(Appearance.High);
hash.Add(GfxObjId);
hash.Add(PartTransform);
hash.Add(SurfaceOverrides.Low);
hash.Add(SurfaceOverrides.High);
}
}
private sealed class CandidateKeyComparer : IComparer<CandidateKey>
{
public static CandidateKeyComparer Instance { get; } = new();
@ -302,6 +558,25 @@ internal sealed class RenderScenePViewFrameProductController :
return left.ProjectionId.CompareTo(right.ProjectionId);
}
}
private sealed class PackedInputKeyComparer :
IComparer<PackedInputKey>
{
public static PackedInputKeyComparer Instance { get; } = new();
private PackedInputKeyComparer()
{
}
public int Compare(PackedInputKey left, PackedInputKey right)
{
int value = left.DrawSequence.CompareTo(right.DrawSequence);
if (value != 0) return value;
value = left.ProjectionId.CompareTo(right.ProjectionId);
if (value != 0) return value;
return left.MeshRefIndex.CompareTo(right.MeshRefIndex);
}
}
}
/// <summary>
@ -313,7 +588,7 @@ internal sealed class RenderScenePViewFrameBuilder
{
private const byte EnvCellProjectionDomain = 2;
private readonly HashSet<RenderProjectionId> _transformIds = [];
private readonly HashSet<RenderProjectionId> _projectionIds = [];
private RenderProjectionRecord[] _outdoor = [];
private RenderProjectionRecord[] _dynamics = [];
private RenderProjectionRecord[] _cell = [];
@ -338,7 +613,7 @@ internal sealed class RenderScenePViewFrameBuilder
input.ClipAssembly);
RenderSceneDigest digest = input.SourceDigest;
writer.SetSourceDigest(in digest);
_transformIds.Clear();
_projectionIds.Clear();
LoadSceneIndices(input.Scene);
BuildOutdoorRoutes(writer, in input);
@ -398,7 +673,10 @@ internal sealed class RenderScenePViewFrameBuilder
_survivors[count++] = record;
writer.AddOutdoor(in record);
AddTransform(writer, in record);
AddProjection(
writer,
in record,
input.AnimatedEntityIds);
}
writer.AddRouteRange(
@ -429,7 +707,10 @@ internal sealed class RenderScenePViewFrameBuilder
continue;
for (int index = 0; index < count; index++)
AddTransform(writer, in _cell[index]);
AddProjection(
writer,
in _cell[index],
input.AnimatedEntityIds);
writer.AddRouteRange(
RenderFrameCandidateRoute.LookInObject,
i,
@ -471,7 +752,10 @@ internal sealed class RenderScenePViewFrameBuilder
_survivors[count++] = record;
writer.AddDynamic(in record);
AddTransform(writer, in record);
AddProjection(
writer,
in record,
input.AnimatedEntityIds);
}
writer.AddRouteRange(
@ -512,7 +796,10 @@ internal sealed class RenderScenePViewFrameBuilder
}
_cell[count++] = record;
AddTransform(writer, in record);
AddProjection(
writer,
in record,
input.AnimatedEntityIds);
}
writer.AddCellRange(
@ -563,7 +850,10 @@ internal sealed class RenderScenePViewFrameBuilder
_survivors[count++] = record;
writer.AddDynamic(in record);
AddTransform(writer, in record);
AddProjection(
writer,
in record,
input.AnimatedEntityIds);
}
writer.AddRouteRange(
@ -598,16 +888,20 @@ internal sealed class RenderScenePViewFrameBuilder
return CompactAndSort(_cell, count);
}
private void AddTransform(
private void AddProjection(
RenderFrameWriter writer,
in RenderProjectionRecord record)
in RenderProjectionRecord record,
HashSet<uint>? animatedEntityIds)
{
if (!_transformIds.Add(record.Id))
if (!_projectionIds.Add(record.Id))
return;
writer.AddTransform(new RenderFrameTransformRecord(
record.Id,
-1,
record.Transform.LocalToWorld));
writer.AddEntityCandidate(
in record,
animatedEntityIds?.Contains(record.Source.LocalEntityId) == true);
}
private static int CompactAndSort(