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
|
|
@ -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. |
|
||||
| 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. |
|
||||
| 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. |
|
||||
| G2–G5 | pending | No production consumer has switched. |
|
||||
|
||||
The exact pre-F/G runtime rollback anchor remains `e7d9d6fa`. F0 is
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -377,6 +377,7 @@ public sealed class WorldLifecycleAutomationControllerTests
|
|||
private static WorldLifecycleResourceSnapshot EmptyResources() => new(
|
||||
0, 0, 0, 0, 0, 0, 0, CurrentRenderSceneOracleSnapshot.Disabled,
|
||||
RenderSceneShadowComparisonSnapshot.Disabled,
|
||||
RenderFrameProductComparisonSnapshot.Disabled,
|
||||
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,
|
||||
0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L,
|
||||
|
|
|
|||
|
|
@ -49,6 +49,11 @@ public sealed class RenderFrameProductTests
|
|||
0x01000001,
|
||||
Matrix4x4.Identity,
|
||||
SelectionMesh()));
|
||||
writer.AddRouteRange(
|
||||
RenderFrameCandidateRoute.LandscapeOutdoorStatic,
|
||||
0,
|
||||
0,
|
||||
[outdoor]);
|
||||
var digest = new RenderSceneDigest(
|
||||
Generation,
|
||||
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(1, view.LightSets.Length);
|
||||
Assert.Equal(1, view.SelectionParts.Length);
|
||||
Assert.Equal(outdoor.Id, view.RouteCandidates[0].Id);
|
||||
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);
|
||||
Assert.Equal(digest, view.SourceDigest);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue