1596 lines
57 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|