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
|
|
@ -12,6 +12,20 @@
|
||||||
visibility, clipping, draw order, selection, lighting, animation, or
|
visibility, clipping, draw order, selection, lighting, animation, or
|
||||||
retail-faithful gameplay
|
retail-faithful gameplay
|
||||||
|
|
||||||
|
## 0. Execution ledger
|
||||||
|
|
||||||
|
| Unit | State | Evidence / rollback |
|
||||||
|
|---|---|---|
|
||||||
|
| F0a — partition-input referee | implemented; Release gate green | Diagnostic-only `CurrentRenderSceneOracle`; 3,685 App tests / 3 skips and 8,169 complete-solution tests / 5 skips. No production draw decision changed. |
|
||||||
|
| F0b — survivor/dispatcher referee | active | Extends the same current-path oracle through PView survivor and dispatcher classification boundaries. |
|
||||||
|
| F1–F5 | pending | No render-scene storage or shadow projection exists yet. |
|
||||||
|
| G0–G5 | pending | No production consumer has switched. |
|
||||||
|
|
||||||
|
The exact pre-F/G runtime rollback anchor remains `e7d9d6fa`. F0a is
|
||||||
|
non-drawing diagnostic infrastructure and therefore is not a visual-cutover
|
||||||
|
rollback unit. Each later commit that changes the production draw source is
|
||||||
|
listed in section 10 before it is offered for a connected gate.
|
||||||
|
|
||||||
## 1. Decision record
|
## 1. Decision record
|
||||||
|
|
||||||
### 1.1 Why this work has a separate approval gate
|
### 1.1 Why this work has a separate approval gate
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using AcDream.App.Diagnostics;
|
using AcDream.App.Diagnostics;
|
||||||
using AcDream.App.Input;
|
using AcDream.App.Input;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.Rendering.Scene;
|
||||||
using AcDream.App.Rendering.Vfx;
|
using AcDream.App.Rendering.Vfx;
|
||||||
using AcDream.App.Settings;
|
using AcDream.App.Settings;
|
||||||
using AcDream.App.Update;
|
using AcDream.App.Update;
|
||||||
|
|
@ -368,6 +369,11 @@ internal sealed class FrameRootCompositionPhase
|
||||||
live.SkyRenderer,
|
live.SkyRenderer,
|
||||||
content.ParticleSystem,
|
content.ParticleSystem,
|
||||||
live.ParticleRenderer);
|
live.ParticleRenderer);
|
||||||
|
CurrentRenderSceneOracle? currentRenderSceneOracle =
|
||||||
|
interaction.RetainedUi?.Screenshots is not null
|
||||||
|
&& d.Options.AutomationArtifactDirectory is not null
|
||||||
|
? new CurrentRenderSceneOracle()
|
||||||
|
: null;
|
||||||
var worldSceneRenderer = new WorldSceneRenderer(
|
var worldSceneRenderer = new WorldSceneRenderer(
|
||||||
renderFrameResources,
|
renderFrameResources,
|
||||||
renderLoginState,
|
renderLoginState,
|
||||||
|
|
@ -378,7 +384,7 @@ internal sealed class FrameRootCompositionPhase
|
||||||
d.RetailAlphaQueue,
|
d.RetailAlphaQueue,
|
||||||
d.ParticleVisibility,
|
d.ParticleVisibility,
|
||||||
new WorldScenePViewRenderer(
|
new WorldScenePViewRenderer(
|
||||||
new RetailPViewRenderer(),
|
new RetailPViewRenderer(currentRenderSceneOracle),
|
||||||
retailPViewPassExecutor,
|
retailPViewPassExecutor,
|
||||||
retailPViewPassExecutor),
|
retailPViewPassExecutor),
|
||||||
retailPViewCells,
|
retailPViewCells,
|
||||||
|
|
@ -410,7 +416,8 @@ internal sealed class FrameRootCompositionPhase
|
||||||
live.DrawDispatcher,
|
live.DrawDispatcher,
|
||||||
d.FrameProfiler,
|
d.FrameProfiler,
|
||||||
content.Dats,
|
content.Dats,
|
||||||
foundation.Residency);
|
foundation.Residency,
|
||||||
|
currentRenderSceneOracle);
|
||||||
lifecycleAutomation =
|
lifecycleAutomation =
|
||||||
new WorldLifecycleAutomationController(
|
new WorldLifecycleAutomationController(
|
||||||
() => session.WorldReveal.Snapshot,
|
() => session.WorldReveal.Snapshot,
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.Rendering.Residency;
|
using AcDream.App.Rendering.Residency;
|
||||||
|
using AcDream.App.Rendering.Scene;
|
||||||
using AcDream.App.Streaming;
|
using AcDream.App.Streaming;
|
||||||
using AcDream.App.UI.Testing;
|
using AcDream.App.UI.Testing;
|
||||||
|
|
||||||
|
|
@ -38,6 +39,7 @@ internal sealed record WorldLifecycleResourceSnapshot(
|
||||||
int TotalLandblocks,
|
int TotalLandblocks,
|
||||||
int LiveEntities,
|
int LiveEntities,
|
||||||
int MaterializedLiveEntities,
|
int MaterializedLiveEntities,
|
||||||
|
CurrentRenderSceneOracleSnapshot RenderSceneOracle,
|
||||||
int PendingLiveTeardowns,
|
int PendingLiveTeardowns,
|
||||||
int PendingLandblockRetirements,
|
int PendingLandblockRetirements,
|
||||||
int ParticleEmitters,
|
int ParticleEmitters,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.Rendering.Scene;
|
||||||
using AcDream.App.Rendering.Vfx;
|
using AcDream.App.Rendering.Vfx;
|
||||||
using AcDream.App.Rendering.Wb;
|
using AcDream.App.Rendering.Wb;
|
||||||
using AcDream.App.Rendering.Residency;
|
using AcDream.App.Rendering.Residency;
|
||||||
|
|
@ -38,6 +39,8 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
||||||
private readonly FrameProfiler _frameProfiler;
|
private readonly FrameProfiler _frameProfiler;
|
||||||
private readonly IDatReaderWriter _dats;
|
private readonly IDatReaderWriter _dats;
|
||||||
private readonly ResidencyManager _residency;
|
private readonly ResidencyManager _residency;
|
||||||
|
private readonly ICurrentRenderSceneOracleSnapshotSource?
|
||||||
|
_renderSceneOracle;
|
||||||
|
|
||||||
public WorldLifecycleResourceSnapshotSource(
|
public WorldLifecycleResourceSnapshotSource(
|
||||||
GpuWorldState world,
|
GpuWorldState world,
|
||||||
|
|
@ -55,7 +58,8 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
||||||
WbDrawDispatcher dispatcher,
|
WbDrawDispatcher dispatcher,
|
||||||
FrameProfiler frameProfiler,
|
FrameProfiler frameProfiler,
|
||||||
IDatReaderWriter dats,
|
IDatReaderWriter dats,
|
||||||
ResidencyManager residency)
|
ResidencyManager residency,
|
||||||
|
ICurrentRenderSceneOracleSnapshotSource? renderSceneOracle = null)
|
||||||
{
|
{
|
||||||
_world = world ?? throw new ArgumentNullException(nameof(world));
|
_world = world ?? throw new ArgumentNullException(nameof(world));
|
||||||
_animations = animations
|
_animations = animations
|
||||||
|
|
@ -82,6 +86,7 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
||||||
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
_dats = dats ?? throw new ArgumentNullException(nameof(dats));
|
||||||
_residency = residency
|
_residency = residency
|
||||||
?? throw new ArgumentNullException(nameof(residency));
|
?? throw new ArgumentNullException(nameof(residency));
|
||||||
|
_renderSceneOracle = renderSceneOracle;
|
||||||
}
|
}
|
||||||
|
|
||||||
public WorldLifecycleResourceSnapshot Capture(RenderFrameOutcome outcome)
|
public WorldLifecycleResourceSnapshot Capture(RenderFrameOutcome outcome)
|
||||||
|
|
@ -122,6 +127,8 @@ internal sealed class WorldLifecycleResourceSnapshotSource
|
||||||
TotalLandblocks: outcome.World.TotalLandblocks,
|
TotalLandblocks: outcome.World.TotalLandblocks,
|
||||||
LiveEntities: _liveEntities.Count,
|
LiveEntities: _liveEntities.Count,
|
||||||
MaterializedLiveEntities: _liveEntities.MaterializedCount,
|
MaterializedLiveEntities: _liveEntities.MaterializedCount,
|
||||||
|
RenderSceneOracle: _renderSceneOracle?.Snapshot
|
||||||
|
?? CurrentRenderSceneOracleSnapshot.Disabled,
|
||||||
PendingLiveTeardowns: _liveEntities.PendingTeardownCount,
|
PendingLiveTeardowns: _liveEntities.PendingTeardownCount,
|
||||||
PendingLandblockRetirements: _streaming.PendingRetirementCount,
|
PendingLandblockRetirements: _streaming.PendingRetirementCount,
|
||||||
ParticleEmitters: _particles.ActiveEmitterCount,
|
ParticleEmitters: _particles.ActiveEmitterCount,
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,27 @@ namespace AcDream.App.Rendering;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class InteriorEntityPartition
|
public static class InteriorEntityPartition
|
||||||
{
|
{
|
||||||
|
internal enum ProjectionClass : byte
|
||||||
|
{
|
||||||
|
OutdoorStatic,
|
||||||
|
CellStatic,
|
||||||
|
Dynamic,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface IObserver
|
||||||
|
{
|
||||||
|
void BeginFrame();
|
||||||
|
|
||||||
|
void Observe(
|
||||||
|
uint landblockId,
|
||||||
|
WorldEntity entity,
|
||||||
|
ProjectionClass projectionClass);
|
||||||
|
|
||||||
|
void Complete(Result result);
|
||||||
|
|
||||||
|
void AbortFrame();
|
||||||
|
}
|
||||||
|
|
||||||
public sealed class Result
|
public sealed class Result
|
||||||
{
|
{
|
||||||
public Dictionary<uint, List<WorldEntity>> ByCell { get; } = new();
|
public Dictionary<uint, List<WorldEntity>> ByCell { get; } = new();
|
||||||
|
|
@ -143,6 +164,79 @@ public static class InteriorEntityPartition
|
||||||
result.PruneEmptyCellBuckets();
|
result.PruneEmptyCellBuckets();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current-path referee overload. The observer sees the exact branch that
|
||||||
|
/// populated each accepted partition entry, including its owning
|
||||||
|
/// landblock. A null observer is the production fast path and performs no
|
||||||
|
/// diagnostic allocation or hashing.
|
||||||
|
/// </summary>
|
||||||
|
internal static void Partition(
|
||||||
|
Result result,
|
||||||
|
HashSet<uint> visibleCells,
|
||||||
|
IEnumerable<(uint LandblockId, Vector3 AabbMin, Vector3 AabbMax,
|
||||||
|
IReadOnlyList<WorldEntity> Entities,
|
||||||
|
IReadOnlyDictionary<uint, WorldEntity>? AnimatedById)> landblockEntries,
|
||||||
|
IObserver? observer)
|
||||||
|
{
|
||||||
|
if (observer is null)
|
||||||
|
{
|
||||||
|
Partition(result, visibleCells, landblockEntries);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
observer.BeginFrame();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
result.ClearForReuse();
|
||||||
|
foreach (var entry in landblockEntries)
|
||||||
|
{
|
||||||
|
foreach (var e in entry.Entities)
|
||||||
|
{
|
||||||
|
if (e.MeshRefs.Count == 0) continue;
|
||||||
|
|
||||||
|
// Retail contract: every server-spawned entity is a
|
||||||
|
// DYNAMIC and draws in the last pass, regardless of cell.
|
||||||
|
if (e.ServerGuid != 0)
|
||||||
|
{
|
||||||
|
result.Dynamics.Add(e);
|
||||||
|
observer.Observe(
|
||||||
|
entry.LandblockId,
|
||||||
|
e,
|
||||||
|
ProjectionClass.Dynamic);
|
||||||
|
}
|
||||||
|
else if (e.ParentCellId is uint cell && IsIndoorCellId(cell))
|
||||||
|
{
|
||||||
|
if (!visibleCells.Contains(cell))
|
||||||
|
continue;
|
||||||
|
if (!result.ByCell.TryGetValue(cell, out var list))
|
||||||
|
result.ByCell[cell] = list = new List<WorldEntity>();
|
||||||
|
list.Add(e);
|
||||||
|
observer.Observe(
|
||||||
|
entry.LandblockId,
|
||||||
|
e,
|
||||||
|
ProjectionClass.CellStatic);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
result.OutdoorStatic.Add(e);
|
||||||
|
observer.Observe(
|
||||||
|
entry.LandblockId,
|
||||||
|
e,
|
||||||
|
ProjectionClass.OutdoorStatic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.PruneEmptyCellBuckets();
|
||||||
|
observer.Complete(result);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
observer.AbortFrame();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Shared indoor classification — keep DrawDynamicsLast, the
|
/// <summary>Shared indoor classification — keep DrawDynamicsLast, the
|
||||||
/// outside-stage assignment (#118), and the partition in lockstep.</summary>
|
/// outside-stage assignment (#118), and the partition in lockstep.</summary>
|
||||||
public static bool IsIndoorCellId(uint cellId)
|
public static bool IsIndoorCellId(uint cellId)
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ namespace AcDream.App.Rendering;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class RetailPViewRenderer
|
public sealed class RetailPViewRenderer
|
||||||
{
|
{
|
||||||
|
private readonly InteriorEntityPartition.IObserver? _partitionObserver;
|
||||||
private readonly PortalVisibilityFrame _mainPortalFrameScratch = new();
|
private readonly PortalVisibilityFrame _mainPortalFrameScratch = new();
|
||||||
private readonly ClipFrameAssembly _clipAssemblyScratch = new();
|
private readonly ClipFrameAssembly _clipAssemblyScratch = new();
|
||||||
private readonly ViewconeCuller _viewconeScratch = new();
|
private readonly ViewconeCuller _viewconeScratch = new();
|
||||||
|
|
@ -63,6 +64,16 @@ public sealed class RetailPViewRenderer
|
||||||
private readonly HashSet<uint> _drawableCellsScratch = new();
|
private readonly HashSet<uint> _drawableCellsScratch = new();
|
||||||
private readonly RetailPViewScratchRetention _scratchRetention = new();
|
private readonly RetailPViewScratchRetention _scratchRetention = new();
|
||||||
|
|
||||||
|
public RetailPViewRenderer()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
internal RetailPViewRenderer(
|
||||||
|
InteriorEntityPartition.IObserver? partitionObserver)
|
||||||
|
{
|
||||||
|
_partitionObserver = partitionObserver;
|
||||||
|
}
|
||||||
|
|
||||||
// T2 (BR-4): retail has NO distance constant on the flood-admission chain
|
// T2 (BR-4): retail has NO distance constant on the flood-admission chain
|
||||||
// (DrawBuilding → portal walk → ConstructView: viewconeCheck + side test +
|
// (DrawBuilding → portal walk → ConstructView: viewconeCheck + side test +
|
||||||
// GetClip + GetVisible only). The old 48 m seed cap is replaced by the
|
// GetClip + GetVisible only). The old 48 m seed cap is replaced by the
|
||||||
|
|
@ -156,7 +167,11 @@ public sealed class RetailPViewRenderer
|
||||||
|
|
||||||
passes.PrepareCellBatches(ctx, prepareCells);
|
passes.PrepareCellBatches(ctx, prepareCells);
|
||||||
|
|
||||||
InteriorEntityPartition.Partition(_partitionResult, prepareCells, ctx.LandblockEntries);
|
InteriorEntityPartition.Partition(
|
||||||
|
_partitionResult,
|
||||||
|
prepareCells,
|
||||||
|
ctx.LandblockEntries,
|
||||||
|
_partitionObserver);
|
||||||
var partition = _partitionResult;
|
var partition = _partitionResult;
|
||||||
RetailPViewFrameResult result = _frameResultScratch.Reset(
|
RetailPViewFrameResult result = _frameResultScratch.Reset(
|
||||||
pvFrame,
|
pvFrame,
|
||||||
|
|
|
||||||
389
src/AcDream.App/Rendering/Scene/CurrentRenderSceneOracle.cs
Normal file
389
src/AcDream.App/Rendering/Scene/CurrentRenderSceneOracle.cs
Normal file
|
|
@ -0,0 +1,389 @@
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.Core.World;
|
||||||
|
|
||||||
|
namespace AcDream.App.Rendering.Scene;
|
||||||
|
|
||||||
|
internal readonly record struct RenderSceneHash128(ulong Low, ulong High)
|
||||||
|
{
|
||||||
|
public static RenderSceneHash128 Empty { get; } = new(0, 0);
|
||||||
|
|
||||||
|
public override string ToString() => $"{High:X16}{Low:X16}";
|
||||||
|
}
|
||||||
|
|
||||||
|
internal readonly record struct CurrentRenderProjectionFingerprint(
|
||||||
|
InteriorEntityPartition.ProjectionClass ProjectionClass,
|
||||||
|
uint LandblockId,
|
||||||
|
uint EntityId,
|
||||||
|
uint ServerGuid,
|
||||||
|
uint SourceId,
|
||||||
|
uint ParentCellId,
|
||||||
|
uint EffectCellId,
|
||||||
|
uint BuildingShellAnchorCellId,
|
||||||
|
uint Flags,
|
||||||
|
int MeshCount,
|
||||||
|
RenderSceneHash128 Transform,
|
||||||
|
RenderSceneHash128 Geometry,
|
||||||
|
RenderSceneHash128 Appearance);
|
||||||
|
|
||||||
|
internal readonly record struct CurrentRenderSceneOracleSnapshot(
|
||||||
|
bool Enabled,
|
||||||
|
ulong CompletedFrameSequence,
|
||||||
|
int AbortedFrames,
|
||||||
|
int ProjectionCount,
|
||||||
|
int OutdoorStaticCount,
|
||||||
|
int CellStaticCount,
|
||||||
|
int DynamicCount,
|
||||||
|
int CellBucketCount,
|
||||||
|
RenderSceneHash128 Digest)
|
||||||
|
{
|
||||||
|
public static CurrentRenderSceneOracleSnapshot Disabled { get; } = new(
|
||||||
|
Enabled: false,
|
||||||
|
CompletedFrameSequence: 0,
|
||||||
|
AbortedFrames: 0,
|
||||||
|
ProjectionCount: 0,
|
||||||
|
OutdoorStaticCount: 0,
|
||||||
|
CellStaticCount: 0,
|
||||||
|
DynamicCount: 0,
|
||||||
|
CellBucketCount: 0,
|
||||||
|
Digest: RenderSceneHash128.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal interface ICurrentRenderSceneOracleSnapshotSource
|
||||||
|
{
|
||||||
|
CurrentRenderSceneOracleSnapshot Snapshot { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Deterministic fingerprint of the already-accepted
|
||||||
|
/// <see cref="InteriorEntityPartition"/> output. This is the Slice-F referee:
|
||||||
|
/// it records current behavior before a shadow render scene exists, and never
|
||||||
|
/// makes a draw or visibility decision itself.
|
||||||
|
///
|
||||||
|
/// Collection storage is retained across frames. Production composition does
|
||||||
|
/// not construct this owner unless lifecycle automation is enabled, so normal
|
||||||
|
/// rendering pays no hashing, sorting, or allocation cost.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class CurrentRenderSceneOracle :
|
||||||
|
InteriorEntityPartition.IObserver,
|
||||||
|
ICurrentRenderSceneOracleSnapshotSource
|
||||||
|
{
|
||||||
|
private readonly List<CurrentRenderProjectionFingerprint> _projections = [];
|
||||||
|
private ulong _frameSequence;
|
||||||
|
private int _abortedFrames;
|
||||||
|
private int _outdoorStaticCount;
|
||||||
|
private int _cellStaticCount;
|
||||||
|
private int _dynamicCount;
|
||||||
|
private bool _frameOpen;
|
||||||
|
|
||||||
|
public CurrentRenderSceneOracleSnapshot Snapshot { get; private set; } =
|
||||||
|
CurrentRenderSceneOracleSnapshot.Disabled with { Enabled = true };
|
||||||
|
|
||||||
|
internal IReadOnlyList<CurrentRenderProjectionFingerprint> Projections =>
|
||||||
|
_projections;
|
||||||
|
|
||||||
|
public void BeginFrame()
|
||||||
|
{
|
||||||
|
if (_frameOpen)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"The current render-scene oracle cannot begin a second frame before completing or aborting the first.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_frameOpen = true;
|
||||||
|
_projections.Clear();
|
||||||
|
_outdoorStaticCount = 0;
|
||||||
|
_cellStaticCount = 0;
|
||||||
|
_dynamicCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Observe(
|
||||||
|
uint landblockId,
|
||||||
|
WorldEntity entity,
|
||||||
|
InteriorEntityPartition.ProjectionClass projectionClass)
|
||||||
|
{
|
||||||
|
if (!_frameOpen)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"The current render-scene oracle received a projection outside an open frame.");
|
||||||
|
}
|
||||||
|
ArgumentNullException.ThrowIfNull(entity);
|
||||||
|
|
||||||
|
switch (projectionClass)
|
||||||
|
{
|
||||||
|
case InteriorEntityPartition.ProjectionClass.OutdoorStatic:
|
||||||
|
_outdoorStaticCount++;
|
||||||
|
break;
|
||||||
|
case InteriorEntityPartition.ProjectionClass.CellStatic:
|
||||||
|
_cellStaticCount++;
|
||||||
|
break;
|
||||||
|
case InteriorEntityPartition.ProjectionClass.Dynamic:
|
||||||
|
_dynamicCount++;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException(
|
||||||
|
nameof(projectionClass),
|
||||||
|
projectionClass,
|
||||||
|
"Unknown current render projection class.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_projections.Add(CreateFingerprint(
|
||||||
|
landblockId,
|
||||||
|
entity,
|
||||||
|
projectionClass));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Complete(InteriorEntityPartition.Result result)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(result);
|
||||||
|
if (!_frameOpen)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"The current render-scene oracle cannot complete without an open frame.");
|
||||||
|
}
|
||||||
|
|
||||||
|
int cellStaticCount = 0;
|
||||||
|
foreach (List<WorldEntity> entities in result.ByCell.Values)
|
||||||
|
cellStaticCount = checked(cellStaticCount + entities.Count);
|
||||||
|
if (result.OutdoorStatic.Count != _outdoorStaticCount
|
||||||
|
|| cellStaticCount != _cellStaticCount
|
||||||
|
|| result.Dynamics.Count != _dynamicCount
|
||||||
|
|| _projections.Count
|
||||||
|
!= checked(_outdoorStaticCount + _cellStaticCount + _dynamicCount))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"The current render-scene oracle diverged from the partition it observed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_projections.Sort(CurrentRenderProjectionFingerprintComparer.Instance);
|
||||||
|
StableRenderHash128 aggregate = StableRenderHash128.Create();
|
||||||
|
aggregate.Add(_projections.Count);
|
||||||
|
foreach (CurrentRenderProjectionFingerprint projection in _projections)
|
||||||
|
AddProjection(ref aggregate, in projection);
|
||||||
|
|
||||||
|
_frameOpen = false;
|
||||||
|
Snapshot = new CurrentRenderSceneOracleSnapshot(
|
||||||
|
Enabled: true,
|
||||||
|
CompletedFrameSequence: checked(++_frameSequence),
|
||||||
|
AbortedFrames: _abortedFrames,
|
||||||
|
ProjectionCount: _projections.Count,
|
||||||
|
OutdoorStaticCount: _outdoorStaticCount,
|
||||||
|
CellStaticCount: _cellStaticCount,
|
||||||
|
DynamicCount: _dynamicCount,
|
||||||
|
CellBucketCount: result.ByCell.Count,
|
||||||
|
Digest: aggregate.Finish());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AbortFrame()
|
||||||
|
{
|
||||||
|
if (!_frameOpen)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_frameOpen = false;
|
||||||
|
_projections.Clear();
|
||||||
|
_outdoorStaticCount = 0;
|
||||||
|
_cellStaticCount = 0;
|
||||||
|
_dynamicCount = 0;
|
||||||
|
_abortedFrames = checked(_abortedFrames + 1);
|
||||||
|
Snapshot = Snapshot with { AbortedFrames = _abortedFrames };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static CurrentRenderProjectionFingerprint CreateFingerprint(
|
||||||
|
uint landblockId,
|
||||||
|
WorldEntity entity,
|
||||||
|
InteriorEntityPartition.ProjectionClass projectionClass)
|
||||||
|
{
|
||||||
|
StableRenderHash128 transform = StableRenderHash128.Create();
|
||||||
|
transform.Add(entity.Position);
|
||||||
|
transform.Add(entity.Rotation);
|
||||||
|
transform.Add(entity.Scale);
|
||||||
|
|
||||||
|
StableRenderHash128 geometry = StableRenderHash128.Create();
|
||||||
|
geometry.Add(entity.MeshRefs.Count);
|
||||||
|
foreach (MeshRef mesh in entity.MeshRefs)
|
||||||
|
{
|
||||||
|
geometry.Add(mesh.GfxObjId);
|
||||||
|
geometry.Add(mesh.PartTransform);
|
||||||
|
}
|
||||||
|
|
||||||
|
StableRenderHash128 appearance = StableRenderHash128.Create();
|
||||||
|
if (entity.PaletteOverride is { } palette)
|
||||||
|
{
|
||||||
|
appearance.Add(true);
|
||||||
|
appearance.Add(palette.BasePaletteId);
|
||||||
|
appearance.Add(palette.SubPalettes.Count);
|
||||||
|
foreach (PaletteOverride.SubPaletteRange range in palette.SubPalettes)
|
||||||
|
{
|
||||||
|
appearance.Add(range.SubPaletteId);
|
||||||
|
appearance.Add(range.Offset);
|
||||||
|
appearance.Add(range.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
appearance.Add(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
appearance.Add(entity.PartOverrides.Count);
|
||||||
|
foreach (PartOverride part in entity.PartOverrides)
|
||||||
|
{
|
||||||
|
appearance.Add(part.PartIndex);
|
||||||
|
appearance.Add(part.GfxObjId);
|
||||||
|
}
|
||||||
|
appearance.Add(entity.HiddenPartsMask);
|
||||||
|
|
||||||
|
uint flags = 0;
|
||||||
|
if (entity.IsDrawVisible)
|
||||||
|
flags |= 1u << 0;
|
||||||
|
if (entity.IsAncestorDrawVisible)
|
||||||
|
flags |= 1u << 1;
|
||||||
|
if (entity.IsBuildingShell)
|
||||||
|
flags |= 1u << 2;
|
||||||
|
if (entity.ParentCellId.HasValue)
|
||||||
|
flags |= 1u << 3;
|
||||||
|
if (entity.EffectCellId.HasValue)
|
||||||
|
flags |= 1u << 4;
|
||||||
|
if (entity.BuildingShellAnchorCellId.HasValue)
|
||||||
|
flags |= 1u << 5;
|
||||||
|
|
||||||
|
return new CurrentRenderProjectionFingerprint(
|
||||||
|
ProjectionClass: projectionClass,
|
||||||
|
LandblockId: landblockId,
|
||||||
|
EntityId: entity.Id,
|
||||||
|
ServerGuid: entity.ServerGuid,
|
||||||
|
SourceId: entity.SourceGfxObjOrSetupId,
|
||||||
|
ParentCellId: entity.ParentCellId ?? 0,
|
||||||
|
EffectCellId: entity.EffectCellId ?? 0,
|
||||||
|
BuildingShellAnchorCellId: entity.BuildingShellAnchorCellId ?? 0,
|
||||||
|
Flags: flags,
|
||||||
|
MeshCount: entity.MeshRefs.Count,
|
||||||
|
Transform: transform.Finish(),
|
||||||
|
Geometry: geometry.Finish(),
|
||||||
|
Appearance: appearance.Finish());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddProjection(
|
||||||
|
ref StableRenderHash128 hash,
|
||||||
|
in CurrentRenderProjectionFingerprint projection)
|
||||||
|
{
|
||||||
|
hash.Add((byte)projection.ProjectionClass);
|
||||||
|
hash.Add(projection.LandblockId);
|
||||||
|
hash.Add(projection.EntityId);
|
||||||
|
hash.Add(projection.ServerGuid);
|
||||||
|
hash.Add(projection.SourceId);
|
||||||
|
hash.Add(projection.ParentCellId);
|
||||||
|
hash.Add(projection.EffectCellId);
|
||||||
|
hash.Add(projection.BuildingShellAnchorCellId);
|
||||||
|
hash.Add(projection.Flags);
|
||||||
|
hash.Add(projection.MeshCount);
|
||||||
|
hash.Add(projection.Transform.Low);
|
||||||
|
hash.Add(projection.Transform.High);
|
||||||
|
hash.Add(projection.Geometry.Low);
|
||||||
|
hash.Add(projection.Geometry.High);
|
||||||
|
hash.Add(projection.Appearance.Low);
|
||||||
|
hash.Add(projection.Appearance.High);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class CurrentRenderProjectionFingerprintComparer :
|
||||||
|
IComparer<CurrentRenderProjectionFingerprint>
|
||||||
|
{
|
||||||
|
public static CurrentRenderProjectionFingerprintComparer Instance { get; } =
|
||||||
|
new();
|
||||||
|
|
||||||
|
public int Compare(
|
||||||
|
CurrentRenderProjectionFingerprint x,
|
||||||
|
CurrentRenderProjectionFingerprint y)
|
||||||
|
{
|
||||||
|
int value = x.ProjectionClass.CompareTo(y.ProjectionClass);
|
||||||
|
if (value != 0) return value;
|
||||||
|
value = x.LandblockId.CompareTo(y.LandblockId);
|
||||||
|
if (value != 0) return value;
|
||||||
|
value = x.ParentCellId.CompareTo(y.ParentCellId);
|
||||||
|
if (value != 0) return value;
|
||||||
|
value = x.EntityId.CompareTo(y.EntityId);
|
||||||
|
if (value != 0) return value;
|
||||||
|
value = x.ServerGuid.CompareTo(y.ServerGuid);
|
||||||
|
if (value != 0) return value;
|
||||||
|
value = x.SourceId.CompareTo(y.SourceId);
|
||||||
|
if (value != 0) return value;
|
||||||
|
value = x.EffectCellId.CompareTo(y.EffectCellId);
|
||||||
|
if (value != 0) return value;
|
||||||
|
value = x.BuildingShellAnchorCellId.CompareTo(
|
||||||
|
y.BuildingShellAnchorCellId);
|
||||||
|
if (value != 0) return value;
|
||||||
|
value = x.Flags.CompareTo(y.Flags);
|
||||||
|
if (value != 0) return value;
|
||||||
|
value = x.MeshCount.CompareTo(y.MeshCount);
|
||||||
|
if (value != 0) return value;
|
||||||
|
value = x.Transform.High.CompareTo(y.Transform.High);
|
||||||
|
if (value != 0) return value;
|
||||||
|
value = x.Transform.Low.CompareTo(y.Transform.Low);
|
||||||
|
if (value != 0) return value;
|
||||||
|
value = x.Geometry.High.CompareTo(y.Geometry.High);
|
||||||
|
if (value != 0) return value;
|
||||||
|
value = x.Geometry.Low.CompareTo(y.Geometry.Low);
|
||||||
|
if (value != 0) return value;
|
||||||
|
value = x.Appearance.High.CompareTo(y.Appearance.High);
|
||||||
|
if (value != 0) return value;
|
||||||
|
return x.Appearance.Low.CompareTo(y.Appearance.Low);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal struct StableRenderHash128
|
||||||
|
{
|
||||||
|
private const ulong LowOffset = 14695981039346656037UL;
|
||||||
|
private const ulong LowPrime = 1099511628211UL;
|
||||||
|
private const ulong HighOffset = 7809847782465536322UL;
|
||||||
|
private const ulong HighPrime = 14029467366897019727UL;
|
||||||
|
|
||||||
|
private ulong _low;
|
||||||
|
private ulong _high;
|
||||||
|
|
||||||
|
public static StableRenderHash128 Create() => new()
|
||||||
|
{
|
||||||
|
_low = LowOffset,
|
||||||
|
_high = HighOffset,
|
||||||
|
};
|
||||||
|
|
||||||
|
public readonly RenderSceneHash128 Finish() => new(_low, _high);
|
||||||
|
|
||||||
|
public void Add(bool value) => Add(value ? (byte)1 : (byte)0);
|
||||||
|
|
||||||
|
public void Add(byte value) => Add((ulong)value);
|
||||||
|
|
||||||
|
public void Add(int value) => Add(unchecked((uint)value));
|
||||||
|
|
||||||
|
public void Add(uint value) => Add((ulong)value);
|
||||||
|
|
||||||
|
public void Add(float value) => Add(BitConverter.SingleToUInt32Bits(value));
|
||||||
|
|
||||||
|
public void Add(ulong value)
|
||||||
|
{
|
||||||
|
_low = unchecked((_low ^ value) * LowPrime);
|
||||||
|
ulong folded = value ^ BitOperations.RotateLeft(value, 29);
|
||||||
|
_high = unchecked((_high ^ folded) * HighPrime);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(Vector3 value)
|
||||||
|
{
|
||||||
|
Add(value.X);
|
||||||
|
Add(value.Y);
|
||||||
|
Add(value.Z);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(Quaternion value)
|
||||||
|
{
|
||||||
|
Add(value.X);
|
||||||
|
Add(value.Y);
|
||||||
|
Add(value.Z);
|
||||||
|
Add(value.W);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(Matrix4x4 value)
|
||||||
|
{
|
||||||
|
Add(value.M11); Add(value.M12); Add(value.M13); Add(value.M14);
|
||||||
|
Add(value.M21); Add(value.M22); Add(value.M23); Add(value.M24);
|
||||||
|
Add(value.M31); Add(value.M32); Add(value.M33); Add(value.M34);
|
||||||
|
Add(value.M41); Add(value.M42); Add(value.M43); Add(value.M44);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ using System.Text.Json;
|
||||||
using AcDream.App.Diagnostics;
|
using AcDream.App.Diagnostics;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
using AcDream.App.Rendering.Residency;
|
using AcDream.App.Rendering.Residency;
|
||||||
|
using AcDream.App.Rendering.Scene;
|
||||||
using AcDream.App.Streaming;
|
using AcDream.App.Streaming;
|
||||||
using AcDream.App.UI.Testing;
|
using AcDream.App.UI.Testing;
|
||||||
using SixLabors.ImageSharp;
|
using SixLabors.ImageSharp;
|
||||||
|
|
@ -114,6 +115,18 @@ public sealed class WorldLifecycleAutomationControllerTests
|
||||||
{
|
{
|
||||||
LoadedLandblocks = 1,
|
LoadedLandblocks = 1,
|
||||||
WorldEntities = 42,
|
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,
|
TrackedGpuBytes = 1234,
|
||||||
Residency = new ResidencySnapshot(
|
Residency = new ResidencySnapshot(
|
||||||
[
|
[
|
||||||
|
|
@ -166,6 +179,24 @@ public sealed class WorldLifecycleAutomationControllerTests
|
||||||
Assert.True(json.RootElement.GetProperty("render").GetProperty("presentation")
|
Assert.True(json.RootElement.GetProperty("render").GetProperty("presentation")
|
||||||
.GetProperty("screenshotCaptured").GetBoolean());
|
.GetProperty("screenshotCaptured").GetBoolean());
|
||||||
Assert.Equal(42, json.RootElement.GetProperty("resources").GetProperty("worldEntities").GetInt32());
|
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
|
JsonElement residency = json.RootElement
|
||||||
.GetProperty("resources")
|
.GetProperty("resources")
|
||||||
.GetProperty("residency");
|
.GetProperty("residency");
|
||||||
|
|
@ -304,7 +335,8 @@ public sealed class WorldLifecycleAutomationControllerTests
|
||||||
directory);
|
directory);
|
||||||
|
|
||||||
private static WorldLifecycleResourceSnapshot EmptyResources() => new(
|
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,
|
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,
|
0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L,
|
||||||
default, default, 0d, 0d, null);
|
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