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:
parent
58b712c6ec
commit
29195fb255
11 changed files with 1264 additions and 65 deletions
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue