feat(rendering): establish current-path scene referee
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.
This commit is contained in:
parent
5ecaa5612d
commit
b2b67341ac
9 changed files with 845 additions and 5 deletions
|
|
@ -2,6 +2,7 @@ using System.Text.Json;
|
|||
using AcDream.App.Diagnostics;
|
||||
using AcDream.App.Rendering;
|
||||
using AcDream.App.Rendering.Residency;
|
||||
using AcDream.App.Rendering.Scene;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.UI.Testing;
|
||||
using SixLabors.ImageSharp;
|
||||
|
|
@ -114,6 +115,18 @@ public sealed class WorldLifecycleAutomationControllerTests
|
|||
{
|
||||
LoadedLandblocks = 1,
|
||||
WorldEntities = 42,
|
||||
RenderSceneOracle = new CurrentRenderSceneOracleSnapshot(
|
||||
Enabled: true,
|
||||
CompletedFrameSequence: 19,
|
||||
AbortedFrames: 1,
|
||||
ProjectionCount: 73,
|
||||
OutdoorStaticCount: 41,
|
||||
CellStaticCount: 20,
|
||||
DynamicCount: 12,
|
||||
CellBucketCount: 4,
|
||||
Digest: new RenderSceneHash128(
|
||||
Low: 0x0123456789ABCDEF,
|
||||
High: 0xFEDCBA9876543210)),
|
||||
TrackedGpuBytes = 1234,
|
||||
Residency = new ResidencySnapshot(
|
||||
[
|
||||
|
|
@ -166,6 +179,24 @@ public sealed class WorldLifecycleAutomationControllerTests
|
|||
Assert.True(json.RootElement.GetProperty("render").GetProperty("presentation")
|
||||
.GetProperty("screenshotCaptured").GetBoolean());
|
||||
Assert.Equal(42, json.RootElement.GetProperty("resources").GetProperty("worldEntities").GetInt32());
|
||||
JsonElement renderSceneOracle = json.RootElement
|
||||
.GetProperty("resources")
|
||||
.GetProperty("renderSceneOracle");
|
||||
Assert.True(renderSceneOracle.GetProperty("enabled").GetBoolean());
|
||||
Assert.Equal(
|
||||
19,
|
||||
renderSceneOracle.GetProperty("completedFrameSequence").GetInt64());
|
||||
Assert.Equal(
|
||||
73,
|
||||
renderSceneOracle.GetProperty("projectionCount").GetInt32());
|
||||
Assert.Equal(
|
||||
0x0123456789ABCDEFuL,
|
||||
renderSceneOracle.GetProperty("digest").GetProperty("low")
|
||||
.GetUInt64());
|
||||
Assert.Equal(
|
||||
0xFEDCBA9876543210uL,
|
||||
renderSceneOracle.GetProperty("digest").GetProperty("high")
|
||||
.GetUInt64());
|
||||
JsonElement residency = json.RootElement
|
||||
.GetProperty("resources")
|
||||
.GetProperty("residency");
|
||||
|
|
@ -304,7 +335,8 @@ public sealed class WorldLifecycleAutomationControllerTests
|
|||
directory);
|
||||
|
||||
private static WorldLifecycleResourceSnapshot EmptyResources() => new(
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0, CurrentRenderSceneOracleSnapshot.Disabled,
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
|
||||
0, 0, 0L, 0, 0L, 0L, 0L, 0L, 0, 0, 0, 0, 0, 0, 0, 0L, 0L,
|
||||
0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L,
|
||||
default, default, 0d, 0d, null);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,280 @@
|
|||
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);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue