acdream/src/AcDream.App/Rendering/Scene/RenderScenePViewFrameProduct.cs
Erik 4686a1ac74 probe(render): [dyn-route] DynamicLast admission trace + rain PASS note
The owner''s through-wall remote-player report (standing in 0xF4180101,
a player parented at 0xF4180112 - the south hall, geometrically
unseeable through the cathedral - renders through opaque walls; from
0xF4180104 he is correctly hidden). The DynamicLast admission chain
has three gates (indoor / old-apparatus look-in exclusion /
SphereVisibleInCell against old-assembly cell planes), and gate 3
carries a known trapdoor: a zero-plane scissor-fallback slice is
PASS-ALL. This trace logs, per entity ON CHANGE: parent cell, look-in
exclusion hits, the viewcone verdict, and the cell''s lifted-plane
shape, so one 0104<->0101 crossing with the remote player in place
pins the admitting gate. Rain gate 75eab42f is owner-PASSED.

Hermetic 6,762/0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 21:55:24 +02:00

1586 lines
59 KiB
C#

using System.Numerics;
using AcDream.App.Rendering.Wb;
namespace AcDream.App.Rendering.Scene;
internal readonly record struct RenderScenePViewBuildInput(
RenderSceneQuery Scene,
RenderSceneDigest SourceDigest,
PortalVisibilityFrame PortalFrame,
ClipFrameAssembly ClipAssembly,
ViewconeCuller Viewcone,
IReadOnlyList<PortalVisibilityFrame> LookInFrames,
HashSet<uint> DrawableCells,
IRetailPViewCellSource Cells,
HashSet<uint>? AnimatedEntityIds,
bool RootIsOutdoor);
internal readonly record struct RenderFrameProductComparisonSnapshot(
bool Enabled,
ulong ProductFrameSequence,
ulong CurrentPViewFrameSequence,
ulong ComparisonCount,
ulong SuccessfulComparisonCount,
int ExpectedCandidateCount,
int ActualCandidateCount,
long MismatchCount,
string? FirstMismatch,
RenderSceneHash128 ExpectedDigest,
RenderSceneHash128 ActualDigest,
ulong PackedInputComparisonCount,
ulong PackedInputSuccessfulComparisonCount,
int PackedInputExpectedCount,
int PackedInputActualCount,
long PackedInputMismatchCount,
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,
int PackedClassificationProjectionCount,
int PackedClassificationStaticRebuildCount,
int PackedClassificationSameFrameReuseCount,
int PackedClassificationCrossFrameReuseCount,
int PackedClassificationAnimatedCount,
int PackedClassificationRetiredCount,
long PackedClassificationRetainedPayloadBytes,
RenderFrameDiagnosticCounts ProductCounts)
{
public static RenderFrameProductComparisonSnapshot Disabled { get; } =
new(
Enabled: false,
ProductFrameSequence: 0,
CurrentPViewFrameSequence: 0,
ComparisonCount: 0,
SuccessfulComparisonCount: 0,
ExpectedCandidateCount: 0,
ActualCandidateCount: 0,
MismatchCount: 0,
FirstMismatch: null,
ExpectedDigest: RenderSceneHash128.Empty,
ActualDigest: RenderSceneHash128.Empty,
PackedInputComparisonCount: 0,
PackedInputSuccessfulComparisonCount: 0,
PackedInputExpectedCount: 0,
PackedInputActualCount: 0,
PackedInputMismatchCount: 0,
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,
PackedClassificationProjectionCount: 0,
PackedClassificationStaticRebuildCount: 0,
PackedClassificationSameFrameReuseCount: 0,
PackedClassificationCrossFrameReuseCount: 0,
PackedClassificationAnimatedCount: 0,
PackedClassificationRetiredCount: 0,
PackedClassificationRetainedPayloadBytes: 0,
ProductCounts: default);
}
internal interface IRenderFrameProductSnapshotSource
{
RenderFrameProductComparisonSnapshot Snapshot { get; }
}
/// <summary>
/// Owns the same-frame PView product. Diagnostic composition may compare it
/// with the retired current-path observer; production borrows the exact same
/// arena and supplies its ordered ranges directly to the dispatcher.
/// </summary>
internal sealed class RenderScenePViewFrameProductController :
IRenderFrameProductSnapshotSource
{
private readonly RenderSceneShadowRuntime _shadow;
private readonly CurrentRenderSceneOracle? _current;
/// <summary>Campaign FW3.2b-2: the frame's retained-scene read view for
/// the walk's world-data provider (the same query
/// <see cref="BuildAndBorrow"/> reads its routes from).</summary>
internal RenderSceneQuery SceneQuery => _shadow.Query;
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;
private ulong _successfulComparisonCount;
private long _mismatchCount;
private string? _firstMismatch;
private ulong _packedInputComparisonCount;
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;
private PackedClassificationCacheSnapshot
_packedClassification;
public RenderScenePViewFrameProductController(
RenderSceneShadowRuntime shadow,
CurrentRenderSceneOracle? current = null,
Action<string>? log = null,
WbDrawDispatcher? dispatcher = null)
{
_shadow = shadow ?? throw new ArgumentNullException(nameof(shadow));
_current = current;
_log = log;
_dispatcher = dispatcher;
Snapshot = RenderFrameProductComparisonSnapshot.Disabled with
{
Enabled = true,
};
}
public RenderFrameProductComparisonSnapshot Snapshot { get; private set; }
private CurrentRenderSceneOracle Current =>
_current
?? throw new InvalidOperationException(
"Current-path comparison was requested without a referee.");
public RenderFrameView BuildAndBorrow(
PortalVisibilityFrame portalFrame,
ClipFrameAssembly clipAssembly,
ViewconeCuller viewcone,
IReadOnlyList<PortalVisibilityFrame> lookInFrames,
HashSet<uint> drawableCells,
IRetailPViewCellSource cells,
HashSet<uint>? animatedEntityIds,
bool rootIsOutdoor)
{
ArgumentNullException.ThrowIfNull(portalFrame);
ArgumentNullException.ThrowIfNull(clipAssembly);
ArgumentNullException.ThrowIfNull(viewcone);
ArgumentNullException.ThrowIfNull(lookInFrames);
ArgumentNullException.ThrowIfNull(drawableCells);
ArgumentNullException.ThrowIfNull(cells);
ulong frameSequence = checked(++_productFrameSequence);
RenderSceneQuery scene = _shadow.Query;
RenderSceneDigest sourceDigest = _current is null
? new RenderSceneDigest(
scene.Generation,
scene.Counts,
default)
: _shadow.BuildDigest();
var input = new RenderScenePViewBuildInput(
scene,
sourceDigest,
portalFrame,
clipAssembly,
viewcone,
lookInFrames,
drawableCells,
cells,
animatedEntityIds,
rootIsOutdoor);
_builder.Build(_exchange, frameSequence, in input);
return _exchange.BorrowLatest(
scene.Generation,
frameSequence);
}
public void Release(in RenderFrameView view) =>
_exchange.Release(in view);
public void BuildAndCompare(
PortalVisibilityFrame portalFrame,
ClipFrameAssembly clipAssembly,
ViewconeCuller viewcone,
IReadOnlyList<PortalVisibilityFrame> lookInFrames,
HashSet<uint> drawableCells,
IRetailPViewCellSource cells,
HashSet<uint>? animatedEntityIds,
uint tupleLandblockId,
bool rootIsOutdoor,
Vector3 cameraWorldPosition = default)
{
RenderFrameView view = BuildAndBorrow(
portalFrame,
clipAssembly,
viewcone,
lookInFrames,
drawableCells,
cells,
animatedEntityIds,
rootIsOutdoor);
try
{
Compare(
in view,
tupleLandblockId,
cameraWorldPosition);
}
finally
{
Release(in view);
}
}
public void CompleteProduction(in RenderFrameView view)
{
_packedClassification =
_dispatcher?.PackedClassificationSnapshot ?? default;
if (_current is null)
{
_shadow.ClearDirty();
Snapshot = Snapshot with
{
Enabled = true,
ProductFrameSequence = view.FrameSequence,
ActualCandidateCount = view.RouteCandidates.Length,
PackedClassificationProjectionCount =
_packedClassification.ProjectionCount,
PackedClassificationStaticRebuildCount =
_packedClassification.StaticRebuildCount,
PackedClassificationSameFrameReuseCount =
_packedClassification.SameFrameReuseCount,
PackedClassificationCrossFrameReuseCount =
_packedClassification.CrossFrameReuseCount,
PackedClassificationAnimatedCount =
_packedClassification.AnimatedClassificationCount,
PackedClassificationRetiredCount =
_packedClassification.RetiredProjectionCount,
PackedClassificationRetainedPayloadBytes =
_packedClassification.RetainedPayloadBytes,
ProductCounts = view.DiagnosticCounts,
};
return;
}
CandidateComparison candidates =
CompareCandidateMembership(in view);
if (candidates.Successful)
_shadow.ClearDirty();
Snapshot = Snapshot with
{
Enabled = true,
ProductFrameSequence = view.FrameSequence,
CurrentPViewFrameSequence =
Current.Snapshot.CompletedPViewFrameSequence,
ComparisonCount = _comparisonCount,
SuccessfulComparisonCount = _successfulComparisonCount,
ExpectedCandidateCount = _expected.Count,
ActualCandidateCount = _actual.Count,
MismatchCount = _mismatchCount,
FirstMismatch = _firstMismatch,
ExpectedDigest = candidates.ExpectedDigest,
ActualDigest = candidates.ActualDigest,
PackedClassificationProjectionCount =
_packedClassification.ProjectionCount,
PackedClassificationStaticRebuildCount =
_packedClassification.StaticRebuildCount,
PackedClassificationSameFrameReuseCount =
_packedClassification.SameFrameReuseCount,
PackedClassificationCrossFrameReuseCount =
_packedClassification.CrossFrameReuseCount,
PackedClassificationAnimatedCount =
_packedClassification.AnimatedClassificationCount,
PackedClassificationRetiredCount =
_packedClassification.RetiredProjectionCount,
PackedClassificationRetainedPayloadBytes =
_packedClassification.RetainedPayloadBytes,
ProductCounts = view.DiagnosticCounts,
};
}
private CandidateComparison CompareCandidateMembership(
in RenderFrameView view)
{
_comparisonCount = checked(_comparisonCount + 1);
_expected.Clear();
_actual.Clear();
IReadOnlyList<CurrentRenderPViewCandidateFingerprint> expected =
Current.PViewCandidates;
if (_expected.Capacity < expected.Count)
_expected.Capacity = expected.Count;
for (int index = 0; index < expected.Count; index++)
{
CurrentRenderPViewCandidateFingerprint candidate =
expected[index];
CurrentRenderProjectionFingerprint projection =
candidate.Projection;
_expected.Add(new CandidateKey(
Map(candidate.Route),
candidate.RouteIndex,
candidate.CellId,
ExpectedId(in projection)));
}
ReadOnlySpan<RenderFrameCandidateRange> ranges =
view.RouteRanges;
ReadOnlySpan<RenderProjectionRecord> records =
view.RouteCandidates;
if (_actual.Capacity < records.Length)
_actual.Capacity = records.Length;
for (int rangeIndex = 0;
rangeIndex < ranges.Length;
rangeIndex++)
{
RenderFrameCandidateRange range = ranges[rangeIndex];
int end = checked(range.Offset + range.Count);
if ((uint)range.Offset > (uint)records.Length
|| (uint)end > (uint)records.Length)
{
throw new InvalidOperationException(
$"Render-frame route range {rangeIndex} exceeds its candidate storage.");
}
for (int index = range.Offset; index < end; index++)
{
_actual.Add(new CandidateKey(
range.Route,
range.RouteIndex,
range.CellId,
records[index].Id));
}
}
RenderSceneHash128 expectedDigest = BuildDigest(_expected);
RenderSceneHash128 actualDigest = BuildDigest(_actual);
string? mismatch = CompareKeys();
if (mismatch is null)
{
_successfulComparisonCount =
checked(_successfulComparisonCount + 1);
}
else
{
_mismatchCount = checked(_mismatchCount + 1);
if (_firstMismatch is null)
{
_firstMismatch = mismatch;
_log?.Invoke(
"[render-frame-product] first mismatch: "
+ mismatch);
}
}
return new CandidateComparison(
mismatch is null,
expectedDigest,
actualDigest);
}
private void Compare(
in RenderFrameView view,
uint tupleLandblockId,
Vector3 cameraWorldPosition)
{
CandidateComparison candidates =
CompareCandidateMembership(in view);
RenderSceneHash128 expectedDigest =
candidates.ExpectedDigest;
RenderSceneHash128 actualDigest =
candidates.ActualDigest;
string? mismatch = candidates.Successful
? null
: _firstMismatch;
PackedInputComparison packed =
ComparePackedInput(in view, tupleLandblockId);
ClassifiedOutputComparison classified =
CompareClassifiedOutput(
in view,
tupleLandblockId,
cameraWorldPosition);
if (_dispatcher is not null
&& mismatch is null
&& packed.Successful
&& classified.Successful)
{
// The frame product and retained classifier have consumed every
// dirty fact and matched the production oracle. A later update
// starts the next dirty epoch; a mismatch or exception above
// deliberately leaves dirtiness intact for the next rebuild.
_shadow.ClearDirty();
}
Snapshot = new RenderFrameProductComparisonSnapshot(
Enabled: true,
ProductFrameSequence: view.FrameSequence,
CurrentPViewFrameSequence:
Current.Snapshot.CompletedPViewFrameSequence,
ComparisonCount: _comparisonCount,
SuccessfulComparisonCount: _successfulComparisonCount,
ExpectedCandidateCount: _expected.Count,
ActualCandidateCount: _actual.Count,
MismatchCount: _mismatchCount,
FirstMismatch: _firstMismatch,
ExpectedDigest: expectedDigest,
ActualDigest: actualDigest,
PackedInputComparisonCount: _packedInputComparisonCount,
PackedInputSuccessfulComparisonCount:
_packedInputSuccessfulComparisonCount,
PackedInputExpectedCount: _packedExpected.Count,
PackedInputActualCount: _packedActual.Count,
PackedInputMismatchCount: _packedInputMismatchCount,
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,
PackedClassificationProjectionCount:
_packedClassification.ProjectionCount,
PackedClassificationStaticRebuildCount:
_packedClassification.StaticRebuildCount,
PackedClassificationSameFrameReuseCount:
_packedClassification.SameFrameReuseCount,
PackedClassificationCrossFrameReuseCount:
_packedClassification.CrossFrameReuseCount,
PackedClassificationAnimatedCount:
_packedClassification.AnimatedClassificationCount,
PackedClassificationRetiredCount:
_packedClassification.RetiredProjectionCount,
PackedClassificationRetainedPayloadBytes:
_packedClassification.RetainedPayloadBytes,
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);
_packedClassification =
dispatcher.PackedClassificationSnapshot;
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
+ $" classification={_packedClassification}");
}
}
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
+ $" classification={_packedClassification}");
}
}
return new ClassifiedOutputComparison(
Successful: mismatch is null && selectionMismatch is null,
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)
{
int shared = Math.Min(expected.Count, actual.Count);
for (int index = 0; index < shared; index++)
{
if (expected[index] != actual[index])
{
return $"field=draw index={index} "
+ $"expected={expected[index]} "
+ $"actual={actual[index]}";
}
}
string extra = expected.Count > actual.Count
? $"firstExtraExpected={expected[shared]}"
: $"firstExtraActual={actual[shared]}";
return $"field=drawCount expected={expected.Count} "
+ $"actual={actual.Count} {extra}";
}
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)
{
int shared = Math.Min(expected.Count, actual.Count);
for (int index = 0; index < shared; index++)
{
if (expected[index] != actual[index])
{
return $"field=part index={index} "
+ $"expected={expected[index]} "
+ $"actual={actual[index]}";
}
}
string extra = expected.Count > actual.Count
? $"firstExtraExpected={expected[shared]}"
: $"firstExtraActual={actual[shared]}";
return $"field=partCount expected={expected.Count} "
+ $"actual={actual.Count} {extra}";
}
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)
{
_packedInputComparisonCount =
checked(_packedInputComparisonCount + 1);
_packedExpected.Clear();
_packedActual.Clear();
IReadOnlyList<CurrentRenderDispatcherFingerprint> expected =
Current.DispatcherCandidates;
if (_packedExpected.Capacity < expected.Count)
_packedExpected.Capacity = expected.Count;
for (int index = 0; index < expected.Count; index++)
{
CurrentRenderDispatcherFingerprint candidate = expected[index];
CurrentRenderProjectionFingerprint projection =
candidate.Projection;
_packedExpected.Add(new PackedInputKey(
candidate.DrawSequence,
candidate.Set,
ExpectedId(in projection),
candidate.MeshRefIndex,
candidate.TupleLandblockId,
candidate.CacheLandblockId,
projection.EntityId,
projection.ServerGuid,
projection.SourceId,
projection.ParentCellId,
projection.EffectCellId,
projection.Flags,
projection.Transform,
projection.Geometry,
projection.Appearance,
candidate.GfxObjId,
candidate.PartTransform,
candidate.SurfaceOverrides));
}
ReadOnlySpan<RenderFrameCandidateRange> ranges = view.RouteRanges;
ReadOnlySpan<RenderProjectionRecord> routeCandidates =
view.RouteCandidates;
if (_packedActual.Capacity < view.MeshParts.Length)
_packedActual.Capacity = view.MeshParts.Length;
for (int drawSequence = 0;
drawSequence < ranges.Length;
drawSequence++)
{
RenderFrameCandidateRange range = ranges[drawSequence];
int end = checked(range.Offset + range.Count);
for (int candidateIndex = range.Offset;
candidateIndex < end;
candidateIndex++)
{
RenderProjectionRecord projection =
routeCandidates[candidateIndex];
if ((projection.Flags & RenderProjectionFlags.Draw) == 0)
continue;
IReadOnlyList<AcDream.Core.World.MeshRef>? meshRefs =
projection.EntityPayload.MeshRefs;
int meshCount = meshRefs?.Count ?? 0;
for (int partIndex = 0;
partIndex < meshCount;
partIndex++)
{
AcDream.Core.World.MeshRef meshRef =
meshRefs![partIndex];
_packedActual.Add(new PackedInputKey(
drawSequence,
WbDrawDispatcher.EntitySet.All,
projection.Id,
partIndex,
tupleLandblockId,
DispatcherCacheLandblock(
in projection,
tupleLandblockId),
projection.Source.LocalEntityId,
projection.Source.ServerGuid,
projection.Source.SourceId,
projection.Source.ParentCellId,
projection.Source.EffectCellId,
projection.Source.CurrentProjectionFlags,
projection.Source.TransformFingerprint,
projection.Source.GeometryFingerprint,
projection.Source.AppearanceFingerprint,
meshRef.GfxObjId,
meshRef.PartTransform,
CurrentRenderSceneOracle
.CreateSurfaceOverrideFingerprint(
meshRef.SurfaceOverrides)));
}
}
}
RenderSceneHash128 expectedDigest =
BuildPackedInputDigest(_packedExpected);
RenderSceneHash128 actualDigest =
BuildPackedInputDigest(_packedActual);
string? mismatch = ComparePackedInputKeys(ranges.Length);
if (mismatch is null)
{
_packedInputSuccessfulComparisonCount =
checked(_packedInputSuccessfulComparisonCount + 1);
}
else
{
_packedInputMismatchCount =
checked(_packedInputMismatchCount + 1);
if (_packedInputFirstMismatch is null)
{
_packedInputFirstMismatch = mismatch;
_log?.Invoke(
"[render-packed-input] first mismatch: " + mismatch);
}
}
return new PackedInputComparison(
Successful: mismatch is null,
expectedDigest,
actualDigest);
}
private static uint DispatcherCacheLandblock(
in RenderProjectionRecord projection,
uint tupleLandblockId) =>
projection.Source.ParentCellId != 0
? (projection.Source.ParentCellId & 0xFFFF0000u) | 0xFFFFu
: tupleLandblockId;
private string? ComparePackedInputKeys(int actualDrawCount)
{
if (Current.Snapshot.DispatcherDrawCount != actualDrawCount)
{
return $"field=drawCount expected="
+ $"{Current.Snapshot.DispatcherDrawCount} "
+ $"actual={actualDrawCount}";
}
if (_packedExpected.Count != _packedActual.Count)
{
int shared = Math.Min(
_packedExpected.Count,
_packedActual.Count);
for (int index = 0; index < shared; index++)
{
if (_packedExpected[index] != _packedActual[index])
{
return $"field=meshPart index={index} "
+ $"expected={_packedExpected[index]} "
+ $"actual={_packedActual[index]}";
}
}
string extra = _packedExpected.Count > _packedActual.Count
? $"firstExtraExpected={_packedExpected[shared]}"
: $"firstExtraActual={_packedActual[shared]}";
return $"field=count expected={_packedExpected.Count} "
+ $"actual={_packedActual.Count} {extra}";
}
for (int index = 0; index < _packedExpected.Count; index++)
{
if (_packedExpected[index] != _packedActual[index])
{
return $"field=meshPart index={index} "
+ $"expected={_packedExpected[index]} "
+ $"actual={_packedActual[index]}";
}
}
return null;
}
private static RenderSceneHash128 BuildPackedInputDigest(
List<PackedInputKey> candidates)
{
StableRenderHash128 hash = StableRenderHash128.Create();
hash.Add(candidates.Count);
for (int index = 0; index < candidates.Count; index++)
candidates[index].AddTo(ref hash);
return hash.Finish();
}
private string? CompareKeys()
{
if (_expected.Count != _actual.Count)
{
int shared = Math.Min(_expected.Count, _actual.Count);
for (int index = 0; index < shared; index++)
{
if (_expected[index] != _actual[index])
{
return $"field=candidate index={index} "
+ $"expected={_expected[index]} "
+ $"actual={_actual[index]}";
}
}
string extra = _expected.Count > _actual.Count
? $"firstExtraExpected={_expected[shared]}"
: $"firstExtraActual={_actual[shared]}";
return $"field=count expected={_expected.Count} "
+ $"actual={_actual.Count} {extra}";
}
for (int i = 0; i < _expected.Count; i++)
{
if (_expected[i] != _actual[i])
{
return $"field=candidate index={i} "
+ $"expected={_expected[i]} actual={_actual[i]}";
}
}
return null;
}
private static RenderSceneHash128 BuildDigest(
List<CandidateKey> candidates)
{
StableRenderHash128 hash = StableRenderHash128.Create();
hash.Add(candidates.Count);
for (int i = 0; i < candidates.Count; i++)
{
CandidateKey candidate = candidates[i];
hash.Add((byte)candidate.Route);
hash.Add(candidate.RouteIndex);
hash.Add(candidate.CellId);
candidate.ProjectionId.AddTo(ref hash);
}
return hash.Finish();
}
private static RenderProjectionId ExpectedId(
in CurrentRenderProjectionFingerprint projection) =>
projection.ServerGuid == 0
? StaticRenderProjectionJournal.StaticEntityId(
projection.LandblockId,
projection.EntityId)
: LiveRenderProjectionJournal.ProjectionId(
projection.EntityId);
private static RenderFrameCandidateRoute Map(
CurrentRenderPViewRoute route) =>
route switch
{
CurrentRenderPViewRoute.LandscapeOutdoorStatic =>
RenderFrameCandidateRoute.LandscapeOutdoorStatic,
CurrentRenderPViewRoute.LandscapeBuildingShell =>
RenderFrameCandidateRoute.LandscapeBuildingShell,
CurrentRenderPViewRoute.LandscapeOutsideDynamic =>
RenderFrameCandidateRoute.LandscapeOutsideDynamic,
CurrentRenderPViewRoute.LookInObject =>
RenderFrameCandidateRoute.LookInObject,
CurrentRenderPViewRoute.CellStatic =>
RenderFrameCandidateRoute.CellStatic,
CurrentRenderPViewRoute.DynamicLast =>
RenderFrameCandidateRoute.DynamicLast,
_ => throw new ArgumentOutOfRangeException(
nameof(route),
route,
null),
};
private readonly record struct CandidateKey(
RenderFrameCandidateRoute Route,
int RouteIndex,
uint CellId,
RenderProjectionId ProjectionId);
private readonly record struct PackedInputComparison(
bool Successful,
RenderSceneHash128 ExpectedDigest,
RenderSceneHash128 ActualDigest);
private readonly record struct CandidateComparison(
bool Successful,
RenderSceneHash128 ExpectedDigest,
RenderSceneHash128 ActualDigest);
private readonly record struct ClassifiedOutputComparison(
bool Successful,
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,
RenderProjectionId ProjectionId,
int MeshRefIndex,
uint TupleLandblockId,
uint CacheLandblockId,
uint LocalEntityId,
uint ServerGuid,
uint SourceId,
uint ParentCellId,
uint EffectCellId,
uint SourceFlags,
RenderSceneHash128 Transform,
RenderSceneHash128 Geometry,
RenderSceneHash128 Appearance,
uint GfxObjId,
Matrix4x4 PartTransform,
RenderSceneHash128 SurfaceOverrides)
{
public void AddTo(ref StableRenderHash128 hash)
{
hash.Add(DrawSequence);
hash.Add((int)Set);
ProjectionId.AddTo(ref hash);
hash.Add(MeshRefIndex);
hash.Add(TupleLandblockId);
hash.Add(CacheLandblockId);
hash.Add(LocalEntityId);
hash.Add(ServerGuid);
hash.Add(SourceId);
hash.Add(ParentCellId);
hash.Add(EffectCellId);
hash.Add(SourceFlags);
hash.Add(Transform.Low);
hash.Add(Transform.High);
hash.Add(Geometry.Low);
hash.Add(Geometry.High);
hash.Add(Appearance.Low);
hash.Add(Appearance.High);
hash.Add(GfxObjId);
hash.Add(PartTransform);
hash.Add(SurfaceOverrides.Low);
hash.Add(SurfaceOverrides.High);
}
}
}
/// <summary>
/// Allocation-free scene-query projection for one accepted PView frame.
/// It applies the existing viewcone and outside-stage predicates verbatim,
/// changing only the source of candidate records.
/// </summary>
internal sealed class RenderScenePViewFrameBuilder
{
private const byte EnvCellProjectionDomain = 2;
private readonly HashSet<RenderProjectionId> _projectionIds = [];
private RenderProjectionRecord[] _dynamics = [];
private RenderProjectionRecord[] _cell = [];
private RenderProjectionRecord[] _dirty = [];
private RenderProjectionRecord[] _survivors = [];
// Cells drawn as building look-ins this frame — the DynamicLast route
// honors retail's drawn-once contract (DrawMeshInternal @0x0059F360
// marks every non-player part): an object whose cell drew with a look-in
// must not enter the final dynamics route again.
private readonly HashSet<uint> _lookInCellScratch = new();
private readonly Dictionary<RenderProjectionId, int>
_dynamicPositions = [];
private int _dynamicCount;
// ACDREAM_PROBE_WALK_ROOT companion (throwaway): rate limiter for the
// [walk-dyn] outside-stage classification dump, plus the [dyn-route]
// per-entity on-change admission cache.
private uint _probeDynFrameCounter;
private readonly Dictionary<uint, string> _probeDynRouteStates = new();
private int _dirtyCount;
private RenderSceneGeneration _indexGeneration;
private ulong _indexRevision;
public void Build(
RenderFrameExchange exchange,
ulong frameSequence,
in RenderScenePViewBuildInput input)
{
ArgumentNullException.ThrowIfNull(exchange);
RenderFrameWriter writer =
exchange.BeginBuild(input.Scene.Generation, frameSequence);
try
{
writer.SetBorrowedProducts(
input.PortalFrame,
input.ClipAssembly);
RenderSceneDigest digest = input.SourceDigest;
writer.SetSourceDigest(in digest);
_projectionIds.Clear();
LoadSceneIndices(input.Scene);
// Campaign FW3.2b-2: LandscapeOutdoorStatic, LandscapeBuildingShell,
// and CellStatic no longer emit here — WalkFrameDriver draws every
// outdoor static, building shell, and cell static (including
// look-in cell statics) directly through OrderedDrawStream (plan
// §FW3 "FW3.2b-2 — the production rooting", item 3). LookInObject
// keeps emitting, but BuildLookInRoutes below is now filtered to
// DYNAMIC candidates only — the walk owns that route's statics.
int lookInRouteIndex = 0;
for (int frameIndex = 0;
frameIndex < input.LookInFrames.Count;
frameIndex++)
{
BuildLookInRoutes(
writer,
in input,
frameIndex,
ref lookInRouteIndex);
}
BuildOutsideDynamicRoutes(writer, in input);
BuildDynamicLastRoute(writer, in input);
writer.Publish();
AcknowledgeCachedDirtyRecords();
}
catch
{
writer.Abort();
throw;
}
}
private void LoadSceneIndices(RenderSceneQuery scene)
{
RenderSceneIndexCounts counts = scene.IndexCounts;
ulong revision = scene.IndexRevision;
if (scene.Generation != _indexGeneration
|| revision != _indexRevision)
{
EnsureCapacity(ref _dynamics, counts.Dynamic);
_dynamicCount = scene.CopyIndexTo(
RenderSceneIndex.Dynamic,
_dynamics);
_dynamicCount = CompactAndSort(
_dynamics,
_dynamicCount);
BuildPositionIndex(
_dynamics,
_dynamicCount,
_dynamicPositions);
_indexGeneration = scene.Generation;
_indexRevision = revision;
}
EnsureCapacity(ref _dirty, counts.Dirty);
_dirtyCount = scene.CopyIndexTo(
RenderSceneIndex.Dirty,
_dirty);
for (int index = 0; index < _dirtyCount; index++)
{
RenderProjectionRecord record = _dirty[index];
if (_dynamicPositions.TryGetValue(
record.Id,
out int dynamicPosition))
{
_dynamics[dynamicPosition] = record;
}
}
}
private void AcknowledgeCachedDirtyRecords()
{
for (int index = 0; index < _dirtyCount; index++)
{
RenderProjectionId id = _dirty[index].Id;
if (_dynamicPositions.TryGetValue(
id,
out int dynamicPosition))
{
_dynamics[dynamicPosition] =
_dynamics[dynamicPosition] with
{
DirtyMask = RenderDirtyMask.None,
};
}
}
}
private static void BuildPositionIndex(
RenderProjectionRecord[] records,
int count,
Dictionary<RenderProjectionId, int> positions)
{
positions.Clear();
positions.EnsureCapacity(count);
for (int index = 0; index < count; index++)
positions.Add(records[index].Id, index);
}
/// <summary>Campaign FW3.2b-2: <c>LookInObject</c> now carries DYNAMIC
/// candidates only — the walk draws every look-in cell's STATIC content
/// directly (<c>WalkFrameDriver</c>'s <c>LookInStatic</c> turn, via
/// <c>WalkProductionWorldData.GetCellStatics</c>), so loading this
/// route's cell contents with <c>includeStatics: false</c> is what keeps
/// the two draws from doubling a look-in room's furniture (plan §FW3
/// item 3).</summary>
private void BuildLookInRoutes(
RenderFrameWriter writer,
in RenderScenePViewBuildInput input,
int frameIndex,
ref int routeIndex)
{
PortalVisibilityFrame frame = input.LookInFrames[frameIndex];
for (int i = frame.OrderedVisibleCells.Count - 1; i >= 0; i--)
{
uint cellId = frame.OrderedVisibleCells[i];
var clipKey = new LookInClipCell(frameIndex, cellId);
if (!input.ClipAssembly.LookInCellToViewSlices.TryGetValue(
clipKey,
out ClipViewSlice[]? slices)
|| slices.Length == 0)
{
continue;
}
int count = LoadCell(
input.Scene,
cellId,
includeStatics: false,
includeDynamics: true);
for (int sliceIndex = 0; sliceIndex < slices.Length; sliceIndex++)
{
int currentRouteIndex = routeIndex++;
if (count == 0)
continue;
for (int index = 0; index < count; index++)
AddProjection(
writer,
in _cell[index],
input.AnimatedEntityIds);
writer.AddRouteRange(
RenderFrameCandidateRoute.LookInObject,
currentRouteIndex,
cellId,
_cell.AsSpan(0, count));
}
}
}
private void BuildOutsideDynamicRoutes(
RenderFrameWriter writer,
in RenderScenePViewBuildInput input)
{
if (input.RootIsOutdoor)
return;
int sliceCount = input.ClipAssembly.OutsideViewSlices.Length;
bool probeDynamics =
AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled
&& ++_probeDynFrameCounter % 120 == 0;
for (int sliceIndex = 0; sliceIndex < sliceCount; sliceIndex++)
{
int count = 0;
EnsureCapacity(ref _survivors, _dynamicCount);
for (int i = 0; i < _dynamicCount; i++)
{
RenderProjectionRecord record = _dynamics[i];
Sphere(in record, out Vector3 center, out float radius);
bool outside = RetailPViewRenderer.DynamicDrawsInOutsideStage(
ParentCell(in record),
center,
radius,
input.DrawableCells,
input.Cells);
bool cone = input.Viewcone.SphereVisibleInOutsideSlice(
sliceIndex, in center, radius);
if (probeDynamics)
{
Console.WriteLine(
$"[walk-dyn] slice={sliceIndex} id={record.Source.LocalEntityId:x} "
+ $"parent={ParentCell(in record)?.ToString("x8") ?? "null"} "
+ $"r={radius:F1} outside={(outside ? 1 : 0)} cone={(cone ? 1 : 0)}");
}
if (!outside || !cone)
continue;
_survivors[count++] = record;
writer.AddDynamic(in record);
AddProjection(
writer,
in record,
input.AnimatedEntityIds);
}
writer.AddRouteRange(
RenderFrameCandidateRoute.LandscapeOutsideDynamic,
sliceIndex,
0,
_survivors.AsSpan(0, count));
}
}
private void BuildDynamicLastRoute(
RenderFrameWriter writer,
in RenderScenePViewBuildInput input)
{
_lookInCellScratch.Clear();
for (int frameIndex = 0;
frameIndex < input.LookInFrames.Count;
frameIndex++)
{
PortalVisibilityFrame frame = input.LookInFrames[frameIndex];
for (int i = 0; i < frame.OrderedVisibleCells.Count; i++)
_lookInCellScratch.Add(frame.OrderedVisibleCells[i]);
}
int count = 0;
EnsureCapacity(ref _survivors, _dynamicCount);
for (int i = 0; i < _dynamicCount; i++)
{
RenderProjectionRecord record = _dynamics[i];
uint? parentCellId = ParentCell(in record);
bool indoor = InteriorEntityPartition.IsIndoorCellId(
parentCellId);
if (!input.RootIsOutdoor && !indoor)
continue;
// Drawn-once (retail DrawMeshInternal @0x0059F360): a dynamic
// whose cell drew as a building look-in already rendered with
// that cell in the landscape stage; re-entering the final route
// would draw it after the boundary alpha drain and overpaint
// nearer flames (the Holtburg candle-behind-door class).
if (indoor && _lookInCellScratch.Contains(parentCellId!.Value))
{
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled)
{
string state = $"parent={parentCellId.Value:x8} lookin-excluded";
if (!_probeDynRouteStates.TryGetValue(record.Source.LocalEntityId, out string? prev)
|| prev != state)
{
_probeDynRouteStates[record.Source.LocalEntityId] = state;
Console.WriteLine(
$"[dyn-route] id={record.Source.LocalEntityId:x} {state}");
}
}
continue;
}
Sphere(in record, out Vector3 center, out float radius);
bool visible = indoor
? input.Viewcone.SphereVisibleInCell(
parentCellId!.Value,
in center,
radius)
: input.Viewcone.SphereVisibleOutside(
in center,
radius);
// ACDREAM_PROBE_WALK_ROOT companion (throwaway): the DynamicLast
// admission trace for the through-wall remote-player report —
// per entity, ON CHANGE: parent cell, look-in exclusion, the
// viewcone verdict, and the cell's lifted-plane shape ("0" = a
// pass-all zero-plane slice — the scissor-fallback trapdoor).
if (AcDream.Core.Rendering.RenderingDiagnostics.ProbeWalkRootEnabled
&& indoor)
{
string state =
$"parent={parentCellId!.Value:x8} vis={(visible ? 1 : 0)} "
+ $"cone={input.Viewcone.ProbeDescribeCell(parentCellId.Value)}";
if (!_probeDynRouteStates.TryGetValue(record.Source.LocalEntityId, out string? prev)
|| prev != state)
{
_probeDynRouteStates[record.Source.LocalEntityId] = state;
Console.WriteLine(
$"[dyn-route] id={record.Source.LocalEntityId:x} {state} route=DynamicLast");
}
}
if (!visible)
continue;
_survivors[count++] = record;
writer.AddDynamic(in record);
AddProjection(
writer,
in record,
input.AnimatedEntityIds);
}
writer.AddRouteRange(
RenderFrameCandidateRoute.DynamicLast,
0,
0,
_survivors.AsSpan(0, count));
}
private int LoadCell(
RenderSceneQuery scene,
uint cellId,
bool includeDynamics,
bool includeStatics = true)
{
int staticCount = includeStatics
? scene.GetCellStaticCount(cellId)
: 0;
int dynamicCount = includeDynamics
? scene.GetCellDynamicCount(cellId)
: 0;
EnsureCapacity(
ref _cell,
checked(staticCount + dynamicCount));
int count = 0;
if (staticCount > 0)
{
count = scene.CopyCellStaticsTo(
cellId,
_cell.AsSpan(0, staticCount));
}
if (dynamicCount > 0)
{
count += scene.CopyCellDynamicsTo(
cellId,
_cell.AsSpan(count, dynamicCount));
}
return CompactAndSort(_cell, count);
}
private void AddProjection(
RenderFrameWriter writer,
in RenderProjectionRecord record,
HashSet<uint>? animatedEntityIds)
{
if (!_projectionIds.Add(record.Id))
return;
writer.AddTransform(new RenderFrameTransformRecord(
record.Id,
-1,
record.Transform.LocalToWorld));
writer.AddEntityCandidate(
in record,
animatedEntityIds?.Contains(record.Source.LocalEntityId) == true);
}
private static int CompactAndSort(
RenderProjectionRecord[] records,
int count)
{
int write = 0;
for (int read = 0; read < count; read++)
{
RenderProjectionRecord record = records[read];
if (record.Id.Domain == EnvCellProjectionDomain
|| record.MeshSet.MeshCount <= 0
|| (record.Flags
& RenderProjectionFlags.SpatiallyResident) == 0)
{
continue;
}
records[write++] = record;
}
Array.Sort(
records,
0,
write,
RenderProjectionRecordSortComparer.Instance);
return write;
}
private static uint? ParentCell(in RenderProjectionRecord record) =>
record.Source.ParentCellId == 0
? null
: record.Source.ParentCellId;
private static void Sphere(
in RenderProjectionRecord record,
out Vector3 center,
out float radius)
{
center = (record.Bounds.Minimum + record.Bounds.Maximum) * 0.5f;
radius =
(record.Bounds.Maximum - record.Bounds.Minimum).Length() * 0.5f;
}
private static void EnsureCapacity<T>(ref T[] values, int required)
{
if (values.Length >= required)
return;
int capacity = values.Length == 0 ? 4 : values.Length;
while (capacity < required)
capacity = checked(capacity * 2);
Array.Resize(ref values, capacity);
}
private sealed class RenderProjectionRecordSortComparer :
IComparer<RenderProjectionRecord>
{
public static RenderProjectionRecordSortComparer Instance { get; } =
new();
private RenderProjectionRecordSortComparer()
{
}
public int Compare(
RenderProjectionRecord left,
RenderProjectionRecord right)
{
int value = left.SortKey.Value.CompareTo(right.SortKey.Value);
return value != 0
? value
: left.Id.CompareTo(right.Id);
}
}
}