feat(rendering): pack scene dispatcher inputs
Capture immutable presentation payloads in the render scene, flatten unique entity and mesh-part records into the borrowed frame arena, and compare every routed dispatcher input tuple without changing the production draw source.
This commit is contained in:
parent
720578592b
commit
d8d9897376
13 changed files with 593 additions and 24 deletions
|
|
@ -25,7 +25,7 @@ retail-faithful gameplay
|
|||
| 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 | complete | Exact `e346f8bb` passed capped and uncapped nine-stop routes plus uncapped dense Arwic: 29,392 + 29,025 + 1,522 same-frame candidate comparisons, zero mismatches, equal counts/digests at every checkpoint, exact binary/source identity, and graceful shutdown. Evidence: `docs/research/2026-07-25-slice-g1-scene-query-candidate-gate.md`. |
|
||||
| G2 — packed dispatcher input | active | Convert the accepted scene candidates into an acdream-owned packed dispatcher input and compare its complete instance payload against the current WorldEntity boundary without drawing twice. |
|
||||
| G2 — packed dispatcher input | source-packing gate active | The scene now retains an immutable presentation payload and the frame product flattens each unique projection into entity and mesh-part spans, including palette and surface-override identity. The compare-only controller expands exact PView routes and checks every current dispatcher input tuple without drawing twice. The full Release gate is 3,746 App tests / 3 skips and 8,230 solution tests / 5 skips. Connected source-input parity and final instance-output parity remain before G2 closes. |
|
||||
| G3–G5 | pending | No production consumer has switched. |
|
||||
|
||||
The exact pre-F/G runtime rollback anchor remains `e7d9d6fa`. F0 is
|
||||
|
|
|
|||
|
|
@ -257,6 +257,8 @@ public sealed class RetailPViewRenderer
|
|||
_lookInFrames,
|
||||
drawableCells,
|
||||
ctx.Cells,
|
||||
ctx.AnimatedEntityIds,
|
||||
ctx.PlayerLandblockId ?? 0,
|
||||
ctx.RootCell.IsOutdoorNode);
|
||||
return result;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -430,6 +430,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
|||
_world.Set(entry.Entity, record.Material);
|
||||
_world.Set(entry.Entity, record.DegradeState);
|
||||
_world.Set(entry.Entity, record.Source);
|
||||
_world.Set(entry.Entity, record.EntityPayload);
|
||||
OrDirty(
|
||||
entry.Entity,
|
||||
record.Id,
|
||||
|
|
@ -528,6 +529,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
|||
record.OwnerIncarnation,
|
||||
record.DirtyMask,
|
||||
record.Source,
|
||||
record.EntityPayload,
|
||||
tag);
|
||||
|
||||
private void WriteRecord(
|
||||
|
|
@ -546,6 +548,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
|||
_world.Set(entity, record.OwnerIncarnation);
|
||||
_world.Set(entity, record.DirtyMask);
|
||||
_world.Set(entity, record.Source);
|
||||
_world.Set(entity, record.EntityPayload);
|
||||
}
|
||||
|
||||
private RenderProjectionRecord ReadRecord(in SceneEntry entry) =>
|
||||
|
|
@ -563,7 +566,8 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
|||
_world.Get<RenderDegradeState>(entry.Entity),
|
||||
_world.Get<RenderSortKey>(entry.Entity),
|
||||
_world.Get<RenderDirtyMask>(entry.Entity),
|
||||
_world.Get<RenderSourceMetadata>(entry.Entity));
|
||||
_world.Get<RenderSourceMetadata>(entry.Entity),
|
||||
_world.Get<RenderEntityPayload>(entry.Entity));
|
||||
|
||||
private void Destroy(in SceneEntry entry)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -50,7 +50,10 @@ internal readonly record struct CurrentRenderDispatcherFingerprint(
|
|||
int MeshRefIndex,
|
||||
uint TupleLandblockId,
|
||||
uint CacheLandblockId,
|
||||
CurrentRenderProjectionFingerprint Projection);
|
||||
CurrentRenderProjectionFingerprint Projection,
|
||||
uint GfxObjId,
|
||||
Matrix4x4 PartTransform,
|
||||
RenderSceneHash128 SurfaceOverrides);
|
||||
|
||||
internal readonly record struct CurrentRenderDispatcherSubmission(
|
||||
int VisibleInstanceCount,
|
||||
|
|
@ -535,7 +538,13 @@ internal sealed class CurrentRenderSceneOracle :
|
|||
MeshRefIndex: tuple.MeshRefIndex,
|
||||
TupleLandblockId: tuple.LandblockId,
|
||||
CacheLandblockId: cacheLandblockId,
|
||||
Projection: projection);
|
||||
Projection: projection,
|
||||
GfxObjId: tuple.Entity.MeshRefs[tuple.MeshRefIndex].GfxObjId,
|
||||
PartTransform:
|
||||
tuple.Entity.MeshRefs[tuple.MeshRefIndex].PartTransform,
|
||||
SurfaceOverrides: CreateSurfaceOverrideFingerprint(
|
||||
tuple.Entity.MeshRefs[tuple.MeshRefIndex]
|
||||
.SurfaceOverrides));
|
||||
_dispatcherCandidates.Add(candidate);
|
||||
AddDispatcherCandidate(ref _dispatcherHash, in candidate);
|
||||
}
|
||||
|
|
@ -730,6 +739,7 @@ internal sealed class CurrentRenderSceneOracle :
|
|||
MeshRef mesh = entity.MeshRefs[meshIndex];
|
||||
geometry.Add(mesh.GfxObjId);
|
||||
geometry.Add(mesh.PartTransform);
|
||||
AddSurfaceOverrides(ref geometry, mesh.SurfaceOverrides);
|
||||
}
|
||||
|
||||
StableRenderHash128 appearance = StableRenderHash128.Create();
|
||||
|
|
@ -795,6 +805,54 @@ internal sealed class CurrentRenderSceneOracle :
|
|||
Appearance: appearance.Finish());
|
||||
}
|
||||
|
||||
private static void AddSurfaceOverrides(
|
||||
ref StableRenderHash128 geometry,
|
||||
IReadOnlyDictionary<uint, uint>? overrides)
|
||||
{
|
||||
RenderSceneHash128 fingerprint =
|
||||
CreateSurfaceOverrideFingerprint(overrides);
|
||||
geometry.Add(fingerprint.Low);
|
||||
geometry.Add(fingerprint.High);
|
||||
}
|
||||
|
||||
internal static RenderSceneHash128 CreateSurfaceOverrideFingerprint(
|
||||
IReadOnlyDictionary<uint, uint>? overrides)
|
||||
{
|
||||
if (overrides is null)
|
||||
{
|
||||
StableRenderHash128 missing = StableRenderHash128.Create();
|
||||
missing.Add(-1);
|
||||
return missing.Finish();
|
||||
}
|
||||
|
||||
ulong xorLow = 0;
|
||||
ulong xorHigh = 0;
|
||||
ulong sumLow = 0;
|
||||
ulong sumHigh = 0;
|
||||
foreach ((uint surfaceId, uint replacementId) in overrides)
|
||||
{
|
||||
StableRenderHash128 pair = StableRenderHash128.Create();
|
||||
pair.Add(surfaceId);
|
||||
pair.Add(replacementId);
|
||||
RenderSceneHash128 value = pair.Finish();
|
||||
xorLow ^= value.Low;
|
||||
xorHigh ^= value.High;
|
||||
unchecked
|
||||
{
|
||||
sumLow += value.Low;
|
||||
sumHigh += value.High;
|
||||
}
|
||||
}
|
||||
|
||||
StableRenderHash128 geometry = StableRenderHash128.Create();
|
||||
geometry.Add(overrides.Count);
|
||||
geometry.Add(xorLow);
|
||||
geometry.Add(xorHigh);
|
||||
geometry.Add(sumLow);
|
||||
geometry.Add(sumHigh);
|
||||
return geometry.Finish();
|
||||
}
|
||||
|
||||
private static RenderSceneHash128 FingerprintSelectionGeometry(
|
||||
RetailSelectionMesh mesh)
|
||||
{
|
||||
|
|
@ -854,6 +912,10 @@ internal sealed class CurrentRenderSceneOracle :
|
|||
hash.Add(candidate.MeshRefIndex);
|
||||
hash.Add(candidate.TupleLandblockId);
|
||||
hash.Add(candidate.CacheLandblockId);
|
||||
hash.Add(candidate.GfxObjId);
|
||||
hash.Add(candidate.PartTransform);
|
||||
hash.Add(candidate.SurfaceOverrides.Low);
|
||||
hash.Add(candidate.SurfaceOverrides.High);
|
||||
CurrentRenderProjectionFingerprint projection = candidate.Projection;
|
||||
AddProjection(ref hash, in projection);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Rendering.Scene;
|
||||
|
||||
|
|
@ -36,6 +37,17 @@ internal readonly record struct RenderFrameTransformRecord(
|
|||
int PartIndex,
|
||||
Matrix4x4 LocalToWorld);
|
||||
|
||||
internal readonly record struct RenderFrameEntityCandidate(
|
||||
RenderProjectionRecord Projection,
|
||||
int MeshPartOffset,
|
||||
int MeshPartCount,
|
||||
bool Animated);
|
||||
|
||||
internal readonly record struct RenderFrameMeshPart(
|
||||
RenderProjectionId ProjectionId,
|
||||
int PartIndex,
|
||||
MeshRef MeshRef);
|
||||
|
||||
internal readonly record struct RenderFrameClassificationRecord(
|
||||
RenderProjectionId ProjectionId,
|
||||
int CandidateIndex,
|
||||
|
|
@ -77,7 +89,9 @@ internal readonly record struct RenderFrameDiagnosticCounts(
|
|||
int AlphaClassificationCount,
|
||||
int LightSetCount,
|
||||
int SelectionPartCount,
|
||||
int RouteCandidateCount);
|
||||
int RouteCandidateCount,
|
||||
int EntityCandidateCount,
|
||||
int MeshPartCount);
|
||||
|
||||
/// <summary>
|
||||
/// A validated borrowed view over one reusable frame arena. Copies are safe:
|
||||
|
|
@ -133,6 +147,12 @@ internal readonly struct RenderFrameView
|
|||
public ReadOnlySpan<RenderFrameTransformRecord> Transforms =>
|
||||
Arena.Transforms;
|
||||
|
||||
public ReadOnlySpan<RenderFrameEntityCandidate> EntityCandidates =>
|
||||
Arena.EntityCandidates;
|
||||
|
||||
public ReadOnlySpan<RenderFrameMeshPart> MeshParts =>
|
||||
Arena.MeshParts;
|
||||
|
||||
public ReadOnlySpan<RenderFrameClassificationRecord> Classifications =>
|
||||
Arena.Classifications;
|
||||
|
||||
|
|
@ -192,6 +212,8 @@ internal sealed class RenderFrameArena
|
|||
private RenderFrameCellRange[] _cellRanges = [];
|
||||
private RenderProjectionRecord[] _dynamics = [];
|
||||
private RenderFrameTransformRecord[] _transforms = [];
|
||||
private RenderFrameEntityCandidate[] _entityCandidates = [];
|
||||
private RenderFrameMeshPart[] _meshParts = [];
|
||||
private RenderFrameClassificationRecord[] _classifications = [];
|
||||
private RenderFrameObjectLightSet[] _lightSets = [];
|
||||
private RenderFrameSelectionPart[] _selectionParts = [];
|
||||
|
|
@ -202,6 +224,8 @@ internal sealed class RenderFrameArena
|
|||
private int _cellRangeCount;
|
||||
private int _dynamicCount;
|
||||
private int _transformCount;
|
||||
private int _entityCandidateCount;
|
||||
private int _meshPartCount;
|
||||
private int _classificationCount;
|
||||
private int _lightSetCount;
|
||||
private int _selectionPartCount;
|
||||
|
|
@ -234,7 +258,9 @@ internal sealed class RenderFrameArena
|
|||
_alphaClassificationCount,
|
||||
_lightSetCount,
|
||||
_selectionPartCount,
|
||||
_routeCandidateCount);
|
||||
_routeCandidateCount,
|
||||
_entityCandidateCount,
|
||||
_meshPartCount);
|
||||
|
||||
internal ReadOnlySpan<RenderProjectionRecord> OutdoorStaticCandidates =>
|
||||
_outdoor.AsSpan(0, _outdoorCount);
|
||||
|
|
@ -251,6 +277,12 @@ internal sealed class RenderFrameArena
|
|||
internal ReadOnlySpan<RenderFrameTransformRecord> Transforms =>
|
||||
_transforms.AsSpan(0, _transformCount);
|
||||
|
||||
internal ReadOnlySpan<RenderFrameEntityCandidate> EntityCandidates =>
|
||||
_entityCandidates.AsSpan(0, _entityCandidateCount);
|
||||
|
||||
internal ReadOnlySpan<RenderFrameMeshPart> MeshParts =>
|
||||
_meshParts.AsSpan(0, _meshPartCount);
|
||||
|
||||
internal ReadOnlySpan<RenderFrameClassificationRecord> Classifications =>
|
||||
_classifications.AsSpan(0, _classificationCount);
|
||||
|
||||
|
|
@ -284,6 +316,8 @@ internal sealed class RenderFrameArena
|
|||
_cellRangeCount = 0;
|
||||
_dynamicCount = 0;
|
||||
_transformCount = 0;
|
||||
_entityCandidateCount = 0;
|
||||
_meshPartCount = 0;
|
||||
_classificationCount = 0;
|
||||
_lightSetCount = 0;
|
||||
_selectionPartCount = 0;
|
||||
|
|
@ -397,6 +431,46 @@ internal sealed class RenderFrameArena
|
|||
_transforms[_transformCount++] = transform;
|
||||
}
|
||||
|
||||
internal void AddEntityCandidate(
|
||||
ulong epoch,
|
||||
in RenderProjectionRecord projection,
|
||||
bool animated)
|
||||
{
|
||||
EnsureBuilding(epoch);
|
||||
IReadOnlyList<MeshRef>? meshRefs =
|
||||
projection.EntityPayload.MeshRefs;
|
||||
int meshPartCount = meshRefs?.Count ?? 0;
|
||||
if (meshPartCount != projection.MeshSet.MeshCount)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Projection {projection.Id} declares "
|
||||
+ $"{projection.MeshSet.MeshCount} mesh parts but its "
|
||||
+ $"presentation payload contains {meshPartCount}.");
|
||||
}
|
||||
|
||||
int meshPartOffset = _meshPartCount;
|
||||
EnsureCapacity(
|
||||
ref _meshParts,
|
||||
checked(meshPartOffset + meshPartCount));
|
||||
for (int partIndex = 0; partIndex < meshPartCount; partIndex++)
|
||||
{
|
||||
_meshParts[_meshPartCount++] = new RenderFrameMeshPart(
|
||||
projection.Id,
|
||||
partIndex,
|
||||
meshRefs![partIndex]);
|
||||
}
|
||||
|
||||
EnsureCapacity(
|
||||
ref _entityCandidates,
|
||||
_entityCandidateCount + 1);
|
||||
_entityCandidates[_entityCandidateCount++] =
|
||||
new RenderFrameEntityCandidate(
|
||||
projection,
|
||||
meshPartOffset,
|
||||
meshPartCount,
|
||||
animated);
|
||||
}
|
||||
|
||||
internal void AddClassification(
|
||||
ulong epoch,
|
||||
in RenderFrameClassificationRecord classification)
|
||||
|
|
@ -529,6 +603,10 @@ internal sealed class RenderFrameArena
|
|||
{
|
||||
if (_selectionPartCount > 0)
|
||||
Array.Clear(_selectionParts, 0, _selectionPartCount);
|
||||
if (_entityCandidateCount > 0)
|
||||
Array.Clear(_entityCandidates, 0, _entityCandidateCount);
|
||||
if (_meshPartCount > 0)
|
||||
Array.Clear(_meshParts, 0, _meshPartCount);
|
||||
}
|
||||
|
||||
private static void EnsureCapacity<T>(ref T[] values, int required)
|
||||
|
|
@ -603,6 +681,11 @@ internal readonly struct RenderFrameWriter
|
|||
public void AddTransform(in RenderFrameTransformRecord transform) =>
|
||||
Arena.AddTransform(_epoch, in transform);
|
||||
|
||||
public void AddEntityCandidate(
|
||||
in RenderProjectionRecord projection,
|
||||
bool animated) =>
|
||||
Arena.AddEntityCandidate(_epoch, in projection, animated);
|
||||
|
||||
public void AddClassification(
|
||||
in RenderFrameClassificationRecord classification) =>
|
||||
Arena.AddClassification(_epoch, in classification);
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ internal sealed class RenderProjectionJournal
|
|||
if (prior.MeshSet != current.MeshSet
|
||||
|| prior.Material != current.Material
|
||||
|| prior.DegradeState != current.DegradeState
|
||||
|| prior.EntityPayload != current.EntityPayload
|
||||
|| prior.Source.GeometryFingerprint
|
||||
!= current.Source.GeometryFingerprint
|
||||
|| prior.Source.AppearanceFingerprint
|
||||
|
|
|
|||
|
|
@ -80,7 +80,11 @@ internal static class RenderProjectionRecordFactory
|
|||
fingerprint.Transform,
|
||||
fingerprint.Geometry,
|
||||
fingerprint.Appearance,
|
||||
fingerprint.Flags));
|
||||
fingerprint.Flags),
|
||||
new RenderEntityPayload(
|
||||
entity.MeshRefs,
|
||||
entity.PaletteOverride,
|
||||
entity.IsBuildingShell));
|
||||
}
|
||||
|
||||
private static (Vector3 Minimum, Vector3 Maximum) CalculateBounds(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Numerics;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Rendering.Scene;
|
||||
|
||||
|
|
@ -194,6 +195,18 @@ internal readonly record struct RenderSourceMetadata(
|
|||
RenderSceneHash128 AppearanceFingerprint,
|
||||
uint CurrentProjectionFlags = 0);
|
||||
|
||||
/// <summary>
|
||||
/// Borrowed immutable presentation payload captured at a scene publication
|
||||
/// edge. WorldEntity replaces its MeshRefs collection when animated or
|
||||
/// appearance state changes, so retaining this exact collection reference is
|
||||
/// a snapshot rather than a callback into gameplay state. G2 copies it into
|
||||
/// the reusable frame arena before any dispatcher consumer observes it.
|
||||
/// </summary>
|
||||
internal readonly record struct RenderEntityPayload(
|
||||
IReadOnlyList<MeshRef> MeshRefs,
|
||||
PaletteOverride? PaletteOverride,
|
||||
bool IsBuildingShell);
|
||||
|
||||
internal readonly record struct RenderProjectionRecord(
|
||||
RenderProjectionId Id,
|
||||
RenderProjectionClass ProjectionClass,
|
||||
|
|
@ -208,7 +221,8 @@ internal readonly record struct RenderProjectionRecord(
|
|||
RenderDegradeState DegradeState,
|
||||
RenderSortKey SortKey,
|
||||
RenderDirtyMask DirtyMask,
|
||||
RenderSourceMetadata Source = default);
|
||||
RenderSourceMetadata Source = default,
|
||||
RenderEntityPayload EntityPayload = default);
|
||||
|
||||
internal enum RenderProjectionDeltaKind : byte
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
|
||||
namespace AcDream.App.Rendering.Scene;
|
||||
|
||||
|
|
@ -11,6 +12,7 @@ internal readonly record struct RenderScenePViewBuildInput(
|
|||
IReadOnlyList<PortalVisibilityFrame> LookInFrames,
|
||||
HashSet<uint> DrawableCells,
|
||||
IRetailPViewCellSource Cells,
|
||||
HashSet<uint>? AnimatedEntityIds,
|
||||
bool RootIsOutdoor);
|
||||
|
||||
internal readonly record struct RenderFrameProductComparisonSnapshot(
|
||||
|
|
@ -25,6 +27,14 @@ internal readonly record struct RenderFrameProductComparisonSnapshot(
|
|||
string? FirstMismatch,
|
||||
RenderSceneHash128 ExpectedDigest,
|
||||
RenderSceneHash128 ActualDigest,
|
||||
ulong PackedInputComparisonCount,
|
||||
ulong PackedInputSuccessfulComparisonCount,
|
||||
int PackedInputExpectedCount,
|
||||
int PackedInputActualCount,
|
||||
long PackedInputMismatchCount,
|
||||
string? PackedInputFirstMismatch,
|
||||
RenderSceneHash128 PackedInputExpectedDigest,
|
||||
RenderSceneHash128 PackedInputActualDigest,
|
||||
RenderFrameDiagnosticCounts ProductCounts)
|
||||
{
|
||||
public static RenderFrameProductComparisonSnapshot Disabled { get; } =
|
||||
|
|
@ -40,6 +50,14 @@ internal readonly record struct RenderFrameProductComparisonSnapshot(
|
|||
FirstMismatch: null,
|
||||
ExpectedDigest: RenderSceneHash128.Empty,
|
||||
ActualDigest: RenderSceneHash128.Empty,
|
||||
PackedInputComparisonCount: 0,
|
||||
PackedInputSuccessfulComparisonCount: 0,
|
||||
PackedInputExpectedCount: 0,
|
||||
PackedInputActualCount: 0,
|
||||
PackedInputMismatchCount: 0,
|
||||
PackedInputFirstMismatch: null,
|
||||
PackedInputExpectedDigest: RenderSceneHash128.Empty,
|
||||
PackedInputActualDigest: RenderSceneHash128.Empty,
|
||||
ProductCounts: default);
|
||||
}
|
||||
|
||||
|
|
@ -63,12 +81,18 @@ internal sealed class RenderScenePViewFrameProductController :
|
|||
private readonly RenderFrameExchange _exchange = new();
|
||||
private readonly List<CandidateKey> _expected = [];
|
||||
private readonly List<CandidateKey> _actual = [];
|
||||
private readonly List<PackedInputKey> _packedExpected = [];
|
||||
private readonly List<PackedInputKey> _packedActual = [];
|
||||
private readonly Action<string>? _log;
|
||||
private ulong _productFrameSequence;
|
||||
private ulong _comparisonCount;
|
||||
private ulong _successfulComparisonCount;
|
||||
private long _mismatchCount;
|
||||
private string? _firstMismatch;
|
||||
private ulong _packedInputComparisonCount;
|
||||
private ulong _packedInputSuccessfulComparisonCount;
|
||||
private long _packedInputMismatchCount;
|
||||
private string? _packedInputFirstMismatch;
|
||||
|
||||
public RenderScenePViewFrameProductController(
|
||||
RenderSceneShadowRuntime shadow,
|
||||
|
|
@ -93,6 +117,8 @@ internal sealed class RenderScenePViewFrameProductController :
|
|||
IReadOnlyList<PortalVisibilityFrame> lookInFrames,
|
||||
HashSet<uint> drawableCells,
|
||||
IRetailPViewCellSource cells,
|
||||
HashSet<uint>? animatedEntityIds,
|
||||
uint tupleLandblockId,
|
||||
bool rootIsOutdoor)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(portalFrame);
|
||||
|
|
@ -113,6 +139,7 @@ internal sealed class RenderScenePViewFrameProductController :
|
|||
lookInFrames,
|
||||
drawableCells,
|
||||
cells,
|
||||
animatedEntityIds,
|
||||
rootIsOutdoor);
|
||||
_builder.Build(_exchange, frameSequence, in input);
|
||||
RenderFrameView view = _exchange.BorrowLatest(
|
||||
|
|
@ -120,7 +147,7 @@ internal sealed class RenderScenePViewFrameProductController :
|
|||
frameSequence);
|
||||
try
|
||||
{
|
||||
Compare(in view);
|
||||
Compare(in view, tupleLandblockId);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -128,7 +155,7 @@ internal sealed class RenderScenePViewFrameProductController :
|
|||
}
|
||||
}
|
||||
|
||||
private void Compare(in RenderFrameView view)
|
||||
private void Compare(in RenderFrameView view, uint tupleLandblockId)
|
||||
{
|
||||
_comparisonCount = checked(_comparisonCount + 1);
|
||||
_expected.Clear();
|
||||
|
|
@ -196,6 +223,9 @@ internal sealed class RenderScenePViewFrameProductController :
|
|||
}
|
||||
}
|
||||
|
||||
PackedInputComparison packed =
|
||||
ComparePackedInput(in view, tupleLandblockId);
|
||||
|
||||
Snapshot = new RenderFrameProductComparisonSnapshot(
|
||||
Enabled: true,
|
||||
ProductFrameSequence: view.FrameSequence,
|
||||
|
|
@ -209,9 +239,184 @@ internal sealed class RenderScenePViewFrameProductController :
|
|||
FirstMismatch: _firstMismatch,
|
||||
ExpectedDigest: expectedDigest,
|
||||
ActualDigest: actualDigest,
|
||||
PackedInputComparisonCount: _packedInputComparisonCount,
|
||||
PackedInputSuccessfulComparisonCount:
|
||||
_packedInputSuccessfulComparisonCount,
|
||||
PackedInputExpectedCount: _packedExpected.Count,
|
||||
PackedInputActualCount: _packedActual.Count,
|
||||
PackedInputMismatchCount: _packedInputMismatchCount,
|
||||
PackedInputFirstMismatch: _packedInputFirstMismatch,
|
||||
PackedInputExpectedDigest: packed.ExpectedDigest,
|
||||
PackedInputActualDigest: packed.ActualDigest,
|
||||
ProductCounts: view.DiagnosticCounts);
|
||||
}
|
||||
|
||||
private PackedInputComparison ComparePackedInput(
|
||||
in RenderFrameView view,
|
||||
uint tupleLandblockId)
|
||||
{
|
||||
_packedInputComparisonCount =
|
||||
checked(_packedInputComparisonCount + 1);
|
||||
_packedExpected.Clear();
|
||||
_packedActual.Clear();
|
||||
|
||||
IReadOnlyList<CurrentRenderDispatcherFingerprint> expected =
|
||||
_current.DispatcherCandidates;
|
||||
if (_packedExpected.Capacity < expected.Count)
|
||||
_packedExpected.Capacity = expected.Count;
|
||||
for (int index = 0; index < expected.Count; index++)
|
||||
{
|
||||
CurrentRenderDispatcherFingerprint candidate = expected[index];
|
||||
CurrentRenderProjectionFingerprint projection =
|
||||
candidate.Projection;
|
||||
_packedExpected.Add(new PackedInputKey(
|
||||
candidate.DrawSequence,
|
||||
candidate.Set,
|
||||
ExpectedId(in projection),
|
||||
candidate.MeshRefIndex,
|
||||
candidate.TupleLandblockId,
|
||||
candidate.CacheLandblockId,
|
||||
projection.EntityId,
|
||||
projection.ServerGuid,
|
||||
projection.SourceId,
|
||||
projection.ParentCellId,
|
||||
projection.EffectCellId,
|
||||
projection.Flags,
|
||||
projection.Transform,
|
||||
projection.Geometry,
|
||||
projection.Appearance,
|
||||
candidate.GfxObjId,
|
||||
candidate.PartTransform,
|
||||
candidate.SurfaceOverrides));
|
||||
}
|
||||
|
||||
ReadOnlySpan<RenderFrameCandidateRange> ranges = view.RouteRanges;
|
||||
ReadOnlySpan<RenderProjectionRecord> routeCandidates =
|
||||
view.RouteCandidates;
|
||||
if (_packedActual.Capacity < view.MeshParts.Length)
|
||||
_packedActual.Capacity = view.MeshParts.Length;
|
||||
for (int drawSequence = 0;
|
||||
drawSequence < ranges.Length;
|
||||
drawSequence++)
|
||||
{
|
||||
RenderFrameCandidateRange range = ranges[drawSequence];
|
||||
int end = checked(range.Offset + range.Count);
|
||||
for (int candidateIndex = range.Offset;
|
||||
candidateIndex < end;
|
||||
candidateIndex++)
|
||||
{
|
||||
RenderProjectionRecord projection =
|
||||
routeCandidates[candidateIndex];
|
||||
if ((projection.Flags & RenderProjectionFlags.Draw) == 0)
|
||||
continue;
|
||||
|
||||
IReadOnlyList<AcDream.Core.World.MeshRef>? meshRefs =
|
||||
projection.EntityPayload.MeshRefs;
|
||||
int meshCount = meshRefs?.Count ?? 0;
|
||||
for (int partIndex = 0;
|
||||
partIndex < meshCount;
|
||||
partIndex++)
|
||||
{
|
||||
AcDream.Core.World.MeshRef meshRef =
|
||||
meshRefs![partIndex];
|
||||
_packedActual.Add(new PackedInputKey(
|
||||
drawSequence,
|
||||
WbDrawDispatcher.EntitySet.All,
|
||||
projection.Id,
|
||||
partIndex,
|
||||
tupleLandblockId,
|
||||
DispatcherCacheLandblock(
|
||||
in projection,
|
||||
tupleLandblockId),
|
||||
projection.Source.LocalEntityId,
|
||||
projection.Source.ServerGuid,
|
||||
projection.Source.SourceId,
|
||||
projection.Source.ParentCellId,
|
||||
projection.Source.EffectCellId,
|
||||
projection.Source.CurrentProjectionFlags,
|
||||
projection.Source.TransformFingerprint,
|
||||
projection.Source.GeometryFingerprint,
|
||||
projection.Source.AppearanceFingerprint,
|
||||
meshRef.GfxObjId,
|
||||
meshRef.PartTransform,
|
||||
CurrentRenderSceneOracle
|
||||
.CreateSurfaceOverrideFingerprint(
|
||||
meshRef.SurfaceOverrides)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_packedExpected.Sort(PackedInputKeyComparer.Instance);
|
||||
_packedActual.Sort(PackedInputKeyComparer.Instance);
|
||||
RenderSceneHash128 expectedDigest =
|
||||
BuildPackedInputDigest(_packedExpected);
|
||||
RenderSceneHash128 actualDigest =
|
||||
BuildPackedInputDigest(_packedActual);
|
||||
string? mismatch = ComparePackedInputKeys(ranges.Length);
|
||||
if (mismatch is null)
|
||||
{
|
||||
_packedInputSuccessfulComparisonCount =
|
||||
checked(_packedInputSuccessfulComparisonCount + 1);
|
||||
}
|
||||
else
|
||||
{
|
||||
_packedInputMismatchCount =
|
||||
checked(_packedInputMismatchCount + 1);
|
||||
if (_packedInputFirstMismatch is null)
|
||||
{
|
||||
_packedInputFirstMismatch = mismatch;
|
||||
_log?.Invoke(
|
||||
"[render-packed-input] first mismatch: " + mismatch);
|
||||
}
|
||||
}
|
||||
|
||||
return new PackedInputComparison(expectedDigest, actualDigest);
|
||||
}
|
||||
|
||||
private static uint DispatcherCacheLandblock(
|
||||
in RenderProjectionRecord projection,
|
||||
uint tupleLandblockId) =>
|
||||
projection.Source.ParentCellId != 0
|
||||
? (projection.Source.ParentCellId & 0xFFFF0000u) | 0xFFFFu
|
||||
: tupleLandblockId;
|
||||
|
||||
private string? ComparePackedInputKeys(int actualDrawCount)
|
||||
{
|
||||
if (_current.Snapshot.DispatcherDrawCount != actualDrawCount)
|
||||
{
|
||||
return $"field=drawCount expected="
|
||||
+ $"{_current.Snapshot.DispatcherDrawCount} "
|
||||
+ $"actual={actualDrawCount}";
|
||||
}
|
||||
if (_packedExpected.Count != _packedActual.Count)
|
||||
{
|
||||
return $"field=count expected={_packedExpected.Count} "
|
||||
+ $"actual={_packedActual.Count}";
|
||||
}
|
||||
|
||||
for (int index = 0; index < _packedExpected.Count; index++)
|
||||
{
|
||||
if (_packedExpected[index] != _packedActual[index])
|
||||
{
|
||||
return $"field=meshPart index={index} "
|
||||
+ $"expected={_packedExpected[index]} "
|
||||
+ $"actual={_packedActual[index]}";
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static RenderSceneHash128 BuildPackedInputDigest(
|
||||
List<PackedInputKey> candidates)
|
||||
{
|
||||
StableRenderHash128 hash = StableRenderHash128.Create();
|
||||
hash.Add(candidates.Count);
|
||||
for (int index = 0; index < candidates.Count; index++)
|
||||
candidates[index].AddTo(ref hash);
|
||||
return hash.Finish();
|
||||
}
|
||||
|
||||
private string? CompareKeys()
|
||||
{
|
||||
if (_expected.Count != _actual.Count)
|
||||
|
|
@ -283,6 +488,57 @@ internal sealed class RenderScenePViewFrameProductController :
|
|||
uint CellId,
|
||||
RenderProjectionId ProjectionId);
|
||||
|
||||
private readonly record struct PackedInputComparison(
|
||||
RenderSceneHash128 ExpectedDigest,
|
||||
RenderSceneHash128 ActualDigest);
|
||||
|
||||
private readonly record struct PackedInputKey(
|
||||
int DrawSequence,
|
||||
WbDrawDispatcher.EntitySet Set,
|
||||
RenderProjectionId ProjectionId,
|
||||
int MeshRefIndex,
|
||||
uint TupleLandblockId,
|
||||
uint CacheLandblockId,
|
||||
uint LocalEntityId,
|
||||
uint ServerGuid,
|
||||
uint SourceId,
|
||||
uint ParentCellId,
|
||||
uint EffectCellId,
|
||||
uint SourceFlags,
|
||||
RenderSceneHash128 Transform,
|
||||
RenderSceneHash128 Geometry,
|
||||
RenderSceneHash128 Appearance,
|
||||
uint GfxObjId,
|
||||
Matrix4x4 PartTransform,
|
||||
RenderSceneHash128 SurfaceOverrides)
|
||||
{
|
||||
public void AddTo(ref StableRenderHash128 hash)
|
||||
{
|
||||
hash.Add(DrawSequence);
|
||||
hash.Add((int)Set);
|
||||
ProjectionId.AddTo(ref hash);
|
||||
hash.Add(MeshRefIndex);
|
||||
hash.Add(TupleLandblockId);
|
||||
hash.Add(CacheLandblockId);
|
||||
hash.Add(LocalEntityId);
|
||||
hash.Add(ServerGuid);
|
||||
hash.Add(SourceId);
|
||||
hash.Add(ParentCellId);
|
||||
hash.Add(EffectCellId);
|
||||
hash.Add(SourceFlags);
|
||||
hash.Add(Transform.Low);
|
||||
hash.Add(Transform.High);
|
||||
hash.Add(Geometry.Low);
|
||||
hash.Add(Geometry.High);
|
||||
hash.Add(Appearance.Low);
|
||||
hash.Add(Appearance.High);
|
||||
hash.Add(GfxObjId);
|
||||
hash.Add(PartTransform);
|
||||
hash.Add(SurfaceOverrides.Low);
|
||||
hash.Add(SurfaceOverrides.High);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class CandidateKeyComparer : IComparer<CandidateKey>
|
||||
{
|
||||
public static CandidateKeyComparer Instance { get; } = new();
|
||||
|
|
@ -302,6 +558,25 @@ internal sealed class RenderScenePViewFrameProductController :
|
|||
return left.ProjectionId.CompareTo(right.ProjectionId);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class PackedInputKeyComparer :
|
||||
IComparer<PackedInputKey>
|
||||
{
|
||||
public static PackedInputKeyComparer Instance { get; } = new();
|
||||
|
||||
private PackedInputKeyComparer()
|
||||
{
|
||||
}
|
||||
|
||||
public int Compare(PackedInputKey left, PackedInputKey right)
|
||||
{
|
||||
int value = left.DrawSequence.CompareTo(right.DrawSequence);
|
||||
if (value != 0) return value;
|
||||
value = left.ProjectionId.CompareTo(right.ProjectionId);
|
||||
if (value != 0) return value;
|
||||
return left.MeshRefIndex.CompareTo(right.MeshRefIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -313,7 +588,7 @@ internal sealed class RenderScenePViewFrameBuilder
|
|||
{
|
||||
private const byte EnvCellProjectionDomain = 2;
|
||||
|
||||
private readonly HashSet<RenderProjectionId> _transformIds = [];
|
||||
private readonly HashSet<RenderProjectionId> _projectionIds = [];
|
||||
private RenderProjectionRecord[] _outdoor = [];
|
||||
private RenderProjectionRecord[] _dynamics = [];
|
||||
private RenderProjectionRecord[] _cell = [];
|
||||
|
|
@ -338,7 +613,7 @@ internal sealed class RenderScenePViewFrameBuilder
|
|||
input.ClipAssembly);
|
||||
RenderSceneDigest digest = input.SourceDigest;
|
||||
writer.SetSourceDigest(in digest);
|
||||
_transformIds.Clear();
|
||||
_projectionIds.Clear();
|
||||
LoadSceneIndices(input.Scene);
|
||||
|
||||
BuildOutdoorRoutes(writer, in input);
|
||||
|
|
@ -398,7 +673,10 @@ internal sealed class RenderScenePViewFrameBuilder
|
|||
|
||||
_survivors[count++] = record;
|
||||
writer.AddOutdoor(in record);
|
||||
AddTransform(writer, in record);
|
||||
AddProjection(
|
||||
writer,
|
||||
in record,
|
||||
input.AnimatedEntityIds);
|
||||
}
|
||||
|
||||
writer.AddRouteRange(
|
||||
|
|
@ -429,7 +707,10 @@ internal sealed class RenderScenePViewFrameBuilder
|
|||
continue;
|
||||
|
||||
for (int index = 0; index < count; index++)
|
||||
AddTransform(writer, in _cell[index]);
|
||||
AddProjection(
|
||||
writer,
|
||||
in _cell[index],
|
||||
input.AnimatedEntityIds);
|
||||
writer.AddRouteRange(
|
||||
RenderFrameCandidateRoute.LookInObject,
|
||||
i,
|
||||
|
|
@ -471,7 +752,10 @@ internal sealed class RenderScenePViewFrameBuilder
|
|||
|
||||
_survivors[count++] = record;
|
||||
writer.AddDynamic(in record);
|
||||
AddTransform(writer, in record);
|
||||
AddProjection(
|
||||
writer,
|
||||
in record,
|
||||
input.AnimatedEntityIds);
|
||||
}
|
||||
|
||||
writer.AddRouteRange(
|
||||
|
|
@ -512,7 +796,10 @@ internal sealed class RenderScenePViewFrameBuilder
|
|||
}
|
||||
|
||||
_cell[count++] = record;
|
||||
AddTransform(writer, in record);
|
||||
AddProjection(
|
||||
writer,
|
||||
in record,
|
||||
input.AnimatedEntityIds);
|
||||
}
|
||||
|
||||
writer.AddCellRange(
|
||||
|
|
@ -563,7 +850,10 @@ internal sealed class RenderScenePViewFrameBuilder
|
|||
|
||||
_survivors[count++] = record;
|
||||
writer.AddDynamic(in record);
|
||||
AddTransform(writer, in record);
|
||||
AddProjection(
|
||||
writer,
|
||||
in record,
|
||||
input.AnimatedEntityIds);
|
||||
}
|
||||
|
||||
writer.AddRouteRange(
|
||||
|
|
@ -598,16 +888,20 @@ internal sealed class RenderScenePViewFrameBuilder
|
|||
return CompactAndSort(_cell, count);
|
||||
}
|
||||
|
||||
private void AddTransform(
|
||||
private void AddProjection(
|
||||
RenderFrameWriter writer,
|
||||
in RenderProjectionRecord record)
|
||||
in RenderProjectionRecord record,
|
||||
HashSet<uint>? animatedEntityIds)
|
||||
{
|
||||
if (!_transformIds.Add(record.Id))
|
||||
if (!_projectionIds.Add(record.Id))
|
||||
return;
|
||||
writer.AddTransform(new RenderFrameTransformRecord(
|
||||
record.Id,
|
||||
-1,
|
||||
record.Transform.LocalToWorld));
|
||||
writer.AddEntityCandidate(
|
||||
in record,
|
||||
animatedEntityIds?.Contains(record.Source.LocalEntityId) == true);
|
||||
}
|
||||
|
||||
private static int CompactAndSort(
|
||||
|
|
|
|||
|
|
@ -105,6 +105,18 @@ internal sealed class StaticRenderProjectionJournal :
|
|||
PreviousTransform = new PreviousRenderTransform(
|
||||
retained.Record.Transform.LocalToWorld),
|
||||
};
|
||||
if (accepted.Source.GeometryFingerprint
|
||||
== retained.Record.Source.GeometryFingerprint
|
||||
&& accepted.Source.AppearanceFingerprint
|
||||
== retained.Record.Source.AppearanceFingerprint
|
||||
&& accepted.EntityPayload.IsBuildingShell
|
||||
== retained.Record.EntityPayload.IsBuildingShell)
|
||||
{
|
||||
accepted = accepted with
|
||||
{
|
||||
EntityPayload = retained.Record.EntityPayload,
|
||||
};
|
||||
}
|
||||
if (accepted.ProjectionClass
|
||||
!= retained.Record.ProjectionClass)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -406,6 +406,41 @@ public sealed class CurrentRenderSceneOracleTests
|
|||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SurfaceOverrideFingerprint_IsOrderIndependentAndContentExact()
|
||||
{
|
||||
var first = new Dictionary<uint, uint>
|
||||
{
|
||||
[0x0800_0002] = 0x0500_0012,
|
||||
[0x0800_0001] = 0x0500_0011,
|
||||
};
|
||||
var reordered = new Dictionary<uint, uint>
|
||||
{
|
||||
[0x0800_0001] = 0x0500_0011,
|
||||
[0x0800_0002] = 0x0500_0012,
|
||||
};
|
||||
var changed = new Dictionary<uint, uint>
|
||||
{
|
||||
[0x0800_0001] = 0x0500_0011,
|
||||
[0x0800_0002] = 0x0500_0013,
|
||||
};
|
||||
|
||||
RenderSceneHash128 expected =
|
||||
CurrentRenderSceneOracle.CreateSurfaceOverrideFingerprint(first);
|
||||
|
||||
Assert.Equal(
|
||||
expected,
|
||||
CurrentRenderSceneOracle.CreateSurfaceOverrideFingerprint(
|
||||
reordered));
|
||||
Assert.NotEqual(
|
||||
expected,
|
||||
CurrentRenderSceneOracle.CreateSurfaceOverrideFingerprint(
|
||||
changed));
|
||||
Assert.NotEqual(
|
||||
expected,
|
||||
CurrentRenderSceneOracle.CreateSurfaceOverrideFingerprint(null));
|
||||
}
|
||||
|
||||
private static CurrentRenderProjectionFingerprint CaptureSingle(
|
||||
WorldEntity entity,
|
||||
uint landblockId,
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System.Numerics;
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.Core.Selection;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
||||
|
|
@ -26,6 +27,7 @@ public sealed class RenderFrameProductTests
|
|||
dynamic.Id,
|
||||
0,
|
||||
Matrix4x4.CreateTranslation(1, 2, 3)));
|
||||
writer.AddEntityCandidate(in dynamic, animated: true);
|
||||
writer.AddClassification(new RenderFrameClassificationRecord(
|
||||
outdoor.Id,
|
||||
0,
|
||||
|
|
@ -85,8 +87,16 @@ public sealed class RenderFrameProductTests
|
|||
1),
|
||||
view.RouteRanges[0]);
|
||||
Assert.Equal(
|
||||
new RenderFrameDiagnosticCounts(1, 2, 1, 1, 1, 1, 1, 1, 1),
|
||||
new RenderFrameDiagnosticCounts(
|
||||
1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1),
|
||||
view.DiagnosticCounts);
|
||||
RenderFrameEntityCandidate packed = Assert.Single(
|
||||
view.EntityCandidates.ToArray());
|
||||
Assert.Equal(dynamic.Id, packed.Projection.Id);
|
||||
Assert.True(packed.Animated);
|
||||
RenderFrameMeshPart part = Assert.Single(view.MeshParts.ToArray());
|
||||
Assert.Equal(dynamic.Id, part.ProjectionId);
|
||||
Assert.Equal(0, part.PartIndex);
|
||||
Assert.Equal(digest, view.SourceDigest);
|
||||
|
||||
exchange.Release(in view);
|
||||
|
|
@ -231,6 +241,7 @@ public sealed class RenderFrameProductTests
|
|||
record.Id,
|
||||
0,
|
||||
Matrix4x4.Identity));
|
||||
writer.AddEntityCandidate(in record, animated: false);
|
||||
writer.AddClassification(new RenderFrameClassificationRecord(
|
||||
record.Id,
|
||||
0,
|
||||
|
|
@ -277,6 +288,10 @@ public sealed class RenderFrameProductTests
|
|||
Transform = new RenderTransform(Matrix4x4.Identity),
|
||||
PreviousTransform = new PreviousRenderTransform(Matrix4x4.Identity),
|
||||
Flags = RenderProjectionFlags.Draw,
|
||||
EntityPayload = new RenderEntityPayload(
|
||||
[new MeshRef((uint)value, Matrix4x4.Identity)],
|
||||
PaletteOverride: null,
|
||||
IsBuildingShell: false),
|
||||
};
|
||||
|
||||
private static RetailSelectionMesh SelectionMesh() =>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System.Numerics;
|
|||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Rendering.Scene.Arch;
|
||||
using AcDream.App.Rendering.Wb;
|
||||
using AcDream.Core.World;
|
||||
|
||||
namespace AcDream.App.Tests.Rendering;
|
||||
|
|
@ -81,6 +82,7 @@ public sealed class RenderScenePViewFrameProductTests
|
|||
[],
|
||||
[Cell],
|
||||
EmptyCellSource.Instance,
|
||||
[],
|
||||
RootIsOutdoor: true);
|
||||
|
||||
builder.Build(exchange, frameSequence: 1, in input);
|
||||
|
|
@ -115,8 +117,12 @@ public sealed class RenderScenePViewFrameProductTests
|
|||
AlphaClassificationCount: 0,
|
||||
LightSetCount: 0,
|
||||
SelectionPartCount: 0,
|
||||
RouteCandidateCount: 5),
|
||||
RouteCandidateCount: 5,
|
||||
EntityCandidateCount: 5,
|
||||
MeshPartCount: 5),
|
||||
view.DiagnosticCounts);
|
||||
Assert.Equal(5, view.EntityCandidates.Length);
|
||||
Assert.Equal(5, view.MeshParts.Length);
|
||||
Assert.Same(portal, view.PortalFrame);
|
||||
Assert.Same(clip, view.ClipAssembly);
|
||||
Assert.Equal(digest, view.SourceDigest);
|
||||
|
|
@ -162,6 +168,22 @@ public sealed class RenderScenePViewFrameProductTests
|
|||
0,
|
||||
[outdoorDynamic, cellDynamic]);
|
||||
oracle.CompletePViewFrame();
|
||||
oracle.BeginDispatcherFrame();
|
||||
oracle.ObserveDispatcherDraw(
|
||||
WbDrawDispatcher.EntitySet.All,
|
||||
entitiesWalked: 1,
|
||||
[(outdoor, 0, Landblock)]);
|
||||
oracle.ObserveDispatcherDraw(
|
||||
WbDrawDispatcher.EntitySet.All,
|
||||
entitiesWalked: 1,
|
||||
[(cellStatic, 0, Landblock)]);
|
||||
oracle.ObserveDispatcherDraw(
|
||||
WbDrawDispatcher.EntitySet.All,
|
||||
entitiesWalked: 2,
|
||||
[
|
||||
(outdoorDynamic, 0, Landblock),
|
||||
(cellDynamic, 0, Landblock),
|
||||
]);
|
||||
|
||||
using var shadow = new RenderSceneShadowRuntime(Generation);
|
||||
foreach (WorldEntity entity in entities)
|
||||
|
|
@ -187,6 +209,8 @@ public sealed class RenderScenePViewFrameProductTests
|
|||
[],
|
||||
[Cell],
|
||||
EmptyCellSource.Instance,
|
||||
[],
|
||||
Landblock,
|
||||
rootIsOutdoor: true);
|
||||
|
||||
RenderFrameProductComparisonSnapshot matched = controller.Snapshot;
|
||||
|
|
@ -198,6 +222,15 @@ public sealed class RenderScenePViewFrameProductTests
|
|||
Assert.Equal(0, matched.MismatchCount);
|
||||
Assert.Null(matched.FirstMismatch);
|
||||
Assert.Equal(matched.ExpectedDigest, matched.ActualDigest);
|
||||
Assert.Equal(1uL, matched.PackedInputComparisonCount);
|
||||
Assert.Equal(1uL, matched.PackedInputSuccessfulComparisonCount);
|
||||
Assert.Equal(4, matched.PackedInputExpectedCount);
|
||||
Assert.Equal(4, matched.PackedInputActualCount);
|
||||
Assert.Equal(0, matched.PackedInputMismatchCount);
|
||||
Assert.Null(matched.PackedInputFirstMismatch);
|
||||
Assert.Equal(
|
||||
matched.PackedInputExpectedDigest,
|
||||
matched.PackedInputActualDigest);
|
||||
Assert.Empty(logs);
|
||||
|
||||
RenderProjectionId withdrawnId =
|
||||
|
|
@ -213,6 +246,8 @@ public sealed class RenderScenePViewFrameProductTests
|
|||
[],
|
||||
[Cell],
|
||||
EmptyCellSource.Instance,
|
||||
[],
|
||||
Landblock,
|
||||
rootIsOutdoor: true);
|
||||
|
||||
RenderFrameProductComparisonSnapshot mismatch = controller.Snapshot;
|
||||
|
|
@ -220,7 +255,11 @@ public sealed class RenderScenePViewFrameProductTests
|
|||
Assert.Equal(1uL, mismatch.SuccessfulComparisonCount);
|
||||
Assert.Equal(1, mismatch.MismatchCount);
|
||||
Assert.Contains("field=count expected=4 actual=3", mismatch.FirstMismatch);
|
||||
Assert.Single(logs);
|
||||
Assert.Equal(1, mismatch.PackedInputMismatchCount);
|
||||
Assert.Contains(
|
||||
"field=count expected=4 actual=3",
|
||||
mismatch.PackedInputFirstMismatch);
|
||||
Assert.Equal(2, logs.Count);
|
||||
}
|
||||
|
||||
private static RenderProjectionRecord Project(WorldEntity entity)
|
||||
|
|
@ -286,7 +325,11 @@ public sealed class RenderScenePViewFrameProductTests
|
|||
BuildingShellAnchorCellId: 0,
|
||||
TransformFingerprint: default,
|
||||
GeometryFingerprint: default,
|
||||
AppearanceFingerprint: default));
|
||||
AppearanceFingerprint: default),
|
||||
new RenderEntityPayload(
|
||||
[new MeshRef((uint)id, Matrix4x4.Identity)],
|
||||
PaletteOverride: null,
|
||||
IsBuildingShell: false));
|
||||
}
|
||||
|
||||
private static WorldEntity Entity(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue