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:
Erik 2026-07-25 00:44:58 +02:00
parent 720578592b
commit d8d9897376
13 changed files with 593 additions and 24 deletions

View file

@ -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. |
| G3G5 | pending | No production consumer has switched. |
The exact pre-F/G runtime rollback anchor remains `e7d9d6fa`. F0 is

View file

@ -257,6 +257,8 @@ public sealed class RetailPViewRenderer
_lookInFrames,
drawableCells,
ctx.Cells,
ctx.AnimatedEntityIds,
ctx.PlayerLandblockId ?? 0,
ctx.RootCell.IsOutdoorNode);
return result;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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() =>

View file

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