feat(rendering): prove packed dispatcher output parity

Build a compare-only dispatcher classifier from RenderFrameView and compare complete opaque, alpha, selection, clip, light, texture, transform, and draw payloads against the accepted path. Preserve retail's stable equal-CYpt ordering with explicit draw-local submission ordinals so material-group history cannot affect alpha ties.
This commit is contained in:
Erik 2026-07-25 01:31:56 +02:00
parent 58b712c6ec
commit 29195fb255
11 changed files with 1264 additions and 65 deletions

View file

@ -65,7 +65,7 @@ namespace AcDream.App.Rendering.Wb;
/// <c>glMultiDrawElementsIndirect</c>.
/// </para>
/// </summary>
public sealed unsafe class WbDrawDispatcher : IDisposable
public sealed unsafe partial class WbDrawDispatcher : IDisposable
{
/// <summary>
/// Which subset of entities to walk in a single Draw call.
@ -646,9 +646,11 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
private readonly Dictionary<GroupKey, InstanceGroup> _groups = new();
private readonly List<InstanceGroup> _opaqueDraws = new();
private readonly List<InstanceGroup> _translucentDraws = new();
private readonly List<AlphaFingerprint> _alphaFingerprintScratch = [];
private readonly List<DeferredAlphaInstance> _deferredAlpha = new(128);
private TranslucencyKind[] _deferredAlphaKinds = new TranslucencyKind[128];
private Matrix4x4 _deferredAlphaViewProjection;
private int _nextInstanceSubmissionOrder;
internal long AlphaScratchBudgetBytes => _alphaScratchPolicy.BudgetBytes;
internal long RetainedAlphaScratchBytes => checked(
@ -1504,6 +1506,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
// Draw is invoked several times per frame (landscape slices, late
// dynamics, paperdoll). Per-dispatch payloads reset here, while group
// retirement happens once in BeginFrame from whole-frame liveness.
_nextInstanceSubmissionOrder = 0;
foreach (InstanceGroup group in _groups.Values)
group.ClearPerInstanceData();
@ -2045,7 +2048,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
ObserveCurrentDispatcherSubmission(
visibleInstanceCount: 0,
immediateInstanceCount: 0,
deferTransparent: false);
deferTransparent: false,
camPos);
_cpuStopwatch.Stop();
if (diag) MaybeFlushDiag();
return;
@ -2067,7 +2071,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
ObserveCurrentDispatcherSubmission(
visibleInstanceCount: 0,
immediateInstanceCount: 0,
deferTransparent);
deferTransparent,
camPos);
_cpuStopwatch.Stop();
if (diag) MaybeFlushDiag();
return;
@ -2191,7 +2196,8 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
ObserveCurrentDispatcherSubmission(
totalInstances,
immediateInstances,
deferTransparent);
deferTransparent,
camPos);
// ── Phase 5: upload four buffers ────────────────────────────────────
ActivateNextDynamicBufferSet();
@ -2533,42 +2539,191 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
private void ObserveCurrentDispatcherSubmission(
int visibleInstanceCount,
int immediateInstanceCount,
bool deferTransparent)
bool deferTransparent,
Vector3 cameraWorldPosition)
{
ICurrentRenderDispatcherObserver? observer =
_currentRenderSceneObserver;
if (observer is null)
return;
CurrentRenderDispatcherSubmission submission =
CreateDispatcherSubmission(
visibleInstanceCount,
immediateInstanceCount,
deferTransparent,
_opaqueDraws,
_translucentDraws,
cameraWorldPosition,
_alphaFingerprintScratch);
observer.ObserveDispatcherSubmission(in submission);
}
internal static CurrentRenderDispatcherSubmission
CreateDispatcherSubmission(
int visibleInstanceCount,
int immediateInstanceCount,
bool deferTransparent,
IReadOnlyList<InstanceGroup> opaque,
IReadOnlyList<InstanceGroup> transparent,
Vector3 cameraWorldPosition,
List<AlphaFingerprint> alphaScratch)
{
int opaqueGroupCount =
visibleInstanceCount == 0 ? 0 : _opaqueDraws.Count;
visibleInstanceCount == 0 ? 0 : opaque.Count;
int transparentGroupCount =
visibleInstanceCount == 0 ? 0 : _translucentDraws.Count;
visibleInstanceCount == 0 ? 0 : transparent.Count;
StableRenderHash128 hash = StableRenderHash128.Create();
hash.Add(visibleInstanceCount);
hash.Add(immediateInstanceCount);
hash.Add(opaqueGroupCount);
hash.Add(transparentGroupCount);
hash.Add(deferTransparent);
if (visibleInstanceCount != 0)
{
foreach (InstanceGroup group in _opaqueDraws)
AddSubmissionGroup(ref hash, group);
foreach (InstanceGroup group in _translucentDraws)
AddSubmissionGroup(ref hash, group);
}
RenderSceneHash128 opaqueDigest =
BuildOpaqueSubmissionDigest(opaque);
RenderSceneHash128 transparentDigest =
BuildTransparentSubmissionDigest(
transparent,
cameraWorldPosition,
alphaScratch);
RenderSceneHash128 transparentSetDigest =
BuildOpaqueSubmissionDigest(transparent);
hash.Add(opaqueDigest.Low);
hash.Add(opaqueDigest.High);
hash.Add(transparentDigest.Low);
hash.Add(transparentDigest.High);
hash.Add(transparentSetDigest.Low);
hash.Add(transparentSetDigest.High);
var submission = new CurrentRenderDispatcherSubmission(
return new CurrentRenderDispatcherSubmission(
VisibleInstanceCount: visibleInstanceCount,
ImmediateInstanceCount: immediateInstanceCount,
OpaqueGroupCount: opaqueGroupCount,
TransparentGroupCount: transparentGroupCount,
TransparentDeferred: deferTransparent,
OpaqueDigest: opaqueDigest,
TransparentDigest: transparentDigest,
TransparentSetDigest: transparentSetDigest,
Digest: hash.Finish());
observer.ObserveDispatcherSubmission(in submission);
}
private static void AddSubmissionGroup(
private static RenderSceneHash128 BuildOpaqueSubmissionDigest(
IReadOnlyList<InstanceGroup> groups)
{
// Opaque groups are a mathematical set: depth testing makes submission
// order irrelevant, and equal-distance List.Sort ties can reflect the
// persistent dictionary's historical insertion order. Preserve exact
// group and per-instance contents while combining group fingerprints
// commutatively. Transparent groups remain strictly order-sensitive.
ulong xorLow = 0;
ulong xorHigh = 0;
ulong sumLow = 0;
ulong sumHigh = 0;
for (int index = 0; index < groups.Count; index++)
{
StableRenderHash128 groupHash = StableRenderHash128.Create();
AddOpaqueSubmissionGroup(
ref groupHash,
groups[index]);
RenderSceneHash128 digest = groupHash.Finish();
xorLow ^= digest.Low;
xorHigh ^= digest.High;
sumLow = unchecked(sumLow + digest.Low);
sumHigh = unchecked(sumHigh + digest.High);
}
StableRenderHash128 hash = StableRenderHash128.Create();
hash.Add(groups.Count);
hash.Add(xorLow);
hash.Add(xorHigh);
hash.Add(sumLow);
hash.Add(sumHigh);
return hash.Finish();
}
private static RenderSceneHash128 BuildTransparentSubmissionDigest(
IReadOnlyList<InstanceGroup> groups,
Vector3 cameraWorldPosition,
List<AlphaFingerprint> scratch)
{
scratch.Clear();
for (int groupIndex = 0;
groupIndex < groups.Count;
groupIndex++)
{
InstanceGroup group = groups[groupIndex];
for (int instanceIndex = 0;
instanceIndex < group.Matrices.Count;
instanceIndex++)
{
float distance =
RetailAlphaOrdering.ComputeViewerDistance(
group.LocalSortCenters[instanceIndex],
group.Matrices[instanceIndex],
cameraWorldPosition);
if (!float.IsFinite(distance) || distance <= 0f)
distance = 0f;
scratch.Add(new AlphaFingerprint(
group,
instanceIndex,
distance,
group.SubmissionOrders[instanceIndex]));
}
}
scratch.Sort(AlphaFingerprintComparer.Instance);
StableRenderHash128 hash = StableRenderHash128.Create();
hash.Add(scratch.Count);
for (int index = 0; index < scratch.Count; index++)
{
AlphaFingerprint entry = scratch[index];
GroupKey key = ToKey(entry.Group);
hash.Add(key.FirstIndex);
hash.Add(key.BaseVertex);
hash.Add(key.IndexCount);
hash.Add(key.BindlessTextureHandle);
hash.Add(key.TextureLayer);
hash.Add((int)key.Translucency);
hash.Add((int)key.CullMode);
hash.Add(entry.ViewerDistance);
AddSubmissionInstance(
ref hash,
entry.Group,
entry.InstanceIndex);
}
return hash.Finish();
}
internal readonly record struct AlphaFingerprint(
InstanceGroup Group,
int InstanceIndex,
float ViewerDistance,
int SubmissionOrder);
private sealed class AlphaFingerprintComparer :
IComparer<AlphaFingerprint>
{
public static AlphaFingerprintComparer Instance { get; } =
new();
private AlphaFingerprintComparer()
{
}
public int Compare(
AlphaFingerprint left,
AlphaFingerprint right)
{
int value = right.ViewerDistance.CompareTo(
left.ViewerDistance);
return value != 0
? value
: left.SubmissionOrder.CompareTo(
right.SubmissionOrder);
}
}
private static void AddOpaqueSubmissionGroup(
ref StableRenderHash128 hash,
InstanceGroup group)
{
@ -2581,22 +2736,52 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
hash.Add((int)key.Translucency);
hash.Add((int)key.CullMode);
hash.Add(group.Matrices.Count);
for (int index = 0; index < group.Matrices.Count; index++)
ulong xorLow = 0;
ulong xorHigh = 0;
ulong sumLow = 0;
ulong sumHigh = 0;
for (int index = 0;
index < group.Matrices.Count;
index++)
{
hash.Add(group.Matrices[index]);
hash.Add(group.LocalSortCenters[index]);
hash.Add(group.Slots[index]);
InstanceLightSet lights = group.LightSets[index];
for (int lightIndex = 0;
lightIndex < LightManager.MaxLightsPerObject;
lightIndex++)
{
hash.Add(lights[lightIndex]);
}
hash.Add(group.IndoorFlags[index]);
hash.Add(group.Opacities[index]);
hash.Add(group.SelectionLighting[index]);
StableRenderHash128 instanceHash =
StableRenderHash128.Create();
AddSubmissionInstance(
ref instanceHash,
group,
index);
RenderSceneHash128 digest = instanceHash.Finish();
xorLow ^= digest.Low;
xorHigh ^= digest.High;
sumLow = unchecked(sumLow + digest.Low);
sumHigh = unchecked(sumHigh + digest.High);
}
hash.Add(xorLow);
hash.Add(xorHigh);
hash.Add(sumLow);
hash.Add(sumHigh);
}
private static void AddSubmissionInstance(
ref StableRenderHash128 hash,
InstanceGroup group,
int index)
{
hash.Add(group.Matrices[index]);
hash.Add(group.LocalSortCenters[index]);
hash.Add(group.Slots[index]);
InstanceLightSet lights = group.LightSets[index];
for (int lightIndex = 0;
lightIndex < LightManager.MaxLightsPerObject;
lightIndex++)
{
hash.Add(lights[lightIndex]);
}
hash.Add(group.IndoorFlags[index]);
hash.Add(group.Opacities[index]);
hash.Add(group.SelectionLighting[index]);
}
private void DeferTransparentGroups(Vector3 cameraWorldPosition, Matrix4x4 viewProjection)
@ -2608,9 +2793,15 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
throw new InvalidOperationException(
"One retail alpha scope cannot combine different view-projection matrices.");
// Retail CShadowPart::insertion_sort (0x006B5130) is stable:
// equal-CYpt parts keep the order in which the cell submitted them.
// Material grouping is an acdream batching detail and must not become
// that tiebreak. Reconstruct the original draw-local instance order
// before handing entries to the queue; its stable CYpt radix then
// preserves this sequence for exact-distance ties.
_alphaFingerprintScratch.Clear();
foreach (InstanceGroup group in _translucentDraws)
{
GroupKey key = ToKey(group);
for (int i = 0; i < group.Matrices.Count; i++)
{
Matrix4x4 model = group.Matrices[i];
@ -2619,20 +2810,55 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
localSortCenter,
model,
cameraWorldPosition);
InstanceLightSet lights = group.LightSets[i];
int token = _deferredAlpha.Count;
_deferredAlpha.Add(new DeferredAlphaInstance(
key,
model,
group.Slots[i],
lights,
group.IndoorFlags[i],
group.Opacities[i],
group.SelectionLighting[i]));
queue.Submit(_alphaSource, token, viewerDistance);
if (!float.IsFinite(viewerDistance)
|| viewerDistance <= 0f)
{
viewerDistance = 0f;
}
_alphaFingerprintScratch.Add(new AlphaFingerprint(
group,
i,
viewerDistance,
group.SubmissionOrders[i]));
}
}
_alphaFingerprintScratch.Sort(
AlphaSubmissionOrderComparer.Instance);
foreach (AlphaFingerprint entry in _alphaFingerprintScratch)
{
InstanceGroup group = entry.Group;
int i = entry.InstanceIndex;
int token = _deferredAlpha.Count;
_deferredAlpha.Add(new DeferredAlphaInstance(
ToKey(group),
group.Matrices[i],
group.Slots[i],
group.LightSets[i],
group.IndoorFlags[i],
group.Opacities[i],
group.SelectionLighting[i]));
queue.Submit(
_alphaSource,
token,
entry.ViewerDistance);
}
}
private sealed class AlphaSubmissionOrderComparer :
IComparer<AlphaFingerprint>
{
public static AlphaSubmissionOrderComparer Instance { get; } =
new();
private AlphaSubmissionOrderComparer()
{
}
public int Compare(
AlphaFingerprint left,
AlphaFingerprint right) =>
left.SubmissionOrder.CompareTo(right.SubmissionOrder);
}
private void PrepareDeferredAlphaDraws(ReadOnlySpan<int> tokens)
@ -3415,6 +3641,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
grp.LastUsedFrame = _groupFrame;
grp.Matrices.Add(model);
grp.LocalSortCenters.Add(localSortCenter);
grp.SubmissionOrders.Add(_nextInstanceSubmissionOrder++);
grp.Slots.Add(_currentEntitySlot); // Phase U.4 — parallel to Matrices
AppendCurrentLightSet(grp); // Fix B — 8 ints per instance, parallel to Matrices
// #188: cache-hit entities are always non-animated (the Tier-1 cache
@ -3586,6 +3813,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
InstanceGroup grp = GetOrCreateInstanceGroup(key);
grp.Matrices.Add(model);
grp.LocalSortCenters.Add(renderData.SortCenter);
grp.SubmissionOrders.Add(_nextInstanceSubmissionOrder++);
grp.Slots.Add(_currentEntitySlot); // Phase U.4 — parallel to Matrices
AppendCurrentLightSet(grp); // Fix B — 8 ints per instance, parallel to Matrices
grp.Opacities.Add(opacityMultiplier); // #188 — parallel to Matrices
@ -4071,6 +4299,12 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
// submissions retain the exact per-part key after material grouping.
public readonly List<Vector3> LocalSortCenters = new();
// Retail CShadowPart::insertion_sort (0x006B5130) is stable for equal
// CYpt. Material groups erase authored entity/part/batch traversal
// unless that order is retained explicitly. SubmissionOrders[i] is
// the draw-local append ordinal for Matrices[i].
public readonly List<int> SubmissionOrders = new();
// Phase U.4: per-instance clip-slot index, parallel to Matrices (Slots[i]
// is the binding=2 CellClip slot for the instance whose matrix is
// Matrices[i]). At layout time the dispatcher writes Slots[i] into
@ -4116,6 +4350,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
{
Matrices.Clear();
LocalSortCenters.Clear();
SubmissionOrders.Clear();
Slots.Clear();
LightSets.Clear();
IndoorFlags.Clear();
@ -4128,6 +4363,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
ClearPerInstanceData();
Matrices.TrimExcess();
LocalSortCenters.TrimExcess();
SubmissionOrders.TrimExcess();
Slots.TrimExcess();
LightSets.TrimExcess();
IndoorFlags.TrimExcess();