feat(rendering): compare scene-built PView candidates
Build a same-frame candidate product from incremental render-scene indices and compare its exact PView routes against the accepted current path without changing the production draw source.
This commit is contained in:
parent
8c638654be
commit
e346f8bbaf
13 changed files with 1187 additions and 9 deletions
|
|
@ -388,6 +388,14 @@ internal sealed class FrameRootCompositionPhase
|
|||
currentRenderSceneOracle,
|
||||
message => d.Log("[UI-PROBE] " + message))
|
||||
: null;
|
||||
RenderScenePViewFrameProductController? renderFrameProduct =
|
||||
currentRenderSceneOracle is not null
|
||||
&& live.RenderSceneShadow is not null
|
||||
? new RenderScenePViewFrameProductController(
|
||||
live.RenderSceneShadow,
|
||||
currentRenderSceneOracle,
|
||||
message => d.Log("[UI-PROBE] " + message))
|
||||
: null;
|
||||
live.DrawDispatcher.SetCurrentRenderSceneObserver(
|
||||
currentRenderSceneOracle);
|
||||
live.SelectionScene.SetCurrentRenderSceneObserver(
|
||||
|
|
@ -402,7 +410,9 @@ internal sealed class FrameRootCompositionPhase
|
|||
d.RetailAlphaQueue,
|
||||
d.ParticleVisibility,
|
||||
new WorldScenePViewRenderer(
|
||||
new RetailPViewRenderer(currentRenderSceneOracle),
|
||||
new RetailPViewRenderer(
|
||||
currentRenderSceneOracle,
|
||||
renderFrameProduct),
|
||||
retailPViewPassExecutor,
|
||||
retailPViewPassExecutor),
|
||||
retailPViewCells,
|
||||
|
|
@ -436,7 +446,8 @@ internal sealed class FrameRootCompositionPhase
|
|||
content.Dats,
|
||||
foundation.Residency,
|
||||
currentRenderSceneOracle,
|
||||
renderSceneShadowComparison);
|
||||
renderSceneShadowComparison,
|
||||
renderFrameProduct);
|
||||
lifecycleAutomation =
|
||||
new WorldLifecycleAutomationController(
|
||||
() => session.WorldReveal.Snapshot,
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ internal sealed record WorldLifecycleResourceSnapshot(
|
|||
int MaterializedLiveEntities,
|
||||
CurrentRenderSceneOracleSnapshot RenderSceneOracle,
|
||||
RenderSceneShadowComparisonSnapshot RenderSceneShadow,
|
||||
RenderFrameProductComparisonSnapshot RenderFrameProduct,
|
||||
int PendingLiveTeardowns,
|
||||
int PendingLandblockRetirements,
|
||||
int ParticleEmitters,
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
_renderSceneOracle;
|
||||
private readonly IRenderSceneShadowSnapshotSource?
|
||||
_renderSceneShadow;
|
||||
private readonly IRenderFrameProductSnapshotSource?
|
||||
_renderFrameProduct;
|
||||
|
||||
public WorldLifecycleResourceSnapshotSource(
|
||||
GpuWorldState world,
|
||||
|
|
@ -62,7 +64,8 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
IDatReaderWriter dats,
|
||||
ResidencyManager residency,
|
||||
ICurrentRenderSceneOracleSnapshotSource? renderSceneOracle = null,
|
||||
IRenderSceneShadowSnapshotSource? renderSceneShadow = null)
|
||||
IRenderSceneShadowSnapshotSource? renderSceneShadow = null,
|
||||
IRenderFrameProductSnapshotSource? renderFrameProduct = null)
|
||||
{
|
||||
_world = world ?? throw new ArgumentNullException(nameof(world));
|
||||
_animations = animations
|
||||
|
|
@ -91,6 +94,7 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
?? throw new ArgumentNullException(nameof(residency));
|
||||
_renderSceneOracle = renderSceneOracle;
|
||||
_renderSceneShadow = renderSceneShadow;
|
||||
_renderFrameProduct = renderFrameProduct;
|
||||
}
|
||||
|
||||
public WorldLifecycleResourceSnapshot Capture(RenderFrameOutcome outcome)
|
||||
|
|
@ -136,6 +140,9 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
|||
RenderSceneShadow:
|
||||
_renderSceneShadow?.CaptureCheckpointSnapshot()
|
||||
?? RenderSceneShadowComparisonSnapshot.Disabled,
|
||||
RenderFrameProduct:
|
||||
_renderFrameProduct?.Snapshot
|
||||
?? RenderFrameProductComparisonSnapshot.Disabled,
|
||||
PendingLiveTeardowns: _liveEntities.PendingTeardownCount,
|
||||
PendingLandblockRetirements: _streaming.PendingRetirementCount,
|
||||
ParticleEmitters: _particles.ActiveEmitterCount,
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ public sealed class RetailPViewRenderer
|
|||
{
|
||||
private readonly InteriorEntityPartition.IObserver? _partitionObserver;
|
||||
private readonly ICurrentRenderPViewObserver? _candidateObserver;
|
||||
private readonly RenderScenePViewFrameProductController?
|
||||
_sceneFrameProduct;
|
||||
private readonly PortalVisibilityFrame _mainPortalFrameScratch = new();
|
||||
private readonly ClipFrameAssembly _clipAssemblyScratch = new();
|
||||
private readonly ViewconeCuller _viewconeScratch = new();
|
||||
|
|
@ -71,10 +73,19 @@ public sealed class RetailPViewRenderer
|
|||
}
|
||||
|
||||
internal RetailPViewRenderer(
|
||||
InteriorEntityPartition.IObserver? partitionObserver)
|
||||
InteriorEntityPartition.IObserver? partitionObserver,
|
||||
RenderScenePViewFrameProductController? sceneFrameProduct = null)
|
||||
{
|
||||
if (sceneFrameProduct is not null
|
||||
&& partitionObserver is not ICurrentRenderPViewObserver)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"The scene frame-product referee requires the current PView observer.",
|
||||
nameof(partitionObserver));
|
||||
}
|
||||
_partitionObserver = partitionObserver;
|
||||
_candidateObserver = partitionObserver as ICurrentRenderPViewObserver;
|
||||
_sceneFrameProduct = sceneFrameProduct;
|
||||
}
|
||||
|
||||
// T2 (BR-4): retail has NO distance constant on the flood-admission chain
|
||||
|
|
@ -239,6 +250,14 @@ public sealed class RetailPViewRenderer
|
|||
DrawDynamicsLast(ctx, passes, partition, viewcone, ctx.RootCell.IsOutdoorNode);
|
||||
|
||||
_candidateObserver?.CompletePViewFrame();
|
||||
_sceneFrameProduct?.BuildAndCompare(
|
||||
pvFrame,
|
||||
clipAssembly,
|
||||
viewcone,
|
||||
_lookInFrames,
|
||||
drawableCells,
|
||||
ctx.Cells,
|
||||
ctx.RootCell.IsOutdoorNode);
|
||||
return result;
|
||||
}
|
||||
catch
|
||||
|
|
|
|||
|
|
@ -9,6 +9,22 @@ internal enum RenderFrameBlendClass : byte
|
|||
Alpha,
|
||||
}
|
||||
|
||||
internal enum RenderFrameCandidateRoute : byte
|
||||
{
|
||||
LandscapeOutdoorStatic,
|
||||
LandscapeOutsideDynamic,
|
||||
LookInObject,
|
||||
CellStatic,
|
||||
DynamicLast,
|
||||
}
|
||||
|
||||
internal readonly record struct RenderFrameCandidateRange(
|
||||
RenderFrameCandidateRoute Route,
|
||||
int RouteIndex,
|
||||
uint CellId,
|
||||
int Offset,
|
||||
int Count);
|
||||
|
||||
internal readonly record struct RenderFrameCellRange(
|
||||
uint CellId,
|
||||
int PViewOrder,
|
||||
|
|
@ -60,7 +76,8 @@ internal readonly record struct RenderFrameDiagnosticCounts(
|
|||
int OpaqueClassificationCount,
|
||||
int AlphaClassificationCount,
|
||||
int LightSetCount,
|
||||
int SelectionPartCount);
|
||||
int SelectionPartCount,
|
||||
int RouteCandidateCount);
|
||||
|
||||
/// <summary>
|
||||
/// A validated borrowed view over one reusable frame arena. Copies are safe:
|
||||
|
|
@ -125,6 +142,12 @@ internal readonly struct RenderFrameView
|
|||
public ReadOnlySpan<RenderFrameSelectionPart> SelectionParts =>
|
||||
Arena.SelectionParts;
|
||||
|
||||
public ReadOnlySpan<RenderProjectionRecord> RouteCandidates =>
|
||||
Arena.RouteCandidates;
|
||||
|
||||
public ReadOnlySpan<RenderFrameCandidateRange> RouteRanges =>
|
||||
Arena.RouteRanges;
|
||||
|
||||
public PortalVisibilityFrame? PortalFrame => Arena.PortalFrame;
|
||||
|
||||
public ClipFrameAssembly? ClipAssembly => Arena.ClipAssembly;
|
||||
|
|
@ -172,6 +195,8 @@ internal sealed class RenderFrameArena
|
|||
private RenderFrameClassificationRecord[] _classifications = [];
|
||||
private RenderFrameObjectLightSet[] _lightSets = [];
|
||||
private RenderFrameSelectionPart[] _selectionParts = [];
|
||||
private RenderProjectionRecord[] _routeCandidates = [];
|
||||
private RenderFrameCandidateRange[] _routeRanges = [];
|
||||
private int _outdoorCount;
|
||||
private int _cellStaticCount;
|
||||
private int _cellRangeCount;
|
||||
|
|
@ -180,6 +205,8 @@ internal sealed class RenderFrameArena
|
|||
private int _classificationCount;
|
||||
private int _lightSetCount;
|
||||
private int _selectionPartCount;
|
||||
private int _routeCandidateCount;
|
||||
private int _routeRangeCount;
|
||||
private int _opaqueClassificationCount;
|
||||
private int _alphaClassificationCount;
|
||||
private ulong _epoch;
|
||||
|
|
@ -206,7 +233,8 @@ internal sealed class RenderFrameArena
|
|||
_opaqueClassificationCount,
|
||||
_alphaClassificationCount,
|
||||
_lightSetCount,
|
||||
_selectionPartCount);
|
||||
_selectionPartCount,
|
||||
_routeCandidateCount);
|
||||
|
||||
internal ReadOnlySpan<RenderProjectionRecord> OutdoorStaticCandidates =>
|
||||
_outdoor.AsSpan(0, _outdoorCount);
|
||||
|
|
@ -232,6 +260,12 @@ internal sealed class RenderFrameArena
|
|||
internal ReadOnlySpan<RenderFrameSelectionPart> SelectionParts =>
|
||||
_selectionParts.AsSpan(0, _selectionPartCount);
|
||||
|
||||
internal ReadOnlySpan<RenderProjectionRecord> RouteCandidates =>
|
||||
_routeCandidates.AsSpan(0, _routeCandidateCount);
|
||||
|
||||
internal ReadOnlySpan<RenderFrameCandidateRange> RouteRanges =>
|
||||
_routeRanges.AsSpan(0, _routeRangeCount);
|
||||
|
||||
internal ulong BeginWrite(
|
||||
RenderSceneGeneration generation,
|
||||
ulong frameSequence)
|
||||
|
|
@ -253,6 +287,8 @@ internal sealed class RenderFrameArena
|
|||
_classificationCount = 0;
|
||||
_lightSetCount = 0;
|
||||
_selectionPartCount = 0;
|
||||
_routeCandidateCount = 0;
|
||||
_routeRangeCount = 0;
|
||||
_opaqueClassificationCount = 0;
|
||||
_alphaClassificationCount = 0;
|
||||
PortalFrame = null;
|
||||
|
|
@ -393,6 +429,34 @@ internal sealed class RenderFrameArena
|
|||
_selectionParts[_selectionPartCount++] = part;
|
||||
}
|
||||
|
||||
internal void AddRouteRange(
|
||||
ulong epoch,
|
||||
RenderFrameCandidateRoute route,
|
||||
int routeIndex,
|
||||
uint cellId,
|
||||
ReadOnlySpan<RenderProjectionRecord> records)
|
||||
{
|
||||
EnsureBuilding(epoch);
|
||||
if (routeIndex < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(routeIndex));
|
||||
if (records.Length == 0)
|
||||
return;
|
||||
|
||||
int offset = _routeCandidateCount;
|
||||
EnsureCapacity(
|
||||
ref _routeCandidates,
|
||||
checked(offset + records.Length));
|
||||
records.CopyTo(_routeCandidates.AsSpan(offset));
|
||||
_routeCandidateCount += records.Length;
|
||||
EnsureCapacity(ref _routeRanges, _routeRangeCount + 1);
|
||||
_routeRanges[_routeRangeCount++] = new RenderFrameCandidateRange(
|
||||
route,
|
||||
routeIndex,
|
||||
cellId,
|
||||
offset,
|
||||
records.Length);
|
||||
}
|
||||
|
||||
internal void Publish(ulong epoch)
|
||||
{
|
||||
EnsureBuilding(epoch);
|
||||
|
|
@ -549,6 +613,18 @@ internal readonly struct RenderFrameWriter
|
|||
public void AddSelectionPart(in RenderFrameSelectionPart part) =>
|
||||
Arena.AddSelectionPart(_epoch, in part);
|
||||
|
||||
public void AddRouteRange(
|
||||
RenderFrameCandidateRoute route,
|
||||
int routeIndex,
|
||||
uint cellId,
|
||||
ReadOnlySpan<RenderProjectionRecord> records) =>
|
||||
Arena.AddRouteRange(
|
||||
_epoch,
|
||||
route,
|
||||
routeIndex,
|
||||
cellId,
|
||||
records);
|
||||
|
||||
public void Publish() =>
|
||||
Owner.Publish(_arenaIndex, _epoch);
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ internal static class RenderProjectionRecordFactory
|
|||
ownerLandblockId,
|
||||
entity);
|
||||
RenderProjectionFlags flags = RenderProjectionFlags.Selectable;
|
||||
if (spatiallyVisible)
|
||||
flags |= RenderProjectionFlags.SpatiallyResident;
|
||||
if (entity.MeshRefs.Count > 0
|
||||
&& spatiallyVisible
|
||||
&& entity.IsDrawVisible
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ internal readonly record struct RenderProjectionId
|
|||
internal ulong RawValue => _value;
|
||||
internal byte Domain => (byte)(_value >> 56);
|
||||
internal RenderSortKey ToSortKey() => new(_value);
|
||||
internal void AddTo(ref StableRenderHash128 hash) => hash.Add(_value);
|
||||
|
||||
public int CompareTo(RenderProjectionId other) =>
|
||||
_value.CompareTo(other._value);
|
||||
|
|
@ -104,6 +105,13 @@ internal enum RenderProjectionFlags : uint
|
|||
Selectable = 1 << 4,
|
||||
LightCandidate = 1 << 5,
|
||||
PortalStraddling = 1 << 6,
|
||||
/// <summary>
|
||||
/// The projection currently participates in the accepted spatial
|
||||
/// presentation workset. Hidden objects retain this bit; a
|
||||
/// portal-withdrawn attachment does not. This is derived render
|
||||
/// ownership, not gameplay PhysicsState.
|
||||
/// </summary>
|
||||
SpatiallyResident = 1 << 7,
|
||||
}
|
||||
|
||||
[Flags]
|
||||
|
|
|
|||
685
src/AcDream.App/Rendering/Scene/RenderScenePViewFrameProduct.cs
Normal file
685
src/AcDream.App/Rendering/Scene/RenderScenePViewFrameProduct.cs
Normal file
|
|
@ -0,0 +1,685 @@
|
|||
using System.Numerics;
|
||||
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
ProductCounts: default);
|
||||
}
|
||||
|
||||
internal interface IRenderFrameProductSnapshotSource
|
||||
{
|
||||
RenderFrameProductComparisonSnapshot Snapshot { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// G1 compare-only owner. It builds the candidate frame product directly from
|
||||
/// the incremental scene after the accepted PView traversal, borrows it in the
|
||||
/// same render frame, and compares route membership against the current-path
|
||||
/// observer. It never supplies a production draw input.
|
||||
/// </summary>
|
||||
internal sealed class RenderScenePViewFrameProductController :
|
||||
IRenderFrameProductSnapshotSource
|
||||
{
|
||||
private readonly RenderSceneShadowRuntime _shadow;
|
||||
private readonly CurrentRenderSceneOracle _current;
|
||||
private readonly RenderScenePViewFrameBuilder _builder = new();
|
||||
private readonly RenderFrameExchange _exchange = new();
|
||||
private readonly List<CandidateKey> _expected = [];
|
||||
private readonly List<CandidateKey> _actual = [];
|
||||
private readonly Action<string>? _log;
|
||||
private ulong _productFrameSequence;
|
||||
private ulong _comparisonCount;
|
||||
private ulong _successfulComparisonCount;
|
||||
private long _mismatchCount;
|
||||
private string? _firstMismatch;
|
||||
|
||||
public RenderScenePViewFrameProductController(
|
||||
RenderSceneShadowRuntime shadow,
|
||||
CurrentRenderSceneOracle current,
|
||||
Action<string>? log = null)
|
||||
{
|
||||
_shadow = shadow ?? throw new ArgumentNullException(nameof(shadow));
|
||||
_current = current ?? throw new ArgumentNullException(nameof(current));
|
||||
_log = log;
|
||||
Snapshot = RenderFrameProductComparisonSnapshot.Disabled with
|
||||
{
|
||||
Enabled = true,
|
||||
};
|
||||
}
|
||||
|
||||
public RenderFrameProductComparisonSnapshot Snapshot { get; private set; }
|
||||
|
||||
public void BuildAndCompare(
|
||||
PortalVisibilityFrame portalFrame,
|
||||
ClipFrameAssembly clipAssembly,
|
||||
ViewconeCuller viewcone,
|
||||
IReadOnlyList<PortalVisibilityFrame> lookInFrames,
|
||||
HashSet<uint> drawableCells,
|
||||
IRetailPViewCellSource cells,
|
||||
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;
|
||||
var input = new RenderScenePViewBuildInput(
|
||||
scene,
|
||||
_shadow.BuildDigest(),
|
||||
portalFrame,
|
||||
clipAssembly,
|
||||
viewcone,
|
||||
lookInFrames,
|
||||
drawableCells,
|
||||
cells,
|
||||
rootIsOutdoor);
|
||||
_builder.Build(_exchange, frameSequence, in input);
|
||||
RenderFrameView view = _exchange.BorrowLatest(
|
||||
scene.Generation,
|
||||
frameSequence);
|
||||
try
|
||||
{
|
||||
Compare(in view);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_exchange.Release(in view);
|
||||
}
|
||||
}
|
||||
|
||||
private void Compare(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 i = 0; i < expected.Count; i++)
|
||||
{
|
||||
CurrentRenderPViewCandidateFingerprint candidate = expected[i];
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
_expected.Sort(CandidateKeyComparer.Instance);
|
||||
_actual.Sort(CandidateKeyComparer.Instance);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
ProductCounts: view.DiagnosticCounts);
|
||||
}
|
||||
|
||||
private string? CompareKeys()
|
||||
{
|
||||
if (_expected.Count != _actual.Count)
|
||||
{
|
||||
return $"field=count expected={_expected.Count} "
|
||||
+ $"actual={_actual.Count}";
|
||||
}
|
||||
|
||||
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 sealed class CandidateKeyComparer : IComparer<CandidateKey>
|
||||
{
|
||||
public static CandidateKeyComparer Instance { get; } = new();
|
||||
|
||||
private CandidateKeyComparer()
|
||||
{
|
||||
}
|
||||
|
||||
public int Compare(CandidateKey left, CandidateKey right)
|
||||
{
|
||||
int value = left.Route.CompareTo(right.Route);
|
||||
if (value != 0) return value;
|
||||
value = left.RouteIndex.CompareTo(right.RouteIndex);
|
||||
if (value != 0) return value;
|
||||
value = left.CellId.CompareTo(right.CellId);
|
||||
if (value != 0) return value;
|
||||
return left.ProjectionId.CompareTo(right.ProjectionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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> _transformIds = [];
|
||||
private RenderProjectionRecord[] _outdoor = [];
|
||||
private RenderProjectionRecord[] _dynamics = [];
|
||||
private RenderProjectionRecord[] _cell = [];
|
||||
private RenderProjectionRecord[] _survivors = [];
|
||||
private RenderProjectionRecord[] _cellRoute = [];
|
||||
private int _outdoorCount;
|
||||
private int _dynamicCount;
|
||||
private int _cellRouteCount;
|
||||
|
||||
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);
|
||||
_transformIds.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();
|
||||
}
|
||||
catch
|
||||
{
|
||||
writer.Abort();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadSceneIndices(RenderSceneQuery scene)
|
||||
{
|
||||
RenderSceneIndexCounts counts = scene.IndexCounts;
|
||||
EnsureCapacity(ref _outdoor, counts.OutdoorStatic);
|
||||
_outdoorCount = scene.CopyIndexTo(
|
||||
RenderSceneIndex.OutdoorStatic,
|
||||
_outdoor);
|
||||
_outdoorCount = CompactAndSort(
|
||||
_outdoor,
|
||||
_outdoorCount);
|
||||
|
||||
EnsureCapacity(ref _dynamics, counts.Dynamic);
|
||||
_dynamicCount = scene.CopyIndexTo(
|
||||
RenderSceneIndex.Dynamic,
|
||||
_dynamics);
|
||||
_dynamicCount = CompactAndSort(
|
||||
_dynamics,
|
||||
_dynamicCount);
|
||||
}
|
||||
|
||||
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);
|
||||
AddTransform(writer, in record);
|
||||
}
|
||||
|
||||
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++)
|
||||
AddTransform(writer, in _cell[index]);
|
||||
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);
|
||||
AddTransform(writer, in record);
|
||||
}
|
||||
|
||||
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;
|
||||
AddTransform(writer, in record);
|
||||
}
|
||||
|
||||
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);
|
||||
AddTransform(writer, in record);
|
||||
}
|
||||
|
||||
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 AddTransform(
|
||||
RenderFrameWriter writer,
|
||||
in RenderProjectionRecord record)
|
||||
{
|
||||
if (!_transformIds.Add(record.Id))
|
||||
return;
|
||||
writer.AddTransform(new RenderFrameTransformRecord(
|
||||
record.Id,
|
||||
-1,
|
||||
record.Transform.LocalToWorld));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -336,7 +336,8 @@ internal sealed class StaticRenderProjectionJournal :
|
|||
new RenderWorldBounds(
|
||||
shell.WorldBounds.Min,
|
||||
shell.WorldBounds.Max),
|
||||
RenderProjectionFlags.Draw,
|
||||
RenderProjectionFlags.Draw
|
||||
| RenderProjectionFlags.SpatiallyResident,
|
||||
default,
|
||||
id.ToSortKey(),
|
||||
RenderDirtyMask.All,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue