acdream/src/AcDream.App/Rendering/Scene/RenderScenePViewFrameProduct.cs
2026-07-25 08:36:11 +02:00

1596 lines
57 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;
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.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[] _outdoor = [];
private RenderProjectionRecord[] _dynamics = [];
private RenderProjectionRecord[] _cell = [];
private RenderProjectionRecord[] _dirty = [];
private RenderProjectionRecord[] _survivors = [];
private RenderProjectionRecord[] _cellRoute = [];
private readonly Dictionary<RenderProjectionId, int>
_outdoorPositions = [];
private readonly Dictionary<RenderProjectionId, int>
_dynamicPositions = [];
private int _outdoorCount;
private int _dynamicCount;
private int _cellRouteCount;
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);
BuildOutdoorRoutes(writer, in input);
BuildLookInRoutes(writer, in input);
BuildOutsideDynamicRoutes(writer, in input);
BuildCellStaticRoute(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 _outdoor, counts.OutdoorStatic);
_outdoorCount = scene.CopyIndexTo(
RenderSceneIndex.OutdoorStatic,
_outdoor);
_outdoorCount = CompactAndSort(
_outdoor,
_outdoorCount);
BuildPositionIndex(
_outdoor,
_outdoorCount,
_outdoorPositions);
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 (_outdoorPositions.TryGetValue(
record.Id,
out int outdoorPosition))
{
_outdoor[outdoorPosition] = record;
}
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 (_outdoorPositions.TryGetValue(
id,
out int outdoorPosition))
{
_outdoor[outdoorPosition] =
_outdoor[outdoorPosition] with
{
DirtyMask = RenderDirtyMask.None,
};
}
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);
}
private void BuildOutdoorRoutes(
RenderFrameWriter writer,
in RenderScenePViewBuildInput input)
{
int sliceCount = input.ClipAssembly.OutsideViewSlices.Length;
for (int sliceIndex = 0; sliceIndex < sliceCount; sliceIndex++)
{
int count = 0;
EnsureCapacity(ref _survivors, _outdoorCount);
for (int i = 0; i < _outdoorCount; i++)
{
RenderProjectionRecord record = _outdoor[i];
Sphere(in record, out Vector3 center, out float radius);
if (!input.Viewcone.SphereVisibleInOutsideSlice(
sliceIndex,
in center,
radius))
{
continue;
}
_survivors[count++] = record;
writer.AddOutdoor(in record);
AddProjection(
writer,
in record,
input.AnimatedEntityIds);
}
writer.AddRouteRange(
RenderFrameCandidateRoute.LandscapeOutdoorStatic,
sliceIndex,
0,
_survivors.AsSpan(0, count));
}
}
private void BuildLookInRoutes(
RenderFrameWriter writer,
in RenderScenePViewBuildInput input)
{
for (int frameIndex = 0;
frameIndex < input.LookInFrames.Count;
frameIndex++)
{
PortalVisibilityFrame frame = input.LookInFrames[frameIndex];
for (int i = frame.OrderedVisibleCells.Count - 1; i >= 0; i--)
{
uint cellId = frame.OrderedVisibleCells[i];
int count = LoadCell(
input.Scene,
cellId,
includeDynamics: true);
if (count == 0)
continue;
for (int index = 0; index < count; index++)
AddProjection(
writer,
in _cell[index],
input.AnimatedEntityIds);
writer.AddRouteRange(
RenderFrameCandidateRoute.LookInObject,
i,
cellId,
_cell.AsSpan(0, count));
}
}
}
private void BuildOutsideDynamicRoutes(
RenderFrameWriter writer,
in RenderScenePViewBuildInput input)
{
if (input.RootIsOutdoor)
return;
int sliceCount = input.ClipAssembly.OutsideViewSlices.Length;
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);
if (!RetailPViewRenderer.DynamicDrawsInOutsideStage(
ParentCell(in record),
center,
radius,
input.DrawableCells,
input.Cells)
|| !input.Viewcone.SphereVisibleInOutsideSlice(
sliceIndex,
in center,
radius))
{
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 BuildCellStaticRoute(
RenderFrameWriter writer,
in RenderScenePViewBuildInput input)
{
_cellRouteCount = 0;
IReadOnlyList<uint> ordered = input.PortalFrame.OrderedVisibleCells;
for (int i = ordered.Count - 1; i >= 0; i--)
{
uint cellId = ordered[i];
if (!input.DrawableCells.Contains(cellId))
continue;
int loaded = LoadCell(
input.Scene,
cellId,
includeDynamics: false);
int count = 0;
for (int index = 0; index < loaded; index++)
{
RenderProjectionRecord record = _cell[index];
Sphere(in record, out Vector3 center, out float radius);
if (!input.Viewcone.SphereVisibleInCell(
cellId,
in center,
radius))
{
continue;
}
_cell[count++] = record;
AddProjection(
writer,
in record,
input.AnimatedEntityIds);
}
writer.AddCellRange(
cellId,
i,
_cell.AsSpan(0, count));
EnsureCapacity(
ref _cellRoute,
checked(_cellRouteCount + count));
_cell.AsSpan(0, count).CopyTo(
_cellRoute.AsSpan(_cellRouteCount));
_cellRouteCount += count;
}
writer.AddRouteRange(
RenderFrameCandidateRoute.CellStatic,
0,
0,
_cellRoute.AsSpan(0, _cellRouteCount));
}
private void BuildDynamicLastRoute(
RenderFrameWriter writer,
in RenderScenePViewBuildInput input)
{
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;
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);
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)
{
int staticCount = scene.GetCellStaticCount(cellId);
int dynamicCount = includeDynamics
? scene.GetCellDynamicCount(cellId)
: 0;
EnsureCapacity(
ref _cell,
checked(staticCount + dynamicCount));
int 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);
}
}
}