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:
Erik 2026-07-25 00:12:53 +02:00
parent 8c638654be
commit e346f8bbaf
13 changed files with 1187 additions and 9 deletions

View file

@ -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,

View file

@ -41,6 +41,7 @@ internal sealed record WorldLifecycleResourceSnapshot(
int MaterializedLiveEntities,
CurrentRenderSceneOracleSnapshot RenderSceneOracle,
RenderSceneShadowComparisonSnapshot RenderSceneShadow,
RenderFrameProductComparisonSnapshot RenderFrameProduct,
int PendingLiveTeardowns,
int PendingLandblockRetirements,
int ParticleEmitters,

View file

@ -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,

View file

@ -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

View file

@ -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);

View file

@ -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

View file

@ -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]

View 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);
}
}
}

View file

@ -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,