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. |
|
| 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. |
|
||||||
| G2–G5 | pending | No production consumer has switched. |
|
| G2–G5 | 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
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
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(
|
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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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