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`. |
|
| 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 | 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`. |
|
| 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. |
|
| G3–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
|
||||||
|
|
|
||||||
|
|
@ -257,6 +257,8 @@ public sealed class RetailPViewRenderer
|
||||||
_lookInFrames,
|
_lookInFrames,
|
||||||
drawableCells,
|
drawableCells,
|
||||||
ctx.Cells,
|
ctx.Cells,
|
||||||
|
ctx.AnimatedEntityIds,
|
||||||
|
ctx.PlayerLandblockId ?? 0,
|
||||||
ctx.RootCell.IsOutdoorNode);
|
ctx.RootCell.IsOutdoorNode);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -430,6 +430,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
_world.Set(entry.Entity, record.Material);
|
_world.Set(entry.Entity, record.Material);
|
||||||
_world.Set(entry.Entity, record.DegradeState);
|
_world.Set(entry.Entity, record.DegradeState);
|
||||||
_world.Set(entry.Entity, record.Source);
|
_world.Set(entry.Entity, record.Source);
|
||||||
|
_world.Set(entry.Entity, record.EntityPayload);
|
||||||
OrDirty(
|
OrDirty(
|
||||||
entry.Entity,
|
entry.Entity,
|
||||||
record.Id,
|
record.Id,
|
||||||
|
|
@ -528,6 +529,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
record.OwnerIncarnation,
|
record.OwnerIncarnation,
|
||||||
record.DirtyMask,
|
record.DirtyMask,
|
||||||
record.Source,
|
record.Source,
|
||||||
|
record.EntityPayload,
|
||||||
tag);
|
tag);
|
||||||
|
|
||||||
private void WriteRecord(
|
private void WriteRecord(
|
||||||
|
|
@ -546,6 +548,7 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
_world.Set(entity, record.OwnerIncarnation);
|
_world.Set(entity, record.OwnerIncarnation);
|
||||||
_world.Set(entity, record.DirtyMask);
|
_world.Set(entity, record.DirtyMask);
|
||||||
_world.Set(entity, record.Source);
|
_world.Set(entity, record.Source);
|
||||||
|
_world.Set(entity, record.EntityPayload);
|
||||||
}
|
}
|
||||||
|
|
||||||
private RenderProjectionRecord ReadRecord(in SceneEntry entry) =>
|
private RenderProjectionRecord ReadRecord(in SceneEntry entry) =>
|
||||||
|
|
@ -563,7 +566,8 @@ internal sealed class ArchRenderScene : IRenderScene, IRenderSceneQuerySource
|
||||||
_world.Get<RenderDegradeState>(entry.Entity),
|
_world.Get<RenderDegradeState>(entry.Entity),
|
||||||
_world.Get<RenderSortKey>(entry.Entity),
|
_world.Get<RenderSortKey>(entry.Entity),
|
||||||
_world.Get<RenderDirtyMask>(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)
|
private void Destroy(in SceneEntry entry)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,10 @@ internal readonly record struct CurrentRenderDispatcherFingerprint(
|
||||||
int MeshRefIndex,
|
int MeshRefIndex,
|
||||||
uint TupleLandblockId,
|
uint TupleLandblockId,
|
||||||
uint CacheLandblockId,
|
uint CacheLandblockId,
|
||||||
CurrentRenderProjectionFingerprint Projection);
|
CurrentRenderProjectionFingerprint Projection,
|
||||||
|
uint GfxObjId,
|
||||||
|
Matrix4x4 PartTransform,
|
||||||
|
RenderSceneHash128 SurfaceOverrides);
|
||||||
|
|
||||||
internal readonly record struct CurrentRenderDispatcherSubmission(
|
internal readonly record struct CurrentRenderDispatcherSubmission(
|
||||||
int VisibleInstanceCount,
|
int VisibleInstanceCount,
|
||||||
|
|
@ -535,7 +538,13 @@ internal sealed class CurrentRenderSceneOracle :
|
||||||
MeshRefIndex: tuple.MeshRefIndex,
|
MeshRefIndex: tuple.MeshRefIndex,
|
||||||
TupleLandblockId: tuple.LandblockId,
|
TupleLandblockId: tuple.LandblockId,
|
||||||
CacheLandblockId: cacheLandblockId,
|
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);
|
_dispatcherCandidates.Add(candidate);
|
||||||
AddDispatcherCandidate(ref _dispatcherHash, in candidate);
|
AddDispatcherCandidate(ref _dispatcherHash, in candidate);
|
||||||
}
|
}
|
||||||
|
|
@ -730,6 +739,7 @@ internal sealed class CurrentRenderSceneOracle :
|
||||||
MeshRef mesh = entity.MeshRefs[meshIndex];
|
MeshRef mesh = entity.MeshRefs[meshIndex];
|
||||||
geometry.Add(mesh.GfxObjId);
|
geometry.Add(mesh.GfxObjId);
|
||||||
geometry.Add(mesh.PartTransform);
|
geometry.Add(mesh.PartTransform);
|
||||||
|
AddSurfaceOverrides(ref geometry, mesh.SurfaceOverrides);
|
||||||
}
|
}
|
||||||
|
|
||||||
StableRenderHash128 appearance = StableRenderHash128.Create();
|
StableRenderHash128 appearance = StableRenderHash128.Create();
|
||||||
|
|
@ -795,6 +805,54 @@ internal sealed class CurrentRenderSceneOracle :
|
||||||
Appearance: appearance.Finish());
|
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(
|
private static RenderSceneHash128 FingerprintSelectionGeometry(
|
||||||
RetailSelectionMesh mesh)
|
RetailSelectionMesh mesh)
|
||||||
{
|
{
|
||||||
|
|
@ -854,6 +912,10 @@ internal sealed class CurrentRenderSceneOracle :
|
||||||
hash.Add(candidate.MeshRefIndex);
|
hash.Add(candidate.MeshRefIndex);
|
||||||
hash.Add(candidate.TupleLandblockId);
|
hash.Add(candidate.TupleLandblockId);
|
||||||
hash.Add(candidate.CacheLandblockId);
|
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;
|
CurrentRenderProjectionFingerprint projection = candidate.Projection;
|
||||||
AddProjection(ref hash, in projection);
|
AddProjection(ref hash, in projection);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AcDream.Core.Selection;
|
using AcDream.Core.Selection;
|
||||||
|
using AcDream.Core.World;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering.Scene;
|
namespace AcDream.App.Rendering.Scene;
|
||||||
|
|
||||||
|
|
@ -36,6 +37,17 @@ internal readonly record struct RenderFrameTransformRecord(
|
||||||
int PartIndex,
|
int PartIndex,
|
||||||
Matrix4x4 LocalToWorld);
|
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(
|
internal readonly record struct RenderFrameClassificationRecord(
|
||||||
RenderProjectionId ProjectionId,
|
RenderProjectionId ProjectionId,
|
||||||
int CandidateIndex,
|
int CandidateIndex,
|
||||||
|
|
@ -77,7 +89,9 @@ internal readonly record struct RenderFrameDiagnosticCounts(
|
||||||
int AlphaClassificationCount,
|
int AlphaClassificationCount,
|
||||||
int LightSetCount,
|
int LightSetCount,
|
||||||
int SelectionPartCount,
|
int SelectionPartCount,
|
||||||
int RouteCandidateCount);
|
int RouteCandidateCount,
|
||||||
|
int EntityCandidateCount,
|
||||||
|
int MeshPartCount);
|
||||||
|
|
||||||
/// <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:
|
||||||
|
|
@ -133,6 +147,12 @@ internal readonly struct RenderFrameView
|
||||||
public ReadOnlySpan<RenderFrameTransformRecord> Transforms =>
|
public ReadOnlySpan<RenderFrameTransformRecord> Transforms =>
|
||||||
Arena.Transforms;
|
Arena.Transforms;
|
||||||
|
|
||||||
|
public ReadOnlySpan<RenderFrameEntityCandidate> EntityCandidates =>
|
||||||
|
Arena.EntityCandidates;
|
||||||
|
|
||||||
|
public ReadOnlySpan<RenderFrameMeshPart> MeshParts =>
|
||||||
|
Arena.MeshParts;
|
||||||
|
|
||||||
public ReadOnlySpan<RenderFrameClassificationRecord> Classifications =>
|
public ReadOnlySpan<RenderFrameClassificationRecord> Classifications =>
|
||||||
Arena.Classifications;
|
Arena.Classifications;
|
||||||
|
|
||||||
|
|
@ -192,6 +212,8 @@ internal sealed class RenderFrameArena
|
||||||
private RenderFrameCellRange[] _cellRanges = [];
|
private RenderFrameCellRange[] _cellRanges = [];
|
||||||
private RenderProjectionRecord[] _dynamics = [];
|
private RenderProjectionRecord[] _dynamics = [];
|
||||||
private RenderFrameTransformRecord[] _transforms = [];
|
private RenderFrameTransformRecord[] _transforms = [];
|
||||||
|
private RenderFrameEntityCandidate[] _entityCandidates = [];
|
||||||
|
private RenderFrameMeshPart[] _meshParts = [];
|
||||||
private RenderFrameClassificationRecord[] _classifications = [];
|
private RenderFrameClassificationRecord[] _classifications = [];
|
||||||
private RenderFrameObjectLightSet[] _lightSets = [];
|
private RenderFrameObjectLightSet[] _lightSets = [];
|
||||||
private RenderFrameSelectionPart[] _selectionParts = [];
|
private RenderFrameSelectionPart[] _selectionParts = [];
|
||||||
|
|
@ -202,6 +224,8 @@ internal sealed class RenderFrameArena
|
||||||
private int _cellRangeCount;
|
private int _cellRangeCount;
|
||||||
private int _dynamicCount;
|
private int _dynamicCount;
|
||||||
private int _transformCount;
|
private int _transformCount;
|
||||||
|
private int _entityCandidateCount;
|
||||||
|
private int _meshPartCount;
|
||||||
private int _classificationCount;
|
private int _classificationCount;
|
||||||
private int _lightSetCount;
|
private int _lightSetCount;
|
||||||
private int _selectionPartCount;
|
private int _selectionPartCount;
|
||||||
|
|
@ -234,7 +258,9 @@ internal sealed class RenderFrameArena
|
||||||
_alphaClassificationCount,
|
_alphaClassificationCount,
|
||||||
_lightSetCount,
|
_lightSetCount,
|
||||||
_selectionPartCount,
|
_selectionPartCount,
|
||||||
_routeCandidateCount);
|
_routeCandidateCount,
|
||||||
|
_entityCandidateCount,
|
||||||
|
_meshPartCount);
|
||||||
|
|
||||||
internal ReadOnlySpan<RenderProjectionRecord> OutdoorStaticCandidates =>
|
internal ReadOnlySpan<RenderProjectionRecord> OutdoorStaticCandidates =>
|
||||||
_outdoor.AsSpan(0, _outdoorCount);
|
_outdoor.AsSpan(0, _outdoorCount);
|
||||||
|
|
@ -251,6 +277,12 @@ internal sealed class RenderFrameArena
|
||||||
internal ReadOnlySpan<RenderFrameTransformRecord> Transforms =>
|
internal ReadOnlySpan<RenderFrameTransformRecord> Transforms =>
|
||||||
_transforms.AsSpan(0, _transformCount);
|
_transforms.AsSpan(0, _transformCount);
|
||||||
|
|
||||||
|
internal ReadOnlySpan<RenderFrameEntityCandidate> EntityCandidates =>
|
||||||
|
_entityCandidates.AsSpan(0, _entityCandidateCount);
|
||||||
|
|
||||||
|
internal ReadOnlySpan<RenderFrameMeshPart> MeshParts =>
|
||||||
|
_meshParts.AsSpan(0, _meshPartCount);
|
||||||
|
|
||||||
internal ReadOnlySpan<RenderFrameClassificationRecord> Classifications =>
|
internal ReadOnlySpan<RenderFrameClassificationRecord> Classifications =>
|
||||||
_classifications.AsSpan(0, _classificationCount);
|
_classifications.AsSpan(0, _classificationCount);
|
||||||
|
|
||||||
|
|
@ -284,6 +316,8 @@ internal sealed class RenderFrameArena
|
||||||
_cellRangeCount = 0;
|
_cellRangeCount = 0;
|
||||||
_dynamicCount = 0;
|
_dynamicCount = 0;
|
||||||
_transformCount = 0;
|
_transformCount = 0;
|
||||||
|
_entityCandidateCount = 0;
|
||||||
|
_meshPartCount = 0;
|
||||||
_classificationCount = 0;
|
_classificationCount = 0;
|
||||||
_lightSetCount = 0;
|
_lightSetCount = 0;
|
||||||
_selectionPartCount = 0;
|
_selectionPartCount = 0;
|
||||||
|
|
@ -397,6 +431,46 @@ internal sealed class RenderFrameArena
|
||||||
_transforms[_transformCount++] = transform;
|
_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(
|
internal void AddClassification(
|
||||||
ulong epoch,
|
ulong epoch,
|
||||||
in RenderFrameClassificationRecord classification)
|
in RenderFrameClassificationRecord classification)
|
||||||
|
|
@ -529,6 +603,10 @@ internal sealed class RenderFrameArena
|
||||||
{
|
{
|
||||||
if (_selectionPartCount > 0)
|
if (_selectionPartCount > 0)
|
||||||
Array.Clear(_selectionParts, 0, _selectionPartCount);
|
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)
|
private static void EnsureCapacity<T>(ref T[] values, int required)
|
||||||
|
|
@ -603,6 +681,11 @@ internal readonly struct RenderFrameWriter
|
||||||
public void AddTransform(in RenderFrameTransformRecord transform) =>
|
public void AddTransform(in RenderFrameTransformRecord transform) =>
|
||||||
Arena.AddTransform(_epoch, in transform);
|
Arena.AddTransform(_epoch, in transform);
|
||||||
|
|
||||||
|
public void AddEntityCandidate(
|
||||||
|
in RenderProjectionRecord projection,
|
||||||
|
bool animated) =>
|
||||||
|
Arena.AddEntityCandidate(_epoch, in projection, animated);
|
||||||
|
|
||||||
public void AddClassification(
|
public void AddClassification(
|
||||||
in RenderFrameClassificationRecord classification) =>
|
in RenderFrameClassificationRecord classification) =>
|
||||||
Arena.AddClassification(_epoch, in classification);
|
Arena.AddClassification(_epoch, in classification);
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ internal sealed class RenderProjectionJournal
|
||||||
if (prior.MeshSet != current.MeshSet
|
if (prior.MeshSet != current.MeshSet
|
||||||
|| prior.Material != current.Material
|
|| prior.Material != current.Material
|
||||||
|| prior.DegradeState != current.DegradeState
|
|| prior.DegradeState != current.DegradeState
|
||||||
|
|| prior.EntityPayload != current.EntityPayload
|
||||||
|| prior.Source.GeometryFingerprint
|
|| prior.Source.GeometryFingerprint
|
||||||
!= current.Source.GeometryFingerprint
|
!= current.Source.GeometryFingerprint
|
||||||
|| prior.Source.AppearanceFingerprint
|
|| prior.Source.AppearanceFingerprint
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,11 @@ internal static class RenderProjectionRecordFactory
|
||||||
fingerprint.Transform,
|
fingerprint.Transform,
|
||||||
fingerprint.Geometry,
|
fingerprint.Geometry,
|
||||||
fingerprint.Appearance,
|
fingerprint.Appearance,
|
||||||
fingerprint.Flags));
|
fingerprint.Flags),
|
||||||
|
new RenderEntityPayload(
|
||||||
|
entity.MeshRefs,
|
||||||
|
entity.PaletteOverride,
|
||||||
|
entity.IsBuildingShell));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static (Vector3 Minimum, Vector3 Maximum) CalculateBounds(
|
private static (Vector3 Minimum, Vector3 Maximum) CalculateBounds(
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using AcDream.Core.World;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering.Scene;
|
namespace AcDream.App.Rendering.Scene;
|
||||||
|
|
||||||
|
|
@ -194,6 +195,18 @@ internal readonly record struct RenderSourceMetadata(
|
||||||
RenderSceneHash128 AppearanceFingerprint,
|
RenderSceneHash128 AppearanceFingerprint,
|
||||||
uint CurrentProjectionFlags = 0);
|
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(
|
internal readonly record struct RenderProjectionRecord(
|
||||||
RenderProjectionId Id,
|
RenderProjectionId Id,
|
||||||
RenderProjectionClass ProjectionClass,
|
RenderProjectionClass ProjectionClass,
|
||||||
|
|
@ -208,7 +221,8 @@ internal readonly record struct RenderProjectionRecord(
|
||||||
RenderDegradeState DegradeState,
|
RenderDegradeState DegradeState,
|
||||||
RenderSortKey SortKey,
|
RenderSortKey SortKey,
|
||||||
RenderDirtyMask DirtyMask,
|
RenderDirtyMask DirtyMask,
|
||||||
RenderSourceMetadata Source = default);
|
RenderSourceMetadata Source = default,
|
||||||
|
RenderEntityPayload EntityPayload = default);
|
||||||
|
|
||||||
internal enum RenderProjectionDeltaKind : byte
|
internal enum RenderProjectionDeltaKind : byte
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using AcDream.App.Rendering.Wb;
|
||||||
|
|
||||||
namespace AcDream.App.Rendering.Scene;
|
namespace AcDream.App.Rendering.Scene;
|
||||||
|
|
||||||
|
|
@ -11,6 +12,7 @@ internal readonly record struct RenderScenePViewBuildInput(
|
||||||
IReadOnlyList<PortalVisibilityFrame> LookInFrames,
|
IReadOnlyList<PortalVisibilityFrame> LookInFrames,
|
||||||
HashSet<uint> DrawableCells,
|
HashSet<uint> DrawableCells,
|
||||||
IRetailPViewCellSource Cells,
|
IRetailPViewCellSource Cells,
|
||||||
|
HashSet<uint>? AnimatedEntityIds,
|
||||||
bool RootIsOutdoor);
|
bool RootIsOutdoor);
|
||||||
|
|
||||||
internal readonly record struct RenderFrameProductComparisonSnapshot(
|
internal readonly record struct RenderFrameProductComparisonSnapshot(
|
||||||
|
|
@ -25,6 +27,14 @@ internal readonly record struct RenderFrameProductComparisonSnapshot(
|
||||||
string? FirstMismatch,
|
string? FirstMismatch,
|
||||||
RenderSceneHash128 ExpectedDigest,
|
RenderSceneHash128 ExpectedDigest,
|
||||||
RenderSceneHash128 ActualDigest,
|
RenderSceneHash128 ActualDigest,
|
||||||
|
ulong PackedInputComparisonCount,
|
||||||
|
ulong PackedInputSuccessfulComparisonCount,
|
||||||
|
int PackedInputExpectedCount,
|
||||||
|
int PackedInputActualCount,
|
||||||
|
long PackedInputMismatchCount,
|
||||||
|
string? PackedInputFirstMismatch,
|
||||||
|
RenderSceneHash128 PackedInputExpectedDigest,
|
||||||
|
RenderSceneHash128 PackedInputActualDigest,
|
||||||
RenderFrameDiagnosticCounts ProductCounts)
|
RenderFrameDiagnosticCounts ProductCounts)
|
||||||
{
|
{
|
||||||
public static RenderFrameProductComparisonSnapshot Disabled { get; } =
|
public static RenderFrameProductComparisonSnapshot Disabled { get; } =
|
||||||
|
|
@ -40,6 +50,14 @@ internal readonly record struct RenderFrameProductComparisonSnapshot(
|
||||||
FirstMismatch: null,
|
FirstMismatch: null,
|
||||||
ExpectedDigest: RenderSceneHash128.Empty,
|
ExpectedDigest: RenderSceneHash128.Empty,
|
||||||
ActualDigest: 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);
|
ProductCounts: default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,12 +81,18 @@ internal sealed class RenderScenePViewFrameProductController :
|
||||||
private readonly RenderFrameExchange _exchange = new();
|
private readonly RenderFrameExchange _exchange = new();
|
||||||
private readonly List<CandidateKey> _expected = [];
|
private readonly List<CandidateKey> _expected = [];
|
||||||
private readonly List<CandidateKey> _actual = [];
|
private readonly List<CandidateKey> _actual = [];
|
||||||
|
private readonly List<PackedInputKey> _packedExpected = [];
|
||||||
|
private readonly List<PackedInputKey> _packedActual = [];
|
||||||
private readonly Action<string>? _log;
|
private readonly Action<string>? _log;
|
||||||
private ulong _productFrameSequence;
|
private ulong _productFrameSequence;
|
||||||
private ulong _comparisonCount;
|
private ulong _comparisonCount;
|
||||||
private ulong _successfulComparisonCount;
|
private ulong _successfulComparisonCount;
|
||||||
private long _mismatchCount;
|
private long _mismatchCount;
|
||||||
private string? _firstMismatch;
|
private string? _firstMismatch;
|
||||||
|
private ulong _packedInputComparisonCount;
|
||||||
|
private ulong _packedInputSuccessfulComparisonCount;
|
||||||
|
private long _packedInputMismatchCount;
|
||||||
|
private string? _packedInputFirstMismatch;
|
||||||
|
|
||||||
public RenderScenePViewFrameProductController(
|
public RenderScenePViewFrameProductController(
|
||||||
RenderSceneShadowRuntime shadow,
|
RenderSceneShadowRuntime shadow,
|
||||||
|
|
@ -93,6 +117,8 @@ internal sealed class RenderScenePViewFrameProductController :
|
||||||
IReadOnlyList<PortalVisibilityFrame> lookInFrames,
|
IReadOnlyList<PortalVisibilityFrame> lookInFrames,
|
||||||
HashSet<uint> drawableCells,
|
HashSet<uint> drawableCells,
|
||||||
IRetailPViewCellSource cells,
|
IRetailPViewCellSource cells,
|
||||||
|
HashSet<uint>? animatedEntityIds,
|
||||||
|
uint tupleLandblockId,
|
||||||
bool rootIsOutdoor)
|
bool rootIsOutdoor)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(portalFrame);
|
ArgumentNullException.ThrowIfNull(portalFrame);
|
||||||
|
|
@ -113,6 +139,7 @@ internal sealed class RenderScenePViewFrameProductController :
|
||||||
lookInFrames,
|
lookInFrames,
|
||||||
drawableCells,
|
drawableCells,
|
||||||
cells,
|
cells,
|
||||||
|
animatedEntityIds,
|
||||||
rootIsOutdoor);
|
rootIsOutdoor);
|
||||||
_builder.Build(_exchange, frameSequence, in input);
|
_builder.Build(_exchange, frameSequence, in input);
|
||||||
RenderFrameView view = _exchange.BorrowLatest(
|
RenderFrameView view = _exchange.BorrowLatest(
|
||||||
|
|
@ -120,7 +147,7 @@ internal sealed class RenderScenePViewFrameProductController :
|
||||||
frameSequence);
|
frameSequence);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
Compare(in view);
|
Compare(in view, tupleLandblockId);
|
||||||
}
|
}
|
||||||
finally
|
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);
|
_comparisonCount = checked(_comparisonCount + 1);
|
||||||
_expected.Clear();
|
_expected.Clear();
|
||||||
|
|
@ -196,6 +223,9 @@ internal sealed class RenderScenePViewFrameProductController :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PackedInputComparison packed =
|
||||||
|
ComparePackedInput(in view, tupleLandblockId);
|
||||||
|
|
||||||
Snapshot = new RenderFrameProductComparisonSnapshot(
|
Snapshot = new RenderFrameProductComparisonSnapshot(
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
ProductFrameSequence: view.FrameSequence,
|
ProductFrameSequence: view.FrameSequence,
|
||||||
|
|
@ -209,9 +239,184 @@ internal sealed class RenderScenePViewFrameProductController :
|
||||||
FirstMismatch: _firstMismatch,
|
FirstMismatch: _firstMismatch,
|
||||||
ExpectedDigest: expectedDigest,
|
ExpectedDigest: expectedDigest,
|
||||||
ActualDigest: actualDigest,
|
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);
|
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()
|
private string? CompareKeys()
|
||||||
{
|
{
|
||||||
if (_expected.Count != _actual.Count)
|
if (_expected.Count != _actual.Count)
|
||||||
|
|
@ -283,6 +488,57 @@ internal sealed class RenderScenePViewFrameProductController :
|
||||||
uint CellId,
|
uint CellId,
|
||||||
RenderProjectionId ProjectionId);
|
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>
|
private sealed class CandidateKeyComparer : IComparer<CandidateKey>
|
||||||
{
|
{
|
||||||
public static CandidateKeyComparer Instance { get; } = new();
|
public static CandidateKeyComparer Instance { get; } = new();
|
||||||
|
|
@ -302,6 +558,25 @@ internal sealed class RenderScenePViewFrameProductController :
|
||||||
return left.ProjectionId.CompareTo(right.ProjectionId);
|
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>
|
/// <summary>
|
||||||
|
|
@ -313,7 +588,7 @@ internal sealed class RenderScenePViewFrameBuilder
|
||||||
{
|
{
|
||||||
private const byte EnvCellProjectionDomain = 2;
|
private const byte EnvCellProjectionDomain = 2;
|
||||||
|
|
||||||
private readonly HashSet<RenderProjectionId> _transformIds = [];
|
private readonly HashSet<RenderProjectionId> _projectionIds = [];
|
||||||
private RenderProjectionRecord[] _outdoor = [];
|
private RenderProjectionRecord[] _outdoor = [];
|
||||||
private RenderProjectionRecord[] _dynamics = [];
|
private RenderProjectionRecord[] _dynamics = [];
|
||||||
private RenderProjectionRecord[] _cell = [];
|
private RenderProjectionRecord[] _cell = [];
|
||||||
|
|
@ -338,7 +613,7 @@ internal sealed class RenderScenePViewFrameBuilder
|
||||||
input.ClipAssembly);
|
input.ClipAssembly);
|
||||||
RenderSceneDigest digest = input.SourceDigest;
|
RenderSceneDigest digest = input.SourceDigest;
|
||||||
writer.SetSourceDigest(in digest);
|
writer.SetSourceDigest(in digest);
|
||||||
_transformIds.Clear();
|
_projectionIds.Clear();
|
||||||
LoadSceneIndices(input.Scene);
|
LoadSceneIndices(input.Scene);
|
||||||
|
|
||||||
BuildOutdoorRoutes(writer, in input);
|
BuildOutdoorRoutes(writer, in input);
|
||||||
|
|
@ -398,7 +673,10 @@ internal sealed class RenderScenePViewFrameBuilder
|
||||||
|
|
||||||
_survivors[count++] = record;
|
_survivors[count++] = record;
|
||||||
writer.AddOutdoor(in record);
|
writer.AddOutdoor(in record);
|
||||||
AddTransform(writer, in record);
|
AddProjection(
|
||||||
|
writer,
|
||||||
|
in record,
|
||||||
|
input.AnimatedEntityIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.AddRouteRange(
|
writer.AddRouteRange(
|
||||||
|
|
@ -429,7 +707,10 @@ internal sealed class RenderScenePViewFrameBuilder
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
for (int index = 0; index < count; index++)
|
for (int index = 0; index < count; index++)
|
||||||
AddTransform(writer, in _cell[index]);
|
AddProjection(
|
||||||
|
writer,
|
||||||
|
in _cell[index],
|
||||||
|
input.AnimatedEntityIds);
|
||||||
writer.AddRouteRange(
|
writer.AddRouteRange(
|
||||||
RenderFrameCandidateRoute.LookInObject,
|
RenderFrameCandidateRoute.LookInObject,
|
||||||
i,
|
i,
|
||||||
|
|
@ -471,7 +752,10 @@ internal sealed class RenderScenePViewFrameBuilder
|
||||||
|
|
||||||
_survivors[count++] = record;
|
_survivors[count++] = record;
|
||||||
writer.AddDynamic(in record);
|
writer.AddDynamic(in record);
|
||||||
AddTransform(writer, in record);
|
AddProjection(
|
||||||
|
writer,
|
||||||
|
in record,
|
||||||
|
input.AnimatedEntityIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.AddRouteRange(
|
writer.AddRouteRange(
|
||||||
|
|
@ -512,7 +796,10 @@ internal sealed class RenderScenePViewFrameBuilder
|
||||||
}
|
}
|
||||||
|
|
||||||
_cell[count++] = record;
|
_cell[count++] = record;
|
||||||
AddTransform(writer, in record);
|
AddProjection(
|
||||||
|
writer,
|
||||||
|
in record,
|
||||||
|
input.AnimatedEntityIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.AddCellRange(
|
writer.AddCellRange(
|
||||||
|
|
@ -563,7 +850,10 @@ internal sealed class RenderScenePViewFrameBuilder
|
||||||
|
|
||||||
_survivors[count++] = record;
|
_survivors[count++] = record;
|
||||||
writer.AddDynamic(in record);
|
writer.AddDynamic(in record);
|
||||||
AddTransform(writer, in record);
|
AddProjection(
|
||||||
|
writer,
|
||||||
|
in record,
|
||||||
|
input.AnimatedEntityIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.AddRouteRange(
|
writer.AddRouteRange(
|
||||||
|
|
@ -598,16 +888,20 @@ internal sealed class RenderScenePViewFrameBuilder
|
||||||
return CompactAndSort(_cell, count);
|
return CompactAndSort(_cell, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AddTransform(
|
private void AddProjection(
|
||||||
RenderFrameWriter writer,
|
RenderFrameWriter writer,
|
||||||
in RenderProjectionRecord record)
|
in RenderProjectionRecord record,
|
||||||
|
HashSet<uint>? animatedEntityIds)
|
||||||
{
|
{
|
||||||
if (!_transformIds.Add(record.Id))
|
if (!_projectionIds.Add(record.Id))
|
||||||
return;
|
return;
|
||||||
writer.AddTransform(new RenderFrameTransformRecord(
|
writer.AddTransform(new RenderFrameTransformRecord(
|
||||||
record.Id,
|
record.Id,
|
||||||
-1,
|
-1,
|
||||||
record.Transform.LocalToWorld));
|
record.Transform.LocalToWorld));
|
||||||
|
writer.AddEntityCandidate(
|
||||||
|
in record,
|
||||||
|
animatedEntityIds?.Contains(record.Source.LocalEntityId) == true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int CompactAndSort(
|
private static int CompactAndSort(
|
||||||
|
|
|
||||||
|
|
@ -105,6 +105,18 @@ internal sealed class StaticRenderProjectionJournal :
|
||||||
PreviousTransform = new PreviousRenderTransform(
|
PreviousTransform = new PreviousRenderTransform(
|
||||||
retained.Record.Transform.LocalToWorld),
|
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
|
if (accepted.ProjectionClass
|
||||||
!= retained.Record.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(
|
private static CurrentRenderProjectionFingerprint CaptureSingle(
|
||||||
WorldEntity entity,
|
WorldEntity entity,
|
||||||
uint landblockId,
|
uint landblockId,
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ using System.Numerics;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.Rendering.Scene;
|
using AcDream.App.Rendering.Scene;
|
||||||
using AcDream.Core.Selection;
|
using AcDream.Core.Selection;
|
||||||
|
using AcDream.Core.World;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Rendering;
|
namespace AcDream.App.Tests.Rendering;
|
||||||
|
|
||||||
|
|
@ -26,6 +27,7 @@ public sealed class RenderFrameProductTests
|
||||||
dynamic.Id,
|
dynamic.Id,
|
||||||
0,
|
0,
|
||||||
Matrix4x4.CreateTranslation(1, 2, 3)));
|
Matrix4x4.CreateTranslation(1, 2, 3)));
|
||||||
|
writer.AddEntityCandidate(in dynamic, animated: true);
|
||||||
writer.AddClassification(new RenderFrameClassificationRecord(
|
writer.AddClassification(new RenderFrameClassificationRecord(
|
||||||
outdoor.Id,
|
outdoor.Id,
|
||||||
0,
|
0,
|
||||||
|
|
@ -85,8 +87,16 @@ public sealed class RenderFrameProductTests
|
||||||
1),
|
1),
|
||||||
view.RouteRanges[0]);
|
view.RouteRanges[0]);
|
||||||
Assert.Equal(
|
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);
|
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);
|
Assert.Equal(digest, view.SourceDigest);
|
||||||
|
|
||||||
exchange.Release(in view);
|
exchange.Release(in view);
|
||||||
|
|
@ -231,6 +241,7 @@ public sealed class RenderFrameProductTests
|
||||||
record.Id,
|
record.Id,
|
||||||
0,
|
0,
|
||||||
Matrix4x4.Identity));
|
Matrix4x4.Identity));
|
||||||
|
writer.AddEntityCandidate(in record, animated: false);
|
||||||
writer.AddClassification(new RenderFrameClassificationRecord(
|
writer.AddClassification(new RenderFrameClassificationRecord(
|
||||||
record.Id,
|
record.Id,
|
||||||
0,
|
0,
|
||||||
|
|
@ -277,6 +288,10 @@ public sealed class RenderFrameProductTests
|
||||||
Transform = new RenderTransform(Matrix4x4.Identity),
|
Transform = new RenderTransform(Matrix4x4.Identity),
|
||||||
PreviousTransform = new PreviousRenderTransform(Matrix4x4.Identity),
|
PreviousTransform = new PreviousRenderTransform(Matrix4x4.Identity),
|
||||||
Flags = RenderProjectionFlags.Draw,
|
Flags = RenderProjectionFlags.Draw,
|
||||||
|
EntityPayload = new RenderEntityPayload(
|
||||||
|
[new MeshRef((uint)value, Matrix4x4.Identity)],
|
||||||
|
PaletteOverride: null,
|
||||||
|
IsBuildingShell: false),
|
||||||
};
|
};
|
||||||
|
|
||||||
private static RetailSelectionMesh SelectionMesh() =>
|
private static RetailSelectionMesh SelectionMesh() =>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ using System.Numerics;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.Rendering.Scene;
|
using AcDream.App.Rendering.Scene;
|
||||||
using AcDream.App.Rendering.Scene.Arch;
|
using AcDream.App.Rendering.Scene.Arch;
|
||||||
|
using AcDream.App.Rendering.Wb;
|
||||||
using AcDream.Core.World;
|
using AcDream.Core.World;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Rendering;
|
namespace AcDream.App.Tests.Rendering;
|
||||||
|
|
@ -81,6 +82,7 @@ public sealed class RenderScenePViewFrameProductTests
|
||||||
[],
|
[],
|
||||||
[Cell],
|
[Cell],
|
||||||
EmptyCellSource.Instance,
|
EmptyCellSource.Instance,
|
||||||
|
[],
|
||||||
RootIsOutdoor: true);
|
RootIsOutdoor: true);
|
||||||
|
|
||||||
builder.Build(exchange, frameSequence: 1, in input);
|
builder.Build(exchange, frameSequence: 1, in input);
|
||||||
|
|
@ -115,8 +117,12 @@ public sealed class RenderScenePViewFrameProductTests
|
||||||
AlphaClassificationCount: 0,
|
AlphaClassificationCount: 0,
|
||||||
LightSetCount: 0,
|
LightSetCount: 0,
|
||||||
SelectionPartCount: 0,
|
SelectionPartCount: 0,
|
||||||
RouteCandidateCount: 5),
|
RouteCandidateCount: 5,
|
||||||
|
EntityCandidateCount: 5,
|
||||||
|
MeshPartCount: 5),
|
||||||
view.DiagnosticCounts);
|
view.DiagnosticCounts);
|
||||||
|
Assert.Equal(5, view.EntityCandidates.Length);
|
||||||
|
Assert.Equal(5, view.MeshParts.Length);
|
||||||
Assert.Same(portal, view.PortalFrame);
|
Assert.Same(portal, view.PortalFrame);
|
||||||
Assert.Same(clip, view.ClipAssembly);
|
Assert.Same(clip, view.ClipAssembly);
|
||||||
Assert.Equal(digest, view.SourceDigest);
|
Assert.Equal(digest, view.SourceDigest);
|
||||||
|
|
@ -162,6 +168,22 @@ public sealed class RenderScenePViewFrameProductTests
|
||||||
0,
|
0,
|
||||||
[outdoorDynamic, cellDynamic]);
|
[outdoorDynamic, cellDynamic]);
|
||||||
oracle.CompletePViewFrame();
|
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);
|
using var shadow = new RenderSceneShadowRuntime(Generation);
|
||||||
foreach (WorldEntity entity in entities)
|
foreach (WorldEntity entity in entities)
|
||||||
|
|
@ -187,6 +209,8 @@ public sealed class RenderScenePViewFrameProductTests
|
||||||
[],
|
[],
|
||||||
[Cell],
|
[Cell],
|
||||||
EmptyCellSource.Instance,
|
EmptyCellSource.Instance,
|
||||||
|
[],
|
||||||
|
Landblock,
|
||||||
rootIsOutdoor: true);
|
rootIsOutdoor: true);
|
||||||
|
|
||||||
RenderFrameProductComparisonSnapshot matched = controller.Snapshot;
|
RenderFrameProductComparisonSnapshot matched = controller.Snapshot;
|
||||||
|
|
@ -198,6 +222,15 @@ public sealed class RenderScenePViewFrameProductTests
|
||||||
Assert.Equal(0, matched.MismatchCount);
|
Assert.Equal(0, matched.MismatchCount);
|
||||||
Assert.Null(matched.FirstMismatch);
|
Assert.Null(matched.FirstMismatch);
|
||||||
Assert.Equal(matched.ExpectedDigest, matched.ActualDigest);
|
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);
|
Assert.Empty(logs);
|
||||||
|
|
||||||
RenderProjectionId withdrawnId =
|
RenderProjectionId withdrawnId =
|
||||||
|
|
@ -213,6 +246,8 @@ public sealed class RenderScenePViewFrameProductTests
|
||||||
[],
|
[],
|
||||||
[Cell],
|
[Cell],
|
||||||
EmptyCellSource.Instance,
|
EmptyCellSource.Instance,
|
||||||
|
[],
|
||||||
|
Landblock,
|
||||||
rootIsOutdoor: true);
|
rootIsOutdoor: true);
|
||||||
|
|
||||||
RenderFrameProductComparisonSnapshot mismatch = controller.Snapshot;
|
RenderFrameProductComparisonSnapshot mismatch = controller.Snapshot;
|
||||||
|
|
@ -220,7 +255,11 @@ public sealed class RenderScenePViewFrameProductTests
|
||||||
Assert.Equal(1uL, mismatch.SuccessfulComparisonCount);
|
Assert.Equal(1uL, mismatch.SuccessfulComparisonCount);
|
||||||
Assert.Equal(1, mismatch.MismatchCount);
|
Assert.Equal(1, mismatch.MismatchCount);
|
||||||
Assert.Contains("field=count expected=4 actual=3", mismatch.FirstMismatch);
|
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)
|
private static RenderProjectionRecord Project(WorldEntity entity)
|
||||||
|
|
@ -286,7 +325,11 @@ public sealed class RenderScenePViewFrameProductTests
|
||||||
BuildingShellAnchorCellId: 0,
|
BuildingShellAnchorCellId: 0,
|
||||||
TransformFingerprint: default,
|
TransformFingerprint: default,
|
||||||
GeometryFingerprint: default,
|
GeometryFingerprint: default,
|
||||||
AppearanceFingerprint: default));
|
AppearanceFingerprint: default),
|
||||||
|
new RenderEntityPayload(
|
||||||
|
[new MeshRef((uint)id, Matrix4x4.Identity)],
|
||||||
|
PaletteOverride: null,
|
||||||
|
IsBuildingShell: false));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static WorldEntity Entity(
|
private static WorldEntity Entity(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue