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

@ -24,7 +24,7 @@ retail-faithful gameplay
| F4 — dynamic indices | complete | The contained scene now maintains outdoor/static, per-cell static/dynamic, special dynamic-route, translucent, selectable, light-candidate, and dirty indices incrementally. Final-frame live/equipped synchronization remains active-only, and active animated statics are synchronized from the scheduler's active workset rather than a resident-world scan. The production scene remains unconstructed and non-drawing. | | F4 — dynamic indices | complete | The contained scene now maintains outdoor/static, per-cell static/dynamic, special dynamic-route, translucent, selectable, light-candidate, and dirty indices incrementally. Final-frame live/equipped synchronization remains active-only, and active animated statics are synchronized from the scheduler's active workset rather than a resident-world scan. The production scene remains unconstructed and non-drawing. |
| F5 | complete | Exact `81e2f1a5` passed capped and uncapped nine-stop routes plus uncapped dense Arwic. Across the final checkpoints the referee completed 1,663 + 1,677 + 81 comparisons with zero mismatches, zero pending deltas, zero rejected operations, exact binary/source identity, and graceful shutdown. Evidence: `docs/research/2026-07-24-slice-f5-render-scene-shadow-gate.md`. | | F5 | complete | Exact `81e2f1a5` passed capped and uncapped nine-stop routes plus uncapped dense Arwic. Across the final checkpoints the referee completed 1,663 + 1,677 + 81 comparisons with zero mismatches, zero pending deltas, zero rejected operations, exact binary/source identity, and graceful shutdown. Evidence: `docs/research/2026-07-24-slice-f5-render-scene-shadow-gate.md`. |
| G0 — borrowed frame product | complete | Two reusable arenas publish exact generation/frame-stamped borrowed views for scene candidates, cell ranges, transforms, classification, light sets, selection, PView/clip references, counts, and source digest. Release/reuse invalidates every copied view; abort never publishes; frame N may build while N-1 is borrowed; the warm path allocates zero bytes. Eight focused tests pass inside the 3,743-App / 8,227-solution Release gate. | | G0 — borrowed frame product | complete | Two reusable arenas publish exact generation/frame-stamped borrowed views for scene candidates, cell ranges, transforms, classification, light sets, selection, PView/clip references, counts, and source digest. Release/reuse invalidates every copied view; abort never publishes; frame N may build while N-1 is borrowed; the warm path allocates zero bytes. Eight focused tests pass inside the 3,743-App / 8,227-solution Release gate. |
| G1 — scene-query replacement | active | Build the candidate product from the incremental scene and compare it with the current PView partition without changing the draw source. | | G1 — scene-query replacement | code gate complete; connected gate pending | The incremental scene now builds the same-frame candidate product from outdoor, per-cell, look-in, outside-stage, and dynamic indices, preserving PView route multiplicity and hidden-versus-withdrawn ownership. A compare-only controller checks canonical route/cell/entity membership against the accepted current PView path; lifecycle JSON publishes counts, digests, and the first mismatch. Twenty-four focused render-scene/frame-product tests pass inside the 3,745-App / 8,229-solution Release gate. No production draw consumer has switched. |
| G2G5 | pending | No production consumer has switched. | | G2G5 | pending | No production consumer has switched. |
The exact pre-F/G runtime rollback anchor remains `e7d9d6fa`. F0 is The exact pre-F/G runtime rollback anchor remains `e7d9d6fa`. F0 is

View file

@ -388,6 +388,14 @@ internal sealed class FrameRootCompositionPhase
currentRenderSceneOracle, currentRenderSceneOracle,
message => d.Log("[UI-PROBE] " + message)) message => d.Log("[UI-PROBE] " + message))
: null; : 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( live.DrawDispatcher.SetCurrentRenderSceneObserver(
currentRenderSceneOracle); currentRenderSceneOracle);
live.SelectionScene.SetCurrentRenderSceneObserver( live.SelectionScene.SetCurrentRenderSceneObserver(
@ -402,7 +410,9 @@ internal sealed class FrameRootCompositionPhase
d.RetailAlphaQueue, d.RetailAlphaQueue,
d.ParticleVisibility, d.ParticleVisibility,
new WorldScenePViewRenderer( new WorldScenePViewRenderer(
new RetailPViewRenderer(currentRenderSceneOracle), new RetailPViewRenderer(
currentRenderSceneOracle,
renderFrameProduct),
retailPViewPassExecutor, retailPViewPassExecutor,
retailPViewPassExecutor), retailPViewPassExecutor),
retailPViewCells, retailPViewCells,
@ -436,7 +446,8 @@ internal sealed class FrameRootCompositionPhase
content.Dats, content.Dats,
foundation.Residency, foundation.Residency,
currentRenderSceneOracle, currentRenderSceneOracle,
renderSceneShadowComparison); renderSceneShadowComparison,
renderFrameProduct);
lifecycleAutomation = lifecycleAutomation =
new WorldLifecycleAutomationController( new WorldLifecycleAutomationController(
() => session.WorldReveal.Snapshot, () => session.WorldReveal.Snapshot,

View file

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

View file

@ -43,6 +43,8 @@ internal sealed class WorldLifecycleResourceSnapshotSource
_renderSceneOracle; _renderSceneOracle;
private readonly IRenderSceneShadowSnapshotSource? private readonly IRenderSceneShadowSnapshotSource?
_renderSceneShadow; _renderSceneShadow;
private readonly IRenderFrameProductSnapshotSource?
_renderFrameProduct;
public WorldLifecycleResourceSnapshotSource( public WorldLifecycleResourceSnapshotSource(
GpuWorldState world, GpuWorldState world,
@ -62,7 +64,8 @@ internal sealed class WorldLifecycleResourceSnapshotSource
IDatReaderWriter dats, IDatReaderWriter dats,
ResidencyManager residency, ResidencyManager residency,
ICurrentRenderSceneOracleSnapshotSource? renderSceneOracle = null, ICurrentRenderSceneOracleSnapshotSource? renderSceneOracle = null,
IRenderSceneShadowSnapshotSource? renderSceneShadow = null) IRenderSceneShadowSnapshotSource? renderSceneShadow = null,
IRenderFrameProductSnapshotSource? renderFrameProduct = null)
{ {
_world = world ?? throw new ArgumentNullException(nameof(world)); _world = world ?? throw new ArgumentNullException(nameof(world));
_animations = animations _animations = animations
@ -91,6 +94,7 @@ internal sealed class WorldLifecycleResourceSnapshotSource
?? throw new ArgumentNullException(nameof(residency)); ?? throw new ArgumentNullException(nameof(residency));
_renderSceneOracle = renderSceneOracle; _renderSceneOracle = renderSceneOracle;
_renderSceneShadow = renderSceneShadow; _renderSceneShadow = renderSceneShadow;
_renderFrameProduct = renderFrameProduct;
} }
public WorldLifecycleResourceSnapshot Capture(RenderFrameOutcome outcome) public WorldLifecycleResourceSnapshot Capture(RenderFrameOutcome outcome)
@ -136,6 +140,9 @@ internal sealed class WorldLifecycleResourceSnapshotSource
RenderSceneShadow: RenderSceneShadow:
_renderSceneShadow?.CaptureCheckpointSnapshot() _renderSceneShadow?.CaptureCheckpointSnapshot()
?? RenderSceneShadowComparisonSnapshot.Disabled, ?? RenderSceneShadowComparisonSnapshot.Disabled,
RenderFrameProduct:
_renderFrameProduct?.Snapshot
?? RenderFrameProductComparisonSnapshot.Disabled,
PendingLiveTeardowns: _liveEntities.PendingTeardownCount, PendingLiveTeardowns: _liveEntities.PendingTeardownCount,
PendingLandblockRetirements: _streaming.PendingRetirementCount, PendingLandblockRetirements: _streaming.PendingRetirementCount,
ParticleEmitters: _particles.ActiveEmitterCount, ParticleEmitters: _particles.ActiveEmitterCount,

View file

@ -15,6 +15,8 @@ public sealed class RetailPViewRenderer
{ {
private readonly InteriorEntityPartition.IObserver? _partitionObserver; private readonly InteriorEntityPartition.IObserver? _partitionObserver;
private readonly ICurrentRenderPViewObserver? _candidateObserver; private readonly ICurrentRenderPViewObserver? _candidateObserver;
private readonly RenderScenePViewFrameProductController?
_sceneFrameProduct;
private readonly PortalVisibilityFrame _mainPortalFrameScratch = new(); private readonly PortalVisibilityFrame _mainPortalFrameScratch = new();
private readonly ClipFrameAssembly _clipAssemblyScratch = new(); private readonly ClipFrameAssembly _clipAssemblyScratch = new();
private readonly ViewconeCuller _viewconeScratch = new(); private readonly ViewconeCuller _viewconeScratch = new();
@ -71,10 +73,19 @@ public sealed class RetailPViewRenderer
} }
internal 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; _partitionObserver = partitionObserver;
_candidateObserver = partitionObserver as ICurrentRenderPViewObserver; _candidateObserver = partitionObserver as ICurrentRenderPViewObserver;
_sceneFrameProduct = sceneFrameProduct;
} }
// T2 (BR-4): retail has NO distance constant on the flood-admission chain // 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); DrawDynamicsLast(ctx, passes, partition, viewcone, ctx.RootCell.IsOutdoorNode);
_candidateObserver?.CompletePViewFrame(); _candidateObserver?.CompletePViewFrame();
_sceneFrameProduct?.BuildAndCompare(
pvFrame,
clipAssembly,
viewcone,
_lookInFrames,
drawableCells,
ctx.Cells,
ctx.RootCell.IsOutdoorNode);
return result; return result;
} }
catch catch

View file

@ -9,6 +9,22 @@ internal enum RenderFrameBlendClass : byte
Alpha, 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( internal readonly record struct RenderFrameCellRange(
uint CellId, uint CellId,
int PViewOrder, int PViewOrder,
@ -60,7 +76,8 @@ internal readonly record struct RenderFrameDiagnosticCounts(
int OpaqueClassificationCount, int OpaqueClassificationCount,
int AlphaClassificationCount, int AlphaClassificationCount,
int LightSetCount, int LightSetCount,
int SelectionPartCount); int SelectionPartCount,
int RouteCandidateCount);
/// <summary> /// <summary>
/// A validated borrowed view over one reusable frame arena. Copies are safe: /// A validated borrowed view over one reusable frame arena. Copies are safe:
@ -125,6 +142,12 @@ internal readonly struct RenderFrameView
public ReadOnlySpan<RenderFrameSelectionPart> SelectionParts => public ReadOnlySpan<RenderFrameSelectionPart> SelectionParts =>
Arena.SelectionParts; Arena.SelectionParts;
public ReadOnlySpan<RenderProjectionRecord> RouteCandidates =>
Arena.RouteCandidates;
public ReadOnlySpan<RenderFrameCandidateRange> RouteRanges =>
Arena.RouteRanges;
public PortalVisibilityFrame? PortalFrame => Arena.PortalFrame; public PortalVisibilityFrame? PortalFrame => Arena.PortalFrame;
public ClipFrameAssembly? ClipAssembly => Arena.ClipAssembly; public ClipFrameAssembly? ClipAssembly => Arena.ClipAssembly;
@ -172,6 +195,8 @@ internal sealed class RenderFrameArena
private RenderFrameClassificationRecord[] _classifications = []; private RenderFrameClassificationRecord[] _classifications = [];
private RenderFrameObjectLightSet[] _lightSets = []; private RenderFrameObjectLightSet[] _lightSets = [];
private RenderFrameSelectionPart[] _selectionParts = []; private RenderFrameSelectionPart[] _selectionParts = [];
private RenderProjectionRecord[] _routeCandidates = [];
private RenderFrameCandidateRange[] _routeRanges = [];
private int _outdoorCount; private int _outdoorCount;
private int _cellStaticCount; private int _cellStaticCount;
private int _cellRangeCount; private int _cellRangeCount;
@ -180,6 +205,8 @@ internal sealed class RenderFrameArena
private int _classificationCount; private int _classificationCount;
private int _lightSetCount; private int _lightSetCount;
private int _selectionPartCount; private int _selectionPartCount;
private int _routeCandidateCount;
private int _routeRangeCount;
private int _opaqueClassificationCount; private int _opaqueClassificationCount;
private int _alphaClassificationCount; private int _alphaClassificationCount;
private ulong _epoch; private ulong _epoch;
@ -206,7 +233,8 @@ internal sealed class RenderFrameArena
_opaqueClassificationCount, _opaqueClassificationCount,
_alphaClassificationCount, _alphaClassificationCount,
_lightSetCount, _lightSetCount,
_selectionPartCount); _selectionPartCount,
_routeCandidateCount);
internal ReadOnlySpan<RenderProjectionRecord> OutdoorStaticCandidates => internal ReadOnlySpan<RenderProjectionRecord> OutdoorStaticCandidates =>
_outdoor.AsSpan(0, _outdoorCount); _outdoor.AsSpan(0, _outdoorCount);
@ -232,6 +260,12 @@ internal sealed class RenderFrameArena
internal ReadOnlySpan<RenderFrameSelectionPart> SelectionParts => internal ReadOnlySpan<RenderFrameSelectionPart> SelectionParts =>
_selectionParts.AsSpan(0, _selectionPartCount); _selectionParts.AsSpan(0, _selectionPartCount);
internal ReadOnlySpan<RenderProjectionRecord> RouteCandidates =>
_routeCandidates.AsSpan(0, _routeCandidateCount);
internal ReadOnlySpan<RenderFrameCandidateRange> RouteRanges =>
_routeRanges.AsSpan(0, _routeRangeCount);
internal ulong BeginWrite( internal ulong BeginWrite(
RenderSceneGeneration generation, RenderSceneGeneration generation,
ulong frameSequence) ulong frameSequence)
@ -253,6 +287,8 @@ internal sealed class RenderFrameArena
_classificationCount = 0; _classificationCount = 0;
_lightSetCount = 0; _lightSetCount = 0;
_selectionPartCount = 0; _selectionPartCount = 0;
_routeCandidateCount = 0;
_routeRangeCount = 0;
_opaqueClassificationCount = 0; _opaqueClassificationCount = 0;
_alphaClassificationCount = 0; _alphaClassificationCount = 0;
PortalFrame = null; PortalFrame = null;
@ -393,6 +429,34 @@ internal sealed class RenderFrameArena
_selectionParts[_selectionPartCount++] = part; _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) internal void Publish(ulong epoch)
{ {
EnsureBuilding(epoch); EnsureBuilding(epoch);
@ -549,6 +613,18 @@ internal readonly struct RenderFrameWriter
public void AddSelectionPart(in RenderFrameSelectionPart part) => public void AddSelectionPart(in RenderFrameSelectionPart part) =>
Arena.AddSelectionPart(_epoch, in 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() => public void Publish() =>
Owner.Publish(_arenaIndex, _epoch); Owner.Publish(_arenaIndex, _epoch);

View file

@ -27,6 +27,8 @@ internal static class RenderProjectionRecordFactory
ownerLandblockId, ownerLandblockId,
entity); entity);
RenderProjectionFlags flags = RenderProjectionFlags.Selectable; RenderProjectionFlags flags = RenderProjectionFlags.Selectable;
if (spatiallyVisible)
flags |= RenderProjectionFlags.SpatiallyResident;
if (entity.MeshRefs.Count > 0 if (entity.MeshRefs.Count > 0
&& spatiallyVisible && spatiallyVisible
&& entity.IsDrawVisible && entity.IsDrawVisible

View file

@ -13,6 +13,7 @@ internal readonly record struct RenderProjectionId
internal ulong RawValue => _value; internal ulong RawValue => _value;
internal byte Domain => (byte)(_value >> 56); internal byte Domain => (byte)(_value >> 56);
internal RenderSortKey ToSortKey() => new(_value); internal RenderSortKey ToSortKey() => new(_value);
internal void AddTo(ref StableRenderHash128 hash) => hash.Add(_value);
public int CompareTo(RenderProjectionId other) => public int CompareTo(RenderProjectionId other) =>
_value.CompareTo(other._value); _value.CompareTo(other._value);
@ -104,6 +105,13 @@ internal enum RenderProjectionFlags : uint
Selectable = 1 << 4, Selectable = 1 << 4,
LightCandidate = 1 << 5, LightCandidate = 1 << 5,
PortalStraddling = 1 << 6, 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] [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( new RenderWorldBounds(
shell.WorldBounds.Min, shell.WorldBounds.Min,
shell.WorldBounds.Max), shell.WorldBounds.Max),
RenderProjectionFlags.Draw, RenderProjectionFlags.Draw
| RenderProjectionFlags.SpatiallyResident,
default, default,
id.ToSortKey(), id.ToSortKey(),
RenderDirtyMask.All, RenderDirtyMask.All,

View file

@ -377,6 +377,7 @@ public sealed class WorldLifecycleAutomationControllerTests
private static WorldLifecycleResourceSnapshot EmptyResources() => new( private static WorldLifecycleResourceSnapshot EmptyResources() => new(
0, 0, 0, 0, 0, 0, 0, CurrentRenderSceneOracleSnapshot.Disabled, 0, 0, 0, 0, 0, 0, 0, CurrentRenderSceneOracleSnapshot.Disabled,
RenderSceneShadowComparisonSnapshot.Disabled, RenderSceneShadowComparisonSnapshot.Disabled,
RenderFrameProductComparisonSnapshot.Disabled,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0L, 0, 0L, 0L, 0L, 0L, 0, 0, 0, 0, 0, 0, 0, 0L, 0L, 0, 0, 0L, 0, 0L, 0L, 0L, 0L, 0, 0, 0, 0, 0, 0, 0, 0L, 0L,
0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L,

View file

@ -49,6 +49,11 @@ public sealed class RenderFrameProductTests
0x01000001, 0x01000001,
Matrix4x4.Identity, Matrix4x4.Identity,
SelectionMesh())); SelectionMesh()));
writer.AddRouteRange(
RenderFrameCandidateRoute.LandscapeOutdoorStatic,
0,
0,
[outdoor]);
var digest = new RenderSceneDigest( var digest = new RenderSceneDigest(
Generation, Generation,
new RenderProjectionCounts(Total: 4, 1, 2, 1, 0, 0), new RenderProjectionCounts(Total: 4, 1, 2, 1, 0, 0),
@ -70,8 +75,17 @@ public sealed class RenderFrameProductTests
Assert.Equal(2, view.Classifications.Length); Assert.Equal(2, view.Classifications.Length);
Assert.Equal(1, view.LightSets.Length); Assert.Equal(1, view.LightSets.Length);
Assert.Equal(1, view.SelectionParts.Length); Assert.Equal(1, view.SelectionParts.Length);
Assert.Equal(outdoor.Id, view.RouteCandidates[0].Id);
Assert.Equal( Assert.Equal(
new RenderFrameDiagnosticCounts(1, 2, 1, 1, 1, 1, 1, 1), new RenderFrameCandidateRange(
RenderFrameCandidateRoute.LandscapeOutdoorStatic,
0,
0,
0,
1),
view.RouteRanges[0]);
Assert.Equal(
new RenderFrameDiagnosticCounts(1, 2, 1, 1, 1, 1, 1, 1, 1),
view.DiagnosticCounts); view.DiagnosticCounts);
Assert.Equal(digest, view.SourceDigest); Assert.Equal(digest, view.SourceDigest);

View file

@ -0,0 +1,353 @@
using System.Numerics;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Scene.Arch;
using AcDream.Core.World;
namespace AcDream.App.Tests.Rendering;
public sealed class RenderScenePViewFrameProductTests
{
private const uint Landblock = 0xA9B4FFFF;
private const uint Cell = 0xA9B40170;
private static readonly RenderSceneGeneration Generation =
RenderSceneGeneration.FromRaw(1);
[Fact]
public void Builder_PreservesPViewRoutesAndExcludesShellsAndWithdrawnOwners()
{
using var scene = new ArchRenderScene(Generation);
RenderProjectionRecord outdoor = Record(
0x0100_0000_0000_0001,
RenderProjectionClass.OutdoorStatic);
RenderProjectionRecord hidden = Record(
0x0100_0000_0000_0002,
RenderProjectionClass.OutdoorStatic,
flags: RenderProjectionFlags.Hidden
| RenderProjectionFlags.SpatiallyResident);
RenderProjectionRecord withdrawn = Record(
0x0100_0000_0000_0003,
RenderProjectionClass.OutdoorStatic,
flags: RenderProjectionFlags.Draw);
RenderProjectionRecord shell = Record(
0x0200_0000_0000_0004,
RenderProjectionClass.IndoorCellStatic,
parentCell: Cell);
RenderProjectionRecord cellStatic = Record(
0x0100_0000_0000_0005,
RenderProjectionClass.IndoorCellStatic,
parentCell: Cell);
RenderProjectionRecord outdoorDynamic = Record(
0x0300_0000_0000_0006,
RenderProjectionClass.LiveDynamicRoot);
RenderProjectionRecord cellDynamic = Record(
0x0300_0000_0000_0007,
RenderProjectionClass.LiveDynamicRoot,
parentCell: Cell);
RenderProjectionRecord[] records =
[
outdoor,
hidden,
withdrawn,
shell,
cellStatic,
outdoorDynamic,
cellDynamic,
];
var deltas = new RenderProjectionDelta[records.Length];
for (int index = 0; index < records.Length; index++)
{
deltas[index] = RenderProjectionDelta.Register(
Generation,
(ulong)index + 1,
records[index]);
}
scene.Apply(deltas);
PortalVisibilityFrame portal = Portal(Cell);
ClipFrameAssembly clip = FullScreenClip(Cell);
ViewconeCuller viewcone =
ViewconeCuller.Build(clip, Matrix4x4.Identity);
var exchange = new RenderFrameExchange();
var builder = new RenderScenePViewFrameBuilder();
RenderSceneDigest digest =
scene.BuildDigest(new RenderSceneDigestBuffer());
var input = new RenderScenePViewBuildInput(
scene.OpenQuery(),
digest,
portal,
clip,
viewcone,
[],
[Cell],
EmptyCellSource.Instance,
RootIsOutdoor: true);
builder.Build(exchange, frameSequence: 1, in input);
RenderFrameView view = exchange.BorrowLatest(Generation, 1);
try
{
Assert.Equal(
[outdoor.Id, hidden.Id],
view.OutdoorStaticCandidates.ToArray()
.Select(static item => item.Id));
Assert.Equal(
[cellStatic.Id],
view.CellStaticCandidates.ToArray()
.Select(static item => item.Id));
Assert.Equal(
[outdoorDynamic.Id, cellDynamic.Id],
view.DynamicCandidates.ToArray()
.Select(static item => item.Id));
Assert.Equal(5, view.Transforms.Length);
Assert.Equal(5, view.RouteCandidates.Length);
Assert.Equal(3, view.RouteRanges.Length);
Assert.DoesNotContain(
view.RouteCandidates.ToArray(),
item => item.Id == withdrawn.Id || item.Id == shell.Id);
Assert.Equal(
new RenderFrameDiagnosticCounts(
OutdoorStaticCandidates: 2,
CellStaticCandidates: 1,
DynamicCandidates: 2,
TransformCount: 5,
OpaqueClassificationCount: 0,
AlphaClassificationCount: 0,
LightSetCount: 0,
SelectionPartCount: 0,
RouteCandidateCount: 5),
view.DiagnosticCounts);
Assert.Same(portal, view.PortalFrame);
Assert.Same(clip, view.ClipAssembly);
Assert.Equal(digest, view.SourceDigest);
}
finally
{
exchange.Release(in view);
}
}
[Fact]
public void Controller_MatchesCurrentPViewThenNamesAWithdrawnCandidate()
{
WorldEntity outdoor = Entity(10, serverGuid: 0, parentCell: null);
WorldEntity cellStatic = Entity(11, serverGuid: 0, parentCell: Cell);
WorldEntity outdoorDynamic =
Entity(1_000_012, 0x8000_0012, parentCell: null);
WorldEntity cellDynamic =
Entity(1_000_013, 0x8000_0013, parentCell: Cell);
WorldEntity[] entities =
[outdoor, cellStatic, outdoorDynamic, cellDynamic];
var oracle = new CurrentRenderSceneOracle();
var partition = new InteriorEntityPartition.Result();
InteriorEntityPartition.Partition(
partition,
[Cell],
[Entry(entities)],
oracle);
oracle.BeginPViewFrame();
oracle.ObservePViewBucket(
CurrentRenderPViewRoute.LandscapeOutdoorStatic,
0,
0,
[outdoor]);
oracle.ObservePViewBucket(
CurrentRenderPViewRoute.CellStatic,
0,
0,
[cellStatic]);
oracle.ObservePViewBucket(
CurrentRenderPViewRoute.DynamicLast,
0,
0,
[outdoorDynamic, cellDynamic]);
oracle.CompletePViewFrame();
using var shadow = new RenderSceneShadowRuntime(Generation);
foreach (WorldEntity entity in entities)
{
RenderProjectionRecord record = Project(entity);
shadow.Journal.Register(in record);
}
shadow.DrainUpdateBoundary();
var logs = new List<string>();
var controller = new RenderScenePViewFrameProductController(
shadow,
oracle,
logs.Add);
PortalVisibilityFrame portal = Portal(Cell);
ClipFrameAssembly clip = FullScreenClip(Cell);
ViewconeCuller viewcone =
ViewconeCuller.Build(clip, Matrix4x4.Identity);
controller.BuildAndCompare(
portal,
clip,
viewcone,
[],
[Cell],
EmptyCellSource.Instance,
rootIsOutdoor: true);
RenderFrameProductComparisonSnapshot matched = controller.Snapshot;
Assert.True(matched.Enabled);
Assert.Equal(1uL, matched.ComparisonCount);
Assert.Equal(1uL, matched.SuccessfulComparisonCount);
Assert.Equal(4, matched.ExpectedCandidateCount);
Assert.Equal(4, matched.ActualCandidateCount);
Assert.Equal(0, matched.MismatchCount);
Assert.Null(matched.FirstMismatch);
Assert.Equal(matched.ExpectedDigest, matched.ActualDigest);
Assert.Empty(logs);
RenderProjectionId withdrawnId =
LiveRenderProjectionJournal.ProjectionId(cellDynamic.Id);
shadow.Journal.Unregister(
withdrawnId,
RenderOwnerIncarnation.FromRaw(cellDynamic.Id));
shadow.DrainUpdateBoundary();
controller.BuildAndCompare(
portal,
clip,
viewcone,
[],
[Cell],
EmptyCellSource.Instance,
rootIsOutdoor: true);
RenderFrameProductComparisonSnapshot mismatch = controller.Snapshot;
Assert.Equal(2uL, mismatch.ComparisonCount);
Assert.Equal(1uL, mismatch.SuccessfulComparisonCount);
Assert.Equal(1, mismatch.MismatchCount);
Assert.Contains("field=count expected=4 actual=3", mismatch.FirstMismatch);
Assert.Single(logs);
}
private static RenderProjectionRecord Project(WorldEntity entity)
{
bool dynamic = entity.ServerGuid != 0;
RenderProjectionId id = dynamic
? LiveRenderProjectionJournal.ProjectionId(entity.Id)
: StaticRenderProjectionJournal.StaticEntityId(
Landblock,
entity.Id);
RenderProjectionClass projectionClass = dynamic
? RenderProjectionClass.LiveDynamicRoot
: entity.ParentCellId is uint parentCell
&& InteriorEntityPartition.IsIndoorCellId(parentCell)
? RenderProjectionClass.IndoorCellStatic
: RenderProjectionClass.OutdoorStatic;
return RenderProjectionRecordFactory.ProjectEntity(
id,
projectionClass,
RenderOwnerIncarnation.FromRaw(entity.Id),
Landblock,
entity.ParentCellId ?? Landblock,
entity,
spatiallyVisible: true);
}
private static RenderProjectionRecord Record(
ulong id,
RenderProjectionClass projectionClass,
uint? parentCell = null,
RenderProjectionFlags flags =
RenderProjectionFlags.Draw
| RenderProjectionFlags.SpatiallyResident)
{
uint fullCellId = parentCell ?? Landblock;
Matrix4x4 transform = Matrix4x4.Identity;
return new RenderProjectionRecord(
RenderProjectionId.FromRaw(id),
projectionClass,
RenderOwnerIncarnation.FromRaw(id),
new RenderTransform(transform),
new PreviousRenderTransform(transform),
new RenderMeshSet(RenderAssetHandle.FromRaw(id), 1, 1),
new RenderMaterialVariant(0, 0, 1),
new RenderSpatialResidency(
RenderSpatialBucket.FromRaw(fullCellId),
Landblock,
fullCellId),
new RenderWorldBounds(
new Vector3(-1),
new Vector3(1)),
flags,
default,
new RenderSortKey(id),
RenderDirtyMask.All,
new RenderSourceMetadata(
LocalEntityId: (uint)id,
ServerGuid: projectionClass
is RenderProjectionClass.LiveDynamicRoot ? (uint)id : 0,
SourceId: 1,
ParentCellId: parentCell ?? 0,
EffectCellId: 0,
BuildingShellAnchorCellId: 0,
TransformFingerprint: default,
GeometryFingerprint: default,
AppearanceFingerprint: default));
}
private static WorldEntity Entity(
uint id,
uint serverGuid,
uint? parentCell) =>
new()
{
Id = id,
ServerGuid = serverGuid,
SourceGfxObjOrSetupId = 0x0200_0001,
Position = Vector3.Zero,
Rotation = Quaternion.Identity,
MeshRefs =
[
new MeshRef(0x0100_0001, Matrix4x4.Identity),
],
ParentCellId = parentCell,
};
private static PortalVisibilityFrame Portal(uint cellId)
{
var frame = new PortalVisibilityFrame();
frame.OrderedVisibleCells.Add(cellId);
return frame;
}
private static ClipFrameAssembly FullScreenClip(uint cellId)
{
var slice = new ClipViewSlice(
Slot: 0,
NdcAabb: new Vector4(-1, -1, 1, 1),
Planes: []);
var assembly = new ClipFrameAssembly();
assembly.SetOutsideViewSlices([slice]);
assembly.CellIdToViewSlices[cellId] = [slice];
return assembly;
}
private static (
uint LandblockId,
Vector3 AabbMin,
Vector3 AabbMax,
IReadOnlyList<WorldEntity> Entities,
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)
Entry(IReadOnlyList<WorldEntity> entities) =>
(
Landblock,
Vector3.Zero,
Vector3.One,
entities,
null);
private sealed class EmptyCellSource : IRetailPViewCellSource
{
public static EmptyCellSource Instance { get; } = new();
private EmptyCellSource()
{
}
public LoadedCell? Find(uint cellId) => null;
}
}