Capture deterministic, landblock-aware fingerprints from the accepted partition only during lifecycle automation. This gives the F/G shadow scene an independent oracle without changing normal draw decisions or adding disabled-path per-entity cost.
280 lines
9.8 KiB
C#
280 lines
9.8 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Rendering.Scene;
|
|
using AcDream.Core.World;
|
|
|
|
namespace AcDream.App.Tests.Rendering;
|
|
|
|
public sealed class CurrentRenderSceneOracleTests
|
|
{
|
|
private const uint LandblockA = 0xA9B4FFFF;
|
|
private const uint LandblockB = 0xA9B5FFFF;
|
|
private const uint CellA = 0xA9B40170;
|
|
private const uint CellB = 0xA9B50170;
|
|
|
|
[Fact]
|
|
public void DigestIsStableAcrossLandblockEnumerationOrder()
|
|
{
|
|
WorldEntity outdoorA = Entity(id: 7, serverGuid: 0, parentCell: null);
|
|
WorldEntity cellStatic = Entity(id: 8, serverGuid: 0, parentCell: CellA);
|
|
WorldEntity live = Entity(
|
|
id: 1_000_001,
|
|
serverGuid: 0x8000_0001,
|
|
parentCell: CellA);
|
|
// Static IDs are landblock-local. The same id in another landblock is
|
|
// a distinct projection and must survive the referee sort.
|
|
WorldEntity outdoorB = Entity(id: 7, serverGuid: 0, parentCell: null);
|
|
|
|
var firstEntries = new[]
|
|
{
|
|
Entry(LandblockA, outdoorA, cellStatic, live),
|
|
Entry(LandblockB, outdoorB),
|
|
};
|
|
var secondEntries = new[]
|
|
{
|
|
Entry(LandblockB, outdoorB),
|
|
Entry(LandblockA, outdoorA, cellStatic, live),
|
|
};
|
|
var visible = new HashSet<uint> { CellA };
|
|
var result = new InteriorEntityPartition.Result();
|
|
var oracle = new CurrentRenderSceneOracle();
|
|
|
|
InteriorEntityPartition.Partition(
|
|
result,
|
|
visible,
|
|
firstEntries,
|
|
oracle);
|
|
CurrentRenderSceneOracleSnapshot first = oracle.Snapshot;
|
|
|
|
InteriorEntityPartition.Partition(
|
|
result,
|
|
visible,
|
|
secondEntries,
|
|
oracle);
|
|
CurrentRenderSceneOracleSnapshot second = oracle.Snapshot;
|
|
|
|
Assert.True(first.Enabled);
|
|
Assert.Equal(4, first.ProjectionCount);
|
|
Assert.Equal(2, first.OutdoorStaticCount);
|
|
Assert.Equal(1, first.CellStaticCount);
|
|
Assert.Equal(1, first.DynamicCount);
|
|
Assert.Equal(1, first.CellBucketCount);
|
|
Assert.Equal(first.Digest, second.Digest);
|
|
Assert.Equal(first.CompletedFrameSequence + 1, second.CompletedFrameSequence);
|
|
Assert.Equal(2, oracle.Projections.Count(
|
|
projection => projection.EntityId == 7));
|
|
Assert.Contains(
|
|
oracle.Projections,
|
|
projection => projection.EntityId == 7
|
|
&& projection.LandblockId == LandblockA);
|
|
Assert.Contains(
|
|
oracle.Projections,
|
|
projection => projection.EntityId == 7
|
|
&& projection.LandblockId == LandblockB);
|
|
}
|
|
|
|
[Fact]
|
|
public void ObservedPartitionMatchesUninstrumentedProductionPath()
|
|
{
|
|
WorldEntity outdoor = Entity(id: 10, serverGuid: 0, parentCell: null);
|
|
WorldEntity cellStatic = Entity(id: 11, serverGuid: 0, parentCell: CellA);
|
|
WorldEntity dynamic = Entity(
|
|
id: 1_000_011,
|
|
serverGuid: 0x8000_0011,
|
|
parentCell: CellB);
|
|
WorldEntity hiddenStatic = Entity(
|
|
id: 12,
|
|
serverGuid: 0,
|
|
parentCell: CellB);
|
|
var entries = new[]
|
|
{
|
|
Entry(LandblockA, outdoor, cellStatic),
|
|
Entry(LandblockB, dynamic, hiddenStatic),
|
|
};
|
|
var visible = new HashSet<uint> { CellA };
|
|
var production = new InteriorEntityPartition.Result();
|
|
var observed = new InteriorEntityPartition.Result();
|
|
var oracle = new CurrentRenderSceneOracle();
|
|
|
|
InteriorEntityPartition.Partition(production, visible, entries);
|
|
InteriorEntityPartition.Partition(observed, visible, entries, oracle);
|
|
|
|
Assert.Equal(production.ByCell.Keys, observed.ByCell.Keys);
|
|
foreach (uint cell in production.ByCell.Keys)
|
|
Assert.Equal(production.ByCell[cell], observed.ByCell[cell]);
|
|
Assert.Equal(production.OutdoorStatic, observed.OutdoorStatic);
|
|
Assert.Equal(production.Dynamics, observed.Dynamics);
|
|
}
|
|
|
|
[Fact]
|
|
public void FingerprintsNameTransformGeometryAppearanceAndFlagChanges()
|
|
{
|
|
var visible = new HashSet<uint> { CellA, CellB };
|
|
|
|
CurrentRenderProjectionFingerprint baseline = CaptureSingle(
|
|
Entity(id: 20, serverGuid: 0, parentCell: CellA),
|
|
LandblockA,
|
|
visible);
|
|
CurrentRenderProjectionFingerprint transform = CaptureSingle(
|
|
Entity(
|
|
id: 20,
|
|
serverGuid: 0,
|
|
parentCell: CellA,
|
|
position: new Vector3(1, 2, 3)),
|
|
LandblockA,
|
|
visible);
|
|
CurrentRenderProjectionFingerprint geometry = CaptureSingle(
|
|
Entity(
|
|
id: 20,
|
|
serverGuid: 0,
|
|
parentCell: CellA,
|
|
meshId: 0x0100_2222),
|
|
LandblockA,
|
|
visible);
|
|
CurrentRenderProjectionFingerprint appearance = CaptureSingle(
|
|
Entity(
|
|
id: 20,
|
|
serverGuid: 0,
|
|
parentCell: CellA,
|
|
palette: new PaletteOverride(
|
|
0x0400_0001,
|
|
[new PaletteOverride.SubPaletteRange(0x0400_0002, 3, 4)])),
|
|
LandblockA,
|
|
visible);
|
|
WorldEntity hidden = Entity(id: 20, serverGuid: 0, parentCell: CellA);
|
|
hidden.IsDrawVisible = false;
|
|
CurrentRenderProjectionFingerprint flags = CaptureSingle(
|
|
hidden,
|
|
LandblockA,
|
|
visible);
|
|
CurrentRenderProjectionFingerprint cell = CaptureSingle(
|
|
Entity(id: 20, serverGuid: 0, parentCell: CellB),
|
|
LandblockB,
|
|
visible);
|
|
|
|
Assert.NotEqual(baseline.Transform, transform.Transform);
|
|
Assert.Equal(baseline.Geometry, transform.Geometry);
|
|
Assert.Equal(baseline.Appearance, transform.Appearance);
|
|
|
|
Assert.NotEqual(baseline.Geometry, geometry.Geometry);
|
|
Assert.Equal(baseline.Transform, geometry.Transform);
|
|
|
|
Assert.NotEqual(baseline.Appearance, appearance.Appearance);
|
|
Assert.NotEqual(baseline.Flags, flags.Flags);
|
|
Assert.NotEqual(baseline.ParentCellId, cell.ParentCellId);
|
|
Assert.NotEqual(baseline.LandblockId, cell.LandblockId);
|
|
}
|
|
|
|
[Fact]
|
|
public void NonFloodedStaticIsAbsentButNonFloodedDynamicRemains()
|
|
{
|
|
WorldEntity hiddenStatic = Entity(
|
|
id: 30,
|
|
serverGuid: 0,
|
|
parentCell: CellB);
|
|
WorldEntity hiddenDynamic = Entity(
|
|
id: 1_000_030,
|
|
serverGuid: 0x8000_0030,
|
|
parentCell: CellB);
|
|
var oracle = new CurrentRenderSceneOracle();
|
|
var result = new InteriorEntityPartition.Result();
|
|
|
|
InteriorEntityPartition.Partition(
|
|
result,
|
|
new HashSet<uint> { CellA },
|
|
new[] { Entry(LandblockB, hiddenStatic, hiddenDynamic) },
|
|
oracle);
|
|
|
|
Assert.Equal(1, oracle.Snapshot.ProjectionCount);
|
|
Assert.Equal(0, oracle.Snapshot.CellStaticCount);
|
|
Assert.Equal(1, oracle.Snapshot.DynamicCount);
|
|
CurrentRenderProjectionFingerprint fingerprint =
|
|
Assert.Single(oracle.Projections);
|
|
Assert.Equal(hiddenDynamic.Id, fingerprint.EntityId);
|
|
Assert.Equal(
|
|
InteriorEntityPartition.ProjectionClass.Dynamic,
|
|
fingerprint.ProjectionClass);
|
|
}
|
|
|
|
[Fact]
|
|
public void AbortPreservesLastCompleteDigestAndRecordsFailure()
|
|
{
|
|
var oracle = new CurrentRenderSceneOracle();
|
|
var result = new InteriorEntityPartition.Result();
|
|
WorldEntity entity = Entity(id: 40, serverGuid: 0, parentCell: null);
|
|
|
|
InteriorEntityPartition.Partition(
|
|
result,
|
|
[],
|
|
new[] { Entry(LandblockA, entity) },
|
|
oracle);
|
|
CurrentRenderSceneOracleSnapshot complete = oracle.Snapshot;
|
|
|
|
oracle.BeginFrame();
|
|
oracle.Observe(
|
|
LandblockA,
|
|
entity,
|
|
InteriorEntityPartition.ProjectionClass.OutdoorStatic);
|
|
oracle.AbortFrame();
|
|
|
|
Assert.Equal(complete.Digest, oracle.Snapshot.Digest);
|
|
Assert.Equal(
|
|
complete.CompletedFrameSequence,
|
|
oracle.Snapshot.CompletedFrameSequence);
|
|
Assert.Equal(complete.AbortedFrames + 1, oracle.Snapshot.AbortedFrames);
|
|
Assert.Empty(oracle.Projections);
|
|
}
|
|
|
|
private static CurrentRenderProjectionFingerprint CaptureSingle(
|
|
WorldEntity entity,
|
|
uint landblockId,
|
|
HashSet<uint> visible)
|
|
{
|
|
var oracle = new CurrentRenderSceneOracle();
|
|
var result = new InteriorEntityPartition.Result();
|
|
InteriorEntityPartition.Partition(
|
|
result,
|
|
visible,
|
|
new[] { Entry(landblockId, entity) },
|
|
oracle);
|
|
return Assert.Single(oracle.Projections);
|
|
}
|
|
|
|
private static WorldEntity Entity(
|
|
uint id,
|
|
uint serverGuid,
|
|
uint? parentCell,
|
|
Vector3? position = null,
|
|
uint meshId = 0x0100_0001,
|
|
PaletteOverride? palette = null) =>
|
|
new()
|
|
{
|
|
Id = id,
|
|
ServerGuid = serverGuid,
|
|
SourceGfxObjOrSetupId = meshId,
|
|
Position = position ?? Vector3.Zero,
|
|
Rotation = Quaternion.Identity,
|
|
MeshRefs =
|
|
[
|
|
new MeshRef(
|
|
meshId,
|
|
Matrix4x4.CreateTranslation(0.25f, 0.5f, 0.75f)),
|
|
],
|
|
ParentCellId = parentCell,
|
|
PaletteOverride = palette,
|
|
};
|
|
|
|
private static (
|
|
uint LandblockId,
|
|
Vector3 AabbMin,
|
|
Vector3 AabbMax,
|
|
IReadOnlyList<WorldEntity> Entities,
|
|
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)
|
|
Entry(uint landblockId, params WorldEntity[] entities) =>
|
|
(
|
|
landblockId,
|
|
Vector3.Zero,
|
|
Vector3.One,
|
|
entities,
|
|
null);
|
|
}
|