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
|
|
@ -394,7 +394,8 @@ internal sealed class FrameRootCompositionPhase
|
|||
? new RenderScenePViewFrameProductController(
|
||||
live.RenderSceneShadow,
|
||||
currentRenderSceneOracle,
|
||||
message => d.Log("[UI-PROBE] " + message))
|
||||
message => d.Log("[UI-PROBE] " + message),
|
||||
live.DrawDispatcher)
|
||||
: null;
|
||||
live.DrawDispatcher.SetCurrentRenderSceneObserver(
|
||||
currentRenderSceneOracle);
|
||||
|
|
|
|||
|
|
@ -259,7 +259,8 @@ public sealed class RetailPViewRenderer
|
|||
ctx.Cells,
|
||||
ctx.AnimatedEntityIds,
|
||||
ctx.PlayerLandblockId ?? 0,
|
||||
ctx.RootCell.IsOutdoorNode);
|
||||
ctx.RootCell.IsOutdoorNode,
|
||||
ctx.CameraWorldPosition);
|
||||
return result;
|
||||
}
|
||||
catch
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Selection;
|
||||
namespace AcDream.App.Rendering.Selection;
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -14,3 +15,19 @@ internal interface IRetailSelectionRenderSink
|
|||
uint gfxObjId,
|
||||
Matrix4x4 partWorld);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Read-only mirror of the exact geometry and drawing-sphere acceptance used
|
||||
/// by the retained selection scene. G2's packed dispatcher referee uses it
|
||||
/// without publishing a second picking frame.
|
||||
/// </summary>
|
||||
internal interface IRetailSelectionRenderOracle
|
||||
{
|
||||
bool TryCreateVisiblePart(
|
||||
uint serverGuid,
|
||||
uint localEntityId,
|
||||
int partIndex,
|
||||
uint gfxObjId,
|
||||
Matrix4x4 partWorld,
|
||||
out RetailSelectionPart part);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ internal interface IWorldSceneSelectionFrame
|
|||
|
||||
internal sealed class RetailSelectionScene :
|
||||
IRetailSelectionRenderSink,
|
||||
IRetailSelectionRenderOracle,
|
||||
IRetailSelectionLightingSource,
|
||||
IWorldSceneSelectionFrame
|
||||
{
|
||||
|
|
@ -128,32 +129,65 @@ internal sealed class RetailSelectionScene :
|
|||
return;
|
||||
if (serverGuid == 0u)
|
||||
return;
|
||||
|
||||
if (!_buildingKeys.Add(new PartKey(
|
||||
localEntityId,
|
||||
partIndex,
|
||||
gfxObjId)))
|
||||
return;
|
||||
RetailSelectionMesh? mesh = _geometry.Resolve(gfxObjId);
|
||||
if (mesh is null)
|
||||
return;
|
||||
if (_viewFrustum is not { } frustum
|
||||
|| !DrawingSphereIntersectsFrustum(mesh, partWorld, frustum))
|
||||
if (!TryCreateVisiblePart(
|
||||
serverGuid,
|
||||
localEntityId,
|
||||
partIndex,
|
||||
gfxObjId,
|
||||
partWorld,
|
||||
out RetailSelectionPart part))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_building.Add(new RetailSelectionPart(
|
||||
serverGuid,
|
||||
localEntityId,
|
||||
partIndex,
|
||||
partWorld,
|
||||
mesh));
|
||||
_building.Add(part);
|
||||
_currentRenderSceneObserver?.ObserveSelectionPart(
|
||||
serverGuid,
|
||||
localEntityId,
|
||||
partIndex,
|
||||
gfxObjId,
|
||||
partWorld,
|
||||
part.Mesh);
|
||||
}
|
||||
|
||||
public bool TryCreateVisiblePart(
|
||||
uint serverGuid,
|
||||
uint localEntityId,
|
||||
int partIndex,
|
||||
uint gfxObjId,
|
||||
Matrix4x4 partWorld,
|
||||
out RetailSelectionPart part)
|
||||
{
|
||||
if (serverGuid == 0u)
|
||||
{
|
||||
part = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
RetailSelectionMesh? mesh = _geometry.Resolve(gfxObjId);
|
||||
if (mesh is null
|
||||
|| _viewFrustum is not { } frustum
|
||||
|| !DrawingSphereIntersectsFrustum(
|
||||
mesh,
|
||||
partWorld,
|
||||
frustum))
|
||||
{
|
||||
part = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
part = new RetailSelectionPart(
|
||||
serverGuid,
|
||||
localEntityId,
|
||||
partIndex,
|
||||
partWorld,
|
||||
mesh);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void CompleteFrame()
|
||||
|
|
|
|||
459
src/AcDream.App/Rendering/Wb/WbDrawDispatcher.PackedOracle.cs
Normal file
459
src/AcDream.App/Rendering/Wb/WbDrawDispatcher.PackedOracle.cs
Normal file
|
|
@ -0,0 +1,459 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Rendering.Selection;
|
||||
using AcDream.Core.Lighting;
|
||||
using AcDream.Core.Meshing;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.World;
|
||||
using DatReaderWriter.Enums;
|
||||
|
||||
namespace AcDream.App.Rendering.Wb;
|
||||
|
||||
/// <summary>
|
||||
/// G2 compare-only classification of the scene-built frame product. This file
|
||||
/// deliberately shares the production dispatcher's exact mesh, texture,
|
||||
/// light, translucency, selection, grouping, and ordering owners while keeping
|
||||
/// its group storage separate. It never uploads or draws.
|
||||
/// </summary>
|
||||
public sealed unsafe partial class WbDrawDispatcher
|
||||
{
|
||||
private readonly Dictionary<RenderProjectionId, RenderFrameEntityCandidate>
|
||||
_packedEntityById = [];
|
||||
private readonly Dictionary<GroupKey, InstanceGroup> _packedGroups = [];
|
||||
private readonly List<GroupKey> _packedRetiredGroupKeys = [];
|
||||
private readonly List<InstanceGroup> _packedOpaque = [];
|
||||
private readonly List<InstanceGroup> _packedTransparent = [];
|
||||
private readonly List<AlphaFingerprint>
|
||||
_packedAlphaFingerprintScratch = [];
|
||||
private readonly List<CurrentRenderDispatcherSubmission>
|
||||
_packedSubmissions = [];
|
||||
private readonly List<CurrentRenderSelectionFingerprint>
|
||||
_packedSelectionParts = [];
|
||||
private readonly HashSet<PackedSelectionKey> _packedSelectionKeys = [];
|
||||
private long _packedGroupFrame;
|
||||
private long _nextPackedGroupRegistration = 1;
|
||||
private int _nextPackedInstanceSubmissionOrder;
|
||||
|
||||
internal IReadOnlyList<CurrentRenderDispatcherSubmission>
|
||||
PackedDispatcherSubmissions => _packedSubmissions;
|
||||
|
||||
internal IReadOnlyList<CurrentRenderSelectionFingerprint>
|
||||
PackedSelectionParts => _packedSelectionParts;
|
||||
|
||||
internal void BuildPackedDispatcherOracle(
|
||||
in RenderFrameView view,
|
||||
uint tupleLandblockId,
|
||||
Vector3 cameraWorldPosition)
|
||||
{
|
||||
if (_packedGroupFrame == long.MaxValue)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Packed dispatcher frame identity was exhausted.");
|
||||
}
|
||||
|
||||
_packedGroupFrame++;
|
||||
PruneInstanceGroupsUnusedBeforeFrame(
|
||||
_packedGroups,
|
||||
_packedRetiredGroupKeys,
|
||||
_packedGroupFrame - 1);
|
||||
_packedEntityById.Clear();
|
||||
_packedSubmissions.Clear();
|
||||
_packedSelectionParts.Clear();
|
||||
_packedSelectionKeys.Clear();
|
||||
|
||||
ReadOnlySpan<RenderFrameEntityCandidate> entities =
|
||||
view.EntityCandidates;
|
||||
for (int i = 0; i < entities.Length; i++)
|
||||
{
|
||||
if (!_packedEntityById.TryAdd(
|
||||
entities[i].Projection.Id,
|
||||
entities[i]))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Packed dispatcher received duplicate projection "
|
||||
+ $"{entities[i].Projection.Id}.");
|
||||
}
|
||||
}
|
||||
|
||||
ReadOnlySpan<RenderFrameMeshPart> meshParts = view.MeshParts;
|
||||
ReadOnlySpan<RenderProjectionRecord> routeCandidates =
|
||||
view.RouteCandidates;
|
||||
ReadOnlySpan<RenderFrameCandidateRange> ranges =
|
||||
view.RouteRanges;
|
||||
for (int rangeIndex = 0;
|
||||
rangeIndex < ranges.Length;
|
||||
rangeIndex++)
|
||||
{
|
||||
_nextPackedInstanceSubmissionOrder = 0;
|
||||
foreach (InstanceGroup group in _packedGroups.Values)
|
||||
group.ClearPerInstanceData();
|
||||
|
||||
RenderFrameCandidateRange range = ranges[rangeIndex];
|
||||
int end = checked(range.Offset + range.Count);
|
||||
if ((uint)range.Offset > (uint)routeCandidates.Length
|
||||
|| (uint)end > (uint)routeCandidates.Length)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Packed dispatcher route {rangeIndex} exceeds "
|
||||
+ "candidate storage.");
|
||||
}
|
||||
|
||||
for (int routeIndex = range.Offset;
|
||||
routeIndex < end;
|
||||
routeIndex++)
|
||||
{
|
||||
RenderProjectionRecord projection =
|
||||
routeCandidates[routeIndex];
|
||||
if ((projection.Flags & RenderProjectionFlags.Draw) == 0)
|
||||
continue;
|
||||
if (!_packedEntityById.TryGetValue(
|
||||
projection.Id,
|
||||
out RenderFrameEntityCandidate source))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Packed route references absent entity "
|
||||
+ $"{projection.Id}.");
|
||||
}
|
||||
|
||||
int meshEnd = checked(
|
||||
source.MeshPartOffset + source.MeshPartCount);
|
||||
if ((uint)source.MeshPartOffset
|
||||
> (uint)meshParts.Length
|
||||
|| (uint)meshEnd > (uint)meshParts.Length)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Packed entity {projection.Id} exceeds mesh "
|
||||
+ "part storage.");
|
||||
}
|
||||
|
||||
RenderInstanceCandidate candidate =
|
||||
RenderInstanceCandidate.FromFrame(
|
||||
in source,
|
||||
tupleLandblockId);
|
||||
ClassifyPackedEntity(
|
||||
in candidate,
|
||||
meshParts.Slice(
|
||||
source.MeshPartOffset,
|
||||
source.MeshPartCount));
|
||||
}
|
||||
|
||||
bool deferTransparent =
|
||||
_alphaQueue?.IsCollecting == true;
|
||||
InstanceLayoutCounts counts = PartitionInstanceGroups(
|
||||
_packedGroups.Values,
|
||||
deferTransparent,
|
||||
cameraWorldPosition,
|
||||
_packedOpaque,
|
||||
_packedTransparent);
|
||||
_packedOpaque.Sort(CompareOpaqueSubmissionOrder);
|
||||
if (!deferTransparent)
|
||||
{
|
||||
_packedTransparent.Sort(
|
||||
CompareTransparentSubmissionOrder);
|
||||
}
|
||||
|
||||
_packedSubmissions.Add(CreateDispatcherSubmission(
|
||||
counts.VisibleInstances,
|
||||
counts.ImmediateInstances,
|
||||
deferTransparent,
|
||||
_packedOpaque,
|
||||
_packedTransparent,
|
||||
cameraWorldPosition,
|
||||
_packedAlphaFingerprintScratch));
|
||||
}
|
||||
}
|
||||
|
||||
private void ClassifyPackedEntity(
|
||||
in RenderInstanceCandidate entity,
|
||||
ReadOnlySpan<RenderFrameMeshPart> meshParts)
|
||||
{
|
||||
(uint slot, bool culled) = ResolveSlotForFrame(
|
||||
_clipRoutingActive,
|
||||
entity.ServerGuid,
|
||||
entity.ParentCell,
|
||||
_cellIdToSlot,
|
||||
_outdoorSlot,
|
||||
_outdoorVisible);
|
||||
if (culled)
|
||||
return;
|
||||
|
||||
ResolvePackedLightSet(
|
||||
in entity,
|
||||
out InstanceLightSet lights,
|
||||
out bool indoor);
|
||||
Vector2 selectionLighting =
|
||||
_selectionLighting?.TryGetLighting(
|
||||
entity.ServerGuid,
|
||||
entity.LocalEntityId,
|
||||
out RetailSelectionLighting lighting) == true
|
||||
? new Vector2(
|
||||
lighting.Luminosity,
|
||||
lighting.Diffuse)
|
||||
: new Vector2(0f, 1f);
|
||||
PaletteCompositeIdentity paletteIdentity = default;
|
||||
if (entity.PaletteOverride is not null)
|
||||
{
|
||||
paletteIdentity = TextureCache.GetPaletteIdentity(
|
||||
entity.PaletteOverride);
|
||||
}
|
||||
|
||||
for (int meshIndex = 0;
|
||||
meshIndex < meshParts.Length;
|
||||
meshIndex++)
|
||||
{
|
||||
RenderFrameMeshPart packedPart = meshParts[meshIndex];
|
||||
if (packedPart.ProjectionId != entity.ProjectionId)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Packed mesh part {packedPart.PartIndex} belongs "
|
||||
+ $"to {packedPart.ProjectionId}, expected "
|
||||
+ $"{entity.ProjectionId}.");
|
||||
}
|
||||
|
||||
int partIndex = packedPart.PartIndex;
|
||||
MeshRef meshRef = packedPart.MeshRef;
|
||||
ObjectRenderData? renderData =
|
||||
_meshAdapter.TryGetRenderData(meshRef.GfxObjId);
|
||||
if (renderData is null)
|
||||
continue;
|
||||
|
||||
if (renderData.IsSetup
|
||||
&& renderData.SetupParts.Count > 0)
|
||||
{
|
||||
for (int setupPartIndex = 0;
|
||||
setupPartIndex < renderData.SetupParts.Count;
|
||||
setupPartIndex++)
|
||||
{
|
||||
(ulong gfxObjId, Matrix4x4 partTransform) =
|
||||
renderData.SetupParts[setupPartIndex];
|
||||
ObjectRenderData? partData =
|
||||
_meshAdapter.TryGetRenderData(gfxObjId);
|
||||
if (partData is null)
|
||||
continue;
|
||||
|
||||
float opacity = PackedPartOpacity(
|
||||
entity.LocalEntityId,
|
||||
(uint)setupPartIndex);
|
||||
if (opacity <= 0f)
|
||||
continue;
|
||||
|
||||
Matrix4x4 model = ComposePartWorldMatrix(
|
||||
entity.RootWorld,
|
||||
meshRef.PartTransform,
|
||||
partTransform);
|
||||
ClassifyPackedBatches(
|
||||
partData,
|
||||
model,
|
||||
in entity,
|
||||
meshRef,
|
||||
paletteIdentity,
|
||||
slot,
|
||||
lights,
|
||||
indoor,
|
||||
selectionLighting,
|
||||
opacity);
|
||||
AddPackedSelectionPart(
|
||||
in entity,
|
||||
unchecked(
|
||||
(partIndex << 16)
|
||||
| (setupPartIndex & 0xFFFF)),
|
||||
(uint)gfxObjId,
|
||||
model);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
float opacity = PackedPartOpacity(
|
||||
entity.LocalEntityId,
|
||||
0u);
|
||||
if (opacity <= 0f)
|
||||
continue;
|
||||
|
||||
Matrix4x4 model =
|
||||
meshRef.PartTransform * entity.RootWorld;
|
||||
ClassifyPackedBatches(
|
||||
renderData,
|
||||
model,
|
||||
in entity,
|
||||
meshRef,
|
||||
paletteIdentity,
|
||||
slot,
|
||||
lights,
|
||||
indoor,
|
||||
selectionLighting,
|
||||
opacity);
|
||||
AddPackedSelectionPart(
|
||||
in entity,
|
||||
partIndex,
|
||||
meshRef.GfxObjId,
|
||||
model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private float PackedPartOpacity(
|
||||
uint localEntityId,
|
||||
uint setupPartIndex)
|
||||
{
|
||||
if (!_translucencyFades.TryGetCurrentValue(
|
||||
localEntityId,
|
||||
setupPartIndex,
|
||||
out float translucency))
|
||||
{
|
||||
return 1f;
|
||||
}
|
||||
|
||||
return translucency >= 1f
|
||||
? 0f
|
||||
: 1f - translucency;
|
||||
}
|
||||
|
||||
private void ClassifyPackedBatches(
|
||||
ObjectRenderData renderData,
|
||||
Matrix4x4 model,
|
||||
in RenderInstanceCandidate entity,
|
||||
MeshRef meshRef,
|
||||
PaletteCompositeIdentity paletteIdentity,
|
||||
uint slot,
|
||||
InstanceLightSet lights,
|
||||
bool indoor,
|
||||
Vector2 selectionLighting,
|
||||
float opacity)
|
||||
{
|
||||
for (int batchIndex = 0;
|
||||
batchIndex < renderData.Batches.Count;
|
||||
batchIndex++)
|
||||
{
|
||||
ObjectRenderBatch batch =
|
||||
renderData.Batches[batchIndex];
|
||||
TranslucencyKind translucency = batch.Translucency;
|
||||
if (opacity < 1f && IsOpaque(translucency))
|
||||
translucency = TranslucencyKind.AlphaBlend;
|
||||
|
||||
ResolvedTexture texture = ResolveTexture(
|
||||
in entity,
|
||||
meshRef,
|
||||
batch,
|
||||
paletteIdentity,
|
||||
out _);
|
||||
if (texture.Handle == 0)
|
||||
continue;
|
||||
|
||||
var key = new GroupKey(
|
||||
batch.FirstIndex,
|
||||
(int)batch.BaseVertex,
|
||||
batch.IndexCount,
|
||||
texture.Handle,
|
||||
texture.Layer,
|
||||
translucency,
|
||||
batch.CullMode);
|
||||
InstanceGroup group = GetOrCreatePackedGroup(key);
|
||||
group.Matrices.Add(model);
|
||||
group.LocalSortCenters.Add(renderData.SortCenter);
|
||||
group.SubmissionOrders.Add(
|
||||
_nextPackedInstanceSubmissionOrder++);
|
||||
group.Slots.Add(slot);
|
||||
group.LightSets.Add(lights);
|
||||
group.IndoorFlags.Add(indoor ? 1u : 0u);
|
||||
group.Opacities.Add(opacity);
|
||||
group.SelectionLighting.Add(selectionLighting);
|
||||
}
|
||||
}
|
||||
|
||||
private InstanceGroup GetOrCreatePackedGroup(GroupKey key)
|
||||
{
|
||||
if (_packedGroups.TryGetValue(
|
||||
key,
|
||||
out InstanceGroup? group))
|
||||
{
|
||||
group.LastUsedFrame = _packedGroupFrame;
|
||||
return group;
|
||||
}
|
||||
if (_nextPackedGroupRegistration == long.MaxValue)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
"Packed instance-group registration space was exhausted.");
|
||||
}
|
||||
|
||||
group = new InstanceGroup
|
||||
{
|
||||
FirstIndex = key.FirstIndex,
|
||||
BaseVertex = key.BaseVertex,
|
||||
IndexCount = key.IndexCount,
|
||||
BindlessTextureHandle = key.BindlessTextureHandle,
|
||||
TextureLayer = key.TextureLayer,
|
||||
Translucency = key.Translucency,
|
||||
CullMode = key.CullMode,
|
||||
Registration = _nextPackedGroupRegistration++,
|
||||
LastUsedFrame = _packedGroupFrame,
|
||||
};
|
||||
_packedGroups.Add(key, group);
|
||||
return group;
|
||||
}
|
||||
|
||||
private void ResolvePackedLightSet(
|
||||
in RenderInstanceCandidate entity,
|
||||
out InstanceLightSet lights,
|
||||
out bool indoor)
|
||||
{
|
||||
indoor = IndoorObjectReceivesTorches(entity.ParentCell);
|
||||
lights = InstanceLightSet.Disabled;
|
||||
IReadOnlyList<LightSource>? snapshot = _pointSnapshot;
|
||||
if (!indoor || snapshot is null || snapshot.Count == 0)
|
||||
return;
|
||||
|
||||
Vector3 center =
|
||||
(entity.Bounds.Minimum + entity.Bounds.Maximum) * 0.5f;
|
||||
float radius =
|
||||
(entity.Bounds.Maximum - entity.Bounds.Minimum)
|
||||
.Length() * 0.5f;
|
||||
Span<int> selected =
|
||||
stackalloc int[LightManager.MaxLightsPerObject];
|
||||
selected.Fill(-1);
|
||||
LightManager.SelectForObject(
|
||||
snapshot,
|
||||
center,
|
||||
radius,
|
||||
selected);
|
||||
lights = InstanceLightSet.From(selected);
|
||||
}
|
||||
|
||||
private void AddPackedSelectionPart(
|
||||
in RenderInstanceCandidate entity,
|
||||
int partIndex,
|
||||
uint gfxObjId,
|
||||
Matrix4x4 localToWorld)
|
||||
{
|
||||
if (_selectionSink is not IRetailSelectionRenderOracle oracle
|
||||
|| !_packedSelectionKeys.Add(new PackedSelectionKey(
|
||||
entity.LocalEntityId,
|
||||
partIndex,
|
||||
gfxObjId))
|
||||
|| !oracle.TryCreateVisiblePart(
|
||||
entity.ServerGuid,
|
||||
entity.LocalEntityId,
|
||||
partIndex,
|
||||
gfxObjId,
|
||||
localToWorld,
|
||||
out RetailSelectionPart part))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_packedSelectionParts.Add(
|
||||
new CurrentRenderSelectionFingerprint(
|
||||
Sequence: _packedSelectionParts.Count,
|
||||
ServerGuid: part.ServerGuid,
|
||||
LocalEntityId: part.LocalEntityId,
|
||||
PartIndex: part.PartIndex,
|
||||
GfxObjId: gfxObjId,
|
||||
LocalToWorld: part.LocalToWorld,
|
||||
Geometry: CurrentRenderSceneOracle
|
||||
.FingerprintSelectionGeometry(part.Mesh)));
|
||||
}
|
||||
|
||||
private readonly record struct PackedSelectionKey(
|
||||
uint LocalEntityId,
|
||||
int PartIndex,
|
||||
uint GfxObjId);
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -318,6 +318,9 @@ public sealed class CurrentRenderSceneOracleTests
|
|||
OpaqueGroupCount: 1,
|
||||
TransparentGroupCount: 1,
|
||||
TransparentDeferred: true,
|
||||
OpaqueDigest: new RenderSceneHash128(11, 12),
|
||||
TransparentDigest: new RenderSceneHash128(21, 22),
|
||||
TransparentSetDigest: new RenderSceneHash128(31, 32),
|
||||
Digest: new RenderSceneHash128(101, 202));
|
||||
oracle.ObserveDispatcherSubmission(in submission);
|
||||
CurrentRenderSceneOracleSnapshot first = oracle.Snapshot;
|
||||
|
|
|
|||
|
|
@ -86,6 +86,58 @@ public class InstanceGroupClearTests
|
|||
Assert.Equal(6, immediate.ImmediateInstances);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DispatcherFingerprint_EqualDistanceAlphaUsesOriginalSubmissionOrder()
|
||||
{
|
||||
WbDrawDispatcher.InstanceGroup first = MakeCompleteGroup(
|
||||
textureHandle: 0xAA,
|
||||
submissionOrder: 0);
|
||||
WbDrawDispatcher.InstanceGroup second = MakeCompleteGroup(
|
||||
textureHandle: 0xBB,
|
||||
submissionOrder: 1);
|
||||
var scratch = new List<WbDrawDispatcher.AlphaFingerprint>();
|
||||
|
||||
var forward = WbDrawDispatcher.CreateDispatcherSubmission(
|
||||
visibleInstanceCount: 2,
|
||||
immediateInstanceCount: 0,
|
||||
deferTransparent: true,
|
||||
opaque: [],
|
||||
transparent: [first, second],
|
||||
cameraWorldPosition: Vector3.Zero,
|
||||
alphaScratch: scratch);
|
||||
var reversedMaterialGroups =
|
||||
WbDrawDispatcher.CreateDispatcherSubmission(
|
||||
visibleInstanceCount: 2,
|
||||
immediateInstanceCount: 0,
|
||||
deferTransparent: true,
|
||||
opaque: [],
|
||||
transparent: [second, first],
|
||||
cameraWorldPosition: Vector3.Zero,
|
||||
alphaScratch: scratch);
|
||||
|
||||
Assert.Equal(
|
||||
forward.TransparentDigest,
|
||||
reversedMaterialGroups.TransparentDigest);
|
||||
|
||||
first.SubmissionOrders[0] = 1;
|
||||
second.SubmissionOrders[0] = 0;
|
||||
var reversedSubmission = WbDrawDispatcher.CreateDispatcherSubmission(
|
||||
visibleInstanceCount: 2,
|
||||
immediateInstanceCount: 0,
|
||||
deferTransparent: true,
|
||||
opaque: [],
|
||||
transparent: [first, second],
|
||||
cameraWorldPosition: Vector3.Zero,
|
||||
alphaScratch: scratch);
|
||||
|
||||
Assert.NotEqual(
|
||||
forward.TransparentDigest,
|
||||
reversedSubmission.TransparentDigest);
|
||||
Assert.Equal(
|
||||
forward.TransparentSetDigest,
|
||||
reversedSubmission.TransparentSetDigest);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CachedGroupHandle_RequiresLiveMatchingRegistration()
|
||||
{
|
||||
|
|
@ -210,9 +262,34 @@ public class InstanceGroupClearTests
|
|||
return group;
|
||||
}
|
||||
|
||||
private static WbDrawDispatcher.InstanceGroup MakeCompleteGroup(
|
||||
ulong textureHandle,
|
||||
int submissionOrder)
|
||||
{
|
||||
var group = new WbDrawDispatcher.InstanceGroup
|
||||
{
|
||||
FirstIndex = 0,
|
||||
BaseVertex = 0,
|
||||
IndexCount = 6,
|
||||
BindlessTextureHandle = textureHandle,
|
||||
TextureLayer = 0,
|
||||
Translucency = TranslucencyKind.AlphaBlend,
|
||||
};
|
||||
group.Matrices.Add(Matrix4x4.CreateTranslation(10f, 0f, 0f));
|
||||
group.LocalSortCenters.Add(Vector3.Zero);
|
||||
group.SubmissionOrders.Add(submissionOrder);
|
||||
group.Slots.Add(0u);
|
||||
group.LightSets.Add(WbDrawDispatcher.InstanceLightSet.Disabled);
|
||||
group.IndoorFlags.Add(0u);
|
||||
group.Opacities.Add(1f);
|
||||
group.SelectionLighting.Add(new Vector2(0f, 1f));
|
||||
return group;
|
||||
}
|
||||
|
||||
// #193 (regression from #188, 2026-07-09): WbDrawDispatcher's InstanceGroup holds
|
||||
// seven per-instance parallel lists — Matrices, LocalSortCenters, Slots,
|
||||
// LightSets, IndoorFlags, Opacities, SelectionLighting — appended in lockstep
|
||||
// eight per-instance parallel lists — Matrices, LocalSortCenters,
|
||||
// SubmissionOrders, Slots, LightSets, IndoorFlags, Opacities, and
|
||||
// SelectionLighting — appended in lockstep
|
||||
// (one entry per drawn instance) every frame. The
|
||||
// per-frame reset must clear ALL of them. #188 added Opacities but left it out of
|
||||
// the inline clear loop, so it grew one float per instance per frame forever; as
|
||||
|
|
@ -225,6 +302,7 @@ public class InstanceGroupClearTests
|
|||
var grp = new WbDrawDispatcher.InstanceGroup();
|
||||
grp.Matrices.Add(Matrix4x4.Identity);
|
||||
grp.LocalSortCenters.Add(Vector3.Zero);
|
||||
grp.SubmissionOrders.Add(0);
|
||||
grp.Slots.Add(1u);
|
||||
grp.LightSets.Add(WbDrawDispatcher.InstanceLightSet.Disabled);
|
||||
grp.IndoorFlags.Add(0u);
|
||||
|
|
@ -235,6 +313,7 @@ public class InstanceGroupClearTests
|
|||
|
||||
Assert.Empty(grp.Matrices);
|
||||
Assert.Empty(grp.LocalSortCenters);
|
||||
Assert.Empty(grp.SubmissionOrders);
|
||||
Assert.Empty(grp.Slots);
|
||||
Assert.Empty(grp.LightSets);
|
||||
Assert.Empty(grp.IndoorFlags);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue