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
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();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue