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

@ -61,6 +61,9 @@ internal readonly record struct CurrentRenderDispatcherSubmission(
int OpaqueGroupCount,
int TransparentGroupCount,
bool TransparentDeferred,
RenderSceneHash128 OpaqueDigest,
RenderSceneHash128 TransparentDigest,
RenderSceneHash128 TransparentSetDigest,
RenderSceneHash128 Digest);
internal readonly record struct CurrentRenderSelectionFingerprint(
@ -209,6 +212,8 @@ internal sealed class CurrentRenderSceneOracle :
_pviewCandidates = [];
private readonly List<CurrentRenderDispatcherFingerprint>
_dispatcherCandidates = [];
private readonly List<CurrentRenderDispatcherSubmission>
_dispatcherSubmissions = [];
private readonly Dictionary<WorldEntity, CurrentRenderProjectionFingerprint>
_dispatcherProjectionByEntity =
new(ReferenceEqualityComparer.Instance);
@ -249,6 +254,8 @@ internal sealed class CurrentRenderSceneOracle :
PViewCandidates => _pviewCandidates;
internal IReadOnlyList<CurrentRenderDispatcherFingerprint>
DispatcherCandidates => _dispatcherCandidates;
internal IReadOnlyList<CurrentRenderDispatcherSubmission>
DispatcherSubmissions => _dispatcherSubmissions;
internal IReadOnlyList<CurrentRenderSelectionFingerprint>
SelectionParts => _selectionParts;
@ -470,6 +477,7 @@ internal sealed class CurrentRenderSceneOracle :
{
_dispatcherFrameOpen = true;
_dispatcherCandidates.Clear();
_dispatcherSubmissions.Clear();
_dispatcherProjectionByEntity.Clear();
_dispatcherDrawCount = 0;
_dispatcherEntityCount = 0;
@ -589,6 +597,7 @@ internal sealed class CurrentRenderSceneOracle :
_dispatcherInstanceCount = checked(
_dispatcherInstanceCount + submission.VisibleInstanceCount);
_dispatcherSubmissions.Add(submission);
_dispatcherOpaqueGroupCount = checked(
_dispatcherOpaqueGroupCount + submission.OpaqueGroupCount);
_dispatcherTransparentGroupCount = checked(
@ -621,6 +630,7 @@ internal sealed class CurrentRenderSceneOracle :
_dispatcherFrameOpen = false;
_dispatcherCandidates.Clear();
_dispatcherSubmissions.Clear();
_dispatcherProjectionByEntity.Clear();
_abortedDispatcherFrames = checked(_abortedDispatcherFrames + 1);
Snapshot = Snapshot with
@ -862,7 +872,7 @@ internal sealed class CurrentRenderSceneOracle :
return geometry.Finish();
}
private static RenderSceneHash128 FingerprintSelectionGeometry(
internal static RenderSceneHash128 FingerprintSelectionGeometry(
RetailSelectionMesh mesh)
{
StableRenderHash128 geometry = StableRenderHash128.Create();

View file

@ -69,6 +69,31 @@ internal readonly record struct RenderInstanceCandidate(
MeshPartCount: meshPartCount,
TupleLandblockId: tupleLandblockId);
}
internal static RenderInstanceCandidate FromFrame(
in RenderFrameEntityCandidate source,
uint tupleLandblockId)
{
RenderProjectionRecord projection = source.Projection;
return new RenderInstanceCandidate(
ProjectionId: projection.Id,
LocalEntityId: projection.Source.LocalEntityId,
ServerGuid: projection.Source.ServerGuid,
SourceId: projection.Source.SourceId,
ParentCellId: projection.Source.ParentCellId,
RootWorld: projection.Transform.LocalToWorld,
Position: projection.Transform.Position,
Rotation: projection.Transform.Rotation,
Scale: projection.Transform.UniformScale,
Bounds: projection.Bounds,
PaletteOverride:
projection.EntityPayload.PaletteOverride,
IsBuildingShell:
projection.EntityPayload.IsBuildingShell,
Animated: source.Animated,
MeshPartCount: source.MeshPartCount,
TupleLandblockId: tupleLandblockId);
}
}
/// <summary>

View file

@ -35,6 +35,22 @@ internal readonly record struct RenderFrameProductComparisonSnapshot(
string? PackedInputFirstMismatch,
RenderSceneHash128 PackedInputExpectedDigest,
RenderSceneHash128 PackedInputActualDigest,
ulong ClassifiedOutputComparisonCount,
ulong ClassifiedOutputSuccessfulComparisonCount,
int ClassifiedOutputExpectedDrawCount,
int ClassifiedOutputActualDrawCount,
long ClassifiedOutputMismatchCount,
string? ClassifiedOutputFirstMismatch,
RenderSceneHash128 ClassifiedOutputExpectedDigest,
RenderSceneHash128 ClassifiedOutputActualDigest,
ulong SelectionOutputComparisonCount,
ulong SelectionOutputSuccessfulComparisonCount,
int SelectionOutputExpectedPartCount,
int SelectionOutputActualPartCount,
long SelectionOutputMismatchCount,
string? SelectionOutputFirstMismatch,
RenderSceneHash128 SelectionOutputExpectedDigest,
RenderSceneHash128 SelectionOutputActualDigest,
RenderFrameDiagnosticCounts ProductCounts)
{
public static RenderFrameProductComparisonSnapshot Disabled { get; } =
@ -58,6 +74,22 @@ internal readonly record struct RenderFrameProductComparisonSnapshot(
PackedInputFirstMismatch: null,
PackedInputExpectedDigest: RenderSceneHash128.Empty,
PackedInputActualDigest: RenderSceneHash128.Empty,
ClassifiedOutputComparisonCount: 0,
ClassifiedOutputSuccessfulComparisonCount: 0,
ClassifiedOutputExpectedDrawCount: 0,
ClassifiedOutputActualDrawCount: 0,
ClassifiedOutputMismatchCount: 0,
ClassifiedOutputFirstMismatch: null,
ClassifiedOutputExpectedDigest: RenderSceneHash128.Empty,
ClassifiedOutputActualDigest: RenderSceneHash128.Empty,
SelectionOutputComparisonCount: 0,
SelectionOutputSuccessfulComparisonCount: 0,
SelectionOutputExpectedPartCount: 0,
SelectionOutputActualPartCount: 0,
SelectionOutputMismatchCount: 0,
SelectionOutputFirstMismatch: null,
SelectionOutputExpectedDigest: RenderSceneHash128.Empty,
SelectionOutputActualDigest: RenderSceneHash128.Empty,
ProductCounts: default);
}
@ -77,12 +109,15 @@ internal sealed class RenderScenePViewFrameProductController :
{
private readonly RenderSceneShadowRuntime _shadow;
private readonly CurrentRenderSceneOracle _current;
private readonly WbDrawDispatcher? _dispatcher;
private readonly RenderScenePViewFrameBuilder _builder = new();
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 List<SelectionKey> _selectionExpected = [];
private readonly List<SelectionKey> _selectionActual = [];
private readonly Action<string>? _log;
private ulong _productFrameSequence;
private ulong _comparisonCount;
@ -93,15 +128,25 @@ internal sealed class RenderScenePViewFrameProductController :
private ulong _packedInputSuccessfulComparisonCount;
private long _packedInputMismatchCount;
private string? _packedInputFirstMismatch;
private ulong _classifiedOutputComparisonCount;
private ulong _classifiedOutputSuccessfulComparisonCount;
private long _classifiedOutputMismatchCount;
private string? _classifiedOutputFirstMismatch;
private ulong _selectionOutputComparisonCount;
private ulong _selectionOutputSuccessfulComparisonCount;
private long _selectionOutputMismatchCount;
private string? _selectionOutputFirstMismatch;
public RenderScenePViewFrameProductController(
RenderSceneShadowRuntime shadow,
CurrentRenderSceneOracle current,
Action<string>? log = null)
Action<string>? log = null,
WbDrawDispatcher? dispatcher = null)
{
_shadow = shadow ?? throw new ArgumentNullException(nameof(shadow));
_current = current ?? throw new ArgumentNullException(nameof(current));
_log = log;
_dispatcher = dispatcher;
Snapshot = RenderFrameProductComparisonSnapshot.Disabled with
{
Enabled = true,
@ -119,7 +164,8 @@ internal sealed class RenderScenePViewFrameProductController :
IRetailPViewCellSource cells,
HashSet<uint>? animatedEntityIds,
uint tupleLandblockId,
bool rootIsOutdoor)
bool rootIsOutdoor,
Vector3 cameraWorldPosition = default)
{
ArgumentNullException.ThrowIfNull(portalFrame);
ArgumentNullException.ThrowIfNull(clipAssembly);
@ -147,7 +193,10 @@ internal sealed class RenderScenePViewFrameProductController :
frameSequence);
try
{
Compare(in view, tupleLandblockId);
Compare(
in view,
tupleLandblockId,
cameraWorldPosition);
}
finally
{
@ -155,7 +204,10 @@ internal sealed class RenderScenePViewFrameProductController :
}
}
private void Compare(in RenderFrameView view, uint tupleLandblockId)
private void Compare(
in RenderFrameView view,
uint tupleLandblockId,
Vector3 cameraWorldPosition)
{
_comparisonCount = checked(_comparisonCount + 1);
_expected.Clear();
@ -225,6 +277,11 @@ internal sealed class RenderScenePViewFrameProductController :
PackedInputComparison packed =
ComparePackedInput(in view, tupleLandblockId);
ClassifiedOutputComparison classified =
CompareClassifiedOutput(
in view,
tupleLandblockId,
cameraWorldPosition);
Snapshot = new RenderFrameProductComparisonSnapshot(
Enabled: true,
@ -248,9 +305,247 @@ internal sealed class RenderScenePViewFrameProductController :
PackedInputFirstMismatch: _packedInputFirstMismatch,
PackedInputExpectedDigest: packed.ExpectedDigest,
PackedInputActualDigest: packed.ActualDigest,
ClassifiedOutputComparisonCount:
_classifiedOutputComparisonCount,
ClassifiedOutputSuccessfulComparisonCount:
_classifiedOutputSuccessfulComparisonCount,
ClassifiedOutputExpectedDrawCount:
classified.ExpectedDrawCount,
ClassifiedOutputActualDrawCount:
classified.ActualDrawCount,
ClassifiedOutputMismatchCount:
_classifiedOutputMismatchCount,
ClassifiedOutputFirstMismatch:
_classifiedOutputFirstMismatch,
ClassifiedOutputExpectedDigest:
classified.ExpectedDigest,
ClassifiedOutputActualDigest:
classified.ActualDigest,
SelectionOutputComparisonCount:
_selectionOutputComparisonCount,
SelectionOutputSuccessfulComparisonCount:
_selectionOutputSuccessfulComparisonCount,
SelectionOutputExpectedPartCount:
classified.ExpectedSelectionCount,
SelectionOutputActualPartCount:
classified.ActualSelectionCount,
SelectionOutputMismatchCount:
_selectionOutputMismatchCount,
SelectionOutputFirstMismatch:
_selectionOutputFirstMismatch,
SelectionOutputExpectedDigest:
classified.ExpectedSelectionDigest,
SelectionOutputActualDigest:
classified.ActualSelectionDigest,
ProductCounts: view.DiagnosticCounts);
}
private ClassifiedOutputComparison CompareClassifiedOutput(
in RenderFrameView view,
uint tupleLandblockId,
Vector3 cameraWorldPosition)
{
WbDrawDispatcher? dispatcher = _dispatcher;
if (dispatcher is null)
return default;
dispatcher.BuildPackedDispatcherOracle(
in view,
tupleLandblockId,
cameraWorldPosition);
IReadOnlyList<CurrentRenderDispatcherSubmission> expected =
_current.DispatcherSubmissions;
IReadOnlyList<CurrentRenderDispatcherSubmission> actual =
dispatcher.PackedDispatcherSubmissions;
RenderSceneHash128 expectedDigest =
BuildSubmissionDigest(expected);
RenderSceneHash128 actualDigest =
BuildSubmissionDigest(actual);
_classifiedOutputComparisonCount =
checked(_classifiedOutputComparisonCount + 1);
string? mismatch = CompareSubmissions(expected, actual);
if (mismatch is null)
{
_classifiedOutputSuccessfulComparisonCount =
checked(
_classifiedOutputSuccessfulComparisonCount + 1);
}
else
{
_classifiedOutputMismatchCount =
checked(_classifiedOutputMismatchCount + 1);
if (_classifiedOutputFirstMismatch is null)
{
_classifiedOutputFirstMismatch = mismatch;
_log?.Invoke(
"[render-classified-output] first mismatch: "
+ mismatch);
}
}
IReadOnlyList<CurrentRenderSelectionFingerprint>
expectedSelection = _current.SelectionParts;
IReadOnlyList<CurrentRenderSelectionFingerprint>
actualSelection = dispatcher.PackedSelectionParts;
BuildSelectionKeys(
expectedSelection,
_selectionExpected);
BuildSelectionKeys(
actualSelection,
_selectionActual);
RenderSceneHash128 expectedSelectionDigest =
BuildSelectionDigest(_selectionExpected);
RenderSceneHash128 actualSelectionDigest =
BuildSelectionDigest(_selectionActual);
_selectionOutputComparisonCount =
checked(_selectionOutputComparisonCount + 1);
string? selectionMismatch = CompareSelection(
_selectionExpected,
_selectionActual);
if (selectionMismatch is null)
{
_selectionOutputSuccessfulComparisonCount =
checked(
_selectionOutputSuccessfulComparisonCount + 1);
}
else
{
_selectionOutputMismatchCount =
checked(_selectionOutputMismatchCount + 1);
if (_selectionOutputFirstMismatch is null)
{
_selectionOutputFirstMismatch = selectionMismatch;
_log?.Invoke(
"[render-selection-output] first mismatch: "
+ selectionMismatch);
}
}
return new ClassifiedOutputComparison(
expected.Count,
actual.Count,
expectedDigest,
actualDigest,
expectedSelection.Count,
actualSelection.Count,
expectedSelectionDigest,
actualSelectionDigest);
}
private static string? CompareSubmissions(
IReadOnlyList<CurrentRenderDispatcherSubmission> expected,
IReadOnlyList<CurrentRenderDispatcherSubmission> actual)
{
if (expected.Count != actual.Count)
{
return $"field=drawCount expected={expected.Count} "
+ $"actual={actual.Count}";
}
for (int index = 0; index < expected.Count; index++)
{
if (expected[index] != actual[index])
{
return $"field=draw index={index} "
+ $"expected={expected[index]} "
+ $"actual={actual[index]}";
}
}
return null;
}
private static string? CompareSelection(
IReadOnlyList<SelectionKey> expected,
IReadOnlyList<SelectionKey> actual)
{
if (expected.Count != actual.Count)
{
return $"field=partCount expected={expected.Count} "
+ $"actual={actual.Count}";
}
for (int index = 0; index < expected.Count; index++)
{
if (expected[index] != actual[index])
{
return $"field=part index={index} "
+ $"expected={expected[index]} "
+ $"actual={actual[index]}";
}
}
return null;
}
private static RenderSceneHash128 BuildSubmissionDigest(
IReadOnlyList<CurrentRenderDispatcherSubmission> submissions)
{
StableRenderHash128 hash = StableRenderHash128.Create();
hash.Add(submissions.Count);
for (int index = 0; index < submissions.Count; index++)
{
CurrentRenderDispatcherSubmission submission =
submissions[index];
hash.Add(submission.VisibleInstanceCount);
hash.Add(submission.ImmediateInstanceCount);
hash.Add(submission.OpaqueGroupCount);
hash.Add(submission.TransparentGroupCount);
hash.Add(submission.TransparentDeferred);
hash.Add(submission.OpaqueDigest.Low);
hash.Add(submission.OpaqueDigest.High);
hash.Add(submission.TransparentDigest.Low);
hash.Add(submission.TransparentDigest.High);
hash.Add(submission.TransparentSetDigest.Low);
hash.Add(submission.TransparentSetDigest.High);
hash.Add(submission.Digest.Low);
hash.Add(submission.Digest.High);
}
return hash.Finish();
}
private static void BuildSelectionKeys(
IReadOnlyList<CurrentRenderSelectionFingerprint> source,
List<SelectionKey> destination)
{
destination.Clear();
if (destination.Capacity < source.Count)
destination.Capacity = source.Count;
for (int index = 0; index < source.Count; index++)
{
CurrentRenderSelectionFingerprint part = source[index];
destination.Add(new SelectionKey(
part.ServerGuid,
part.LocalEntityId,
part.PartIndex,
part.GfxObjId,
part.LocalToWorld,
part.Geometry));
}
destination.Sort(SelectionKeyComparer.Instance);
}
private static RenderSceneHash128 BuildSelectionDigest(
IReadOnlyList<SelectionKey> parts)
{
StableRenderHash128 hash = StableRenderHash128.Create();
hash.Add(parts.Count);
for (int index = 0; index < parts.Count; index++)
{
SelectionKey part = parts[index];
hash.Add(part.ServerGuid);
hash.Add(part.LocalEntityId);
hash.Add(part.PartIndex);
hash.Add(part.GfxObjId);
hash.Add(part.LocalToWorld);
hash.Add(part.Geometry.Low);
hash.Add(part.Geometry.High);
}
return hash.Finish();
}
private PackedInputComparison ComparePackedInput(
in RenderFrameView view,
uint tupleLandblockId)
@ -492,6 +787,45 @@ internal sealed class RenderScenePViewFrameProductController :
RenderSceneHash128 ExpectedDigest,
RenderSceneHash128 ActualDigest);
private readonly record struct ClassifiedOutputComparison(
int ExpectedDrawCount,
int ActualDrawCount,
RenderSceneHash128 ExpectedDigest,
RenderSceneHash128 ActualDigest,
int ExpectedSelectionCount,
int ActualSelectionCount,
RenderSceneHash128 ExpectedSelectionDigest,
RenderSceneHash128 ActualSelectionDigest);
private readonly record struct SelectionKey(
uint ServerGuid,
uint LocalEntityId,
int PartIndex,
uint GfxObjId,
Matrix4x4 LocalToWorld,
RenderSceneHash128 Geometry);
private sealed class SelectionKeyComparer : IComparer<SelectionKey>
{
public static SelectionKeyComparer Instance { get; } = new();
private SelectionKeyComparer()
{
}
public int Compare(SelectionKey left, SelectionKey right)
{
int value = left.LocalEntityId.CompareTo(
right.LocalEntityId);
if (value != 0) return value;
value = left.PartIndex.CompareTo(right.PartIndex);
if (value != 0) return value;
value = left.GfxObjId.CompareTo(right.GfxObjId);
if (value != 0) return value;
return left.ServerGuid.CompareTo(right.ServerGuid);
}
}
private readonly record struct PackedInputKey(
int DrawSequence,
WbDrawDispatcher.EntitySet Set,