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

@ -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);
}

View file

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