perf(rendering): retain packed classification
Retain appearance classification per projection while refreshing transforms, clip slots, lights, selection lighting, and group instance payloads each frame. Incomplete resources and active animation remain on the live classification path, and only an exact full packed comparison acknowledges scene dirtiness.
This commit is contained in:
parent
f10dec58ba
commit
54d17eb446
7 changed files with 731 additions and 30 deletions
|
|
@ -386,7 +386,8 @@ internal sealed class FrameRootCompositionPhase
|
||||||
? new RenderSceneShadowComparisonController(
|
? new RenderSceneShadowComparisonController(
|
||||||
live.RenderSceneShadow,
|
live.RenderSceneShadow,
|
||||||
currentRenderSceneOracle,
|
currentRenderSceneOracle,
|
||||||
message => d.Log("[UI-PROBE] " + message))
|
message => d.Log("[UI-PROBE] " + message),
|
||||||
|
acknowledgeDirty: false)
|
||||||
: null;
|
: null;
|
||||||
RenderScenePViewFrameProductController? renderFrameProduct =
|
RenderScenePViewFrameProductController? renderFrameProduct =
|
||||||
currentRenderSceneOracle is not null
|
currentRenderSceneOracle is not null
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,13 @@ internal readonly record struct RenderFrameProductComparisonSnapshot(
|
||||||
string? SelectionOutputFirstMismatch,
|
string? SelectionOutputFirstMismatch,
|
||||||
RenderSceneHash128 SelectionOutputExpectedDigest,
|
RenderSceneHash128 SelectionOutputExpectedDigest,
|
||||||
RenderSceneHash128 SelectionOutputActualDigest,
|
RenderSceneHash128 SelectionOutputActualDigest,
|
||||||
|
int PackedClassificationProjectionCount,
|
||||||
|
int PackedClassificationStaticRebuildCount,
|
||||||
|
int PackedClassificationSameFrameReuseCount,
|
||||||
|
int PackedClassificationCrossFrameReuseCount,
|
||||||
|
int PackedClassificationAnimatedCount,
|
||||||
|
int PackedClassificationRetiredCount,
|
||||||
|
long PackedClassificationRetainedPayloadBytes,
|
||||||
RenderFrameDiagnosticCounts ProductCounts)
|
RenderFrameDiagnosticCounts ProductCounts)
|
||||||
{
|
{
|
||||||
public static RenderFrameProductComparisonSnapshot Disabled { get; } =
|
public static RenderFrameProductComparisonSnapshot Disabled { get; } =
|
||||||
|
|
@ -90,6 +97,13 @@ internal readonly record struct RenderFrameProductComparisonSnapshot(
|
||||||
SelectionOutputFirstMismatch: null,
|
SelectionOutputFirstMismatch: null,
|
||||||
SelectionOutputExpectedDigest: RenderSceneHash128.Empty,
|
SelectionOutputExpectedDigest: RenderSceneHash128.Empty,
|
||||||
SelectionOutputActualDigest: RenderSceneHash128.Empty,
|
SelectionOutputActualDigest: RenderSceneHash128.Empty,
|
||||||
|
PackedClassificationProjectionCount: 0,
|
||||||
|
PackedClassificationStaticRebuildCount: 0,
|
||||||
|
PackedClassificationSameFrameReuseCount: 0,
|
||||||
|
PackedClassificationCrossFrameReuseCount: 0,
|
||||||
|
PackedClassificationAnimatedCount: 0,
|
||||||
|
PackedClassificationRetiredCount: 0,
|
||||||
|
PackedClassificationRetainedPayloadBytes: 0,
|
||||||
ProductCounts: default);
|
ProductCounts: default);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,6 +150,8 @@ internal sealed class RenderScenePViewFrameProductController :
|
||||||
private ulong _selectionOutputSuccessfulComparisonCount;
|
private ulong _selectionOutputSuccessfulComparisonCount;
|
||||||
private long _selectionOutputMismatchCount;
|
private long _selectionOutputMismatchCount;
|
||||||
private string? _selectionOutputFirstMismatch;
|
private string? _selectionOutputFirstMismatch;
|
||||||
|
private PackedClassificationCacheSnapshot
|
||||||
|
_packedClassification;
|
||||||
|
|
||||||
public RenderScenePViewFrameProductController(
|
public RenderScenePViewFrameProductController(
|
||||||
RenderSceneShadowRuntime shadow,
|
RenderSceneShadowRuntime shadow,
|
||||||
|
|
@ -280,6 +296,17 @@ internal sealed class RenderScenePViewFrameProductController :
|
||||||
in view,
|
in view,
|
||||||
tupleLandblockId,
|
tupleLandblockId,
|
||||||
cameraWorldPosition);
|
cameraWorldPosition);
|
||||||
|
if (_dispatcher is not null
|
||||||
|
&& mismatch is null
|
||||||
|
&& packed.Successful
|
||||||
|
&& classified.Successful)
|
||||||
|
{
|
||||||
|
// The frame product and retained classifier have consumed every
|
||||||
|
// dirty fact and matched the production oracle. A later update
|
||||||
|
// starts the next dirty epoch; a mismatch or exception above
|
||||||
|
// deliberately leaves dirtiness intact for the next rebuild.
|
||||||
|
_shadow.ClearDirty();
|
||||||
|
}
|
||||||
|
|
||||||
Snapshot = new RenderFrameProductComparisonSnapshot(
|
Snapshot = new RenderFrameProductComparisonSnapshot(
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
|
|
@ -335,6 +362,20 @@ internal sealed class RenderScenePViewFrameProductController :
|
||||||
classified.ExpectedSelectionDigest,
|
classified.ExpectedSelectionDigest,
|
||||||
SelectionOutputActualDigest:
|
SelectionOutputActualDigest:
|
||||||
classified.ActualSelectionDigest,
|
classified.ActualSelectionDigest,
|
||||||
|
PackedClassificationProjectionCount:
|
||||||
|
_packedClassification.ProjectionCount,
|
||||||
|
PackedClassificationStaticRebuildCount:
|
||||||
|
_packedClassification.StaticRebuildCount,
|
||||||
|
PackedClassificationSameFrameReuseCount:
|
||||||
|
_packedClassification.SameFrameReuseCount,
|
||||||
|
PackedClassificationCrossFrameReuseCount:
|
||||||
|
_packedClassification.CrossFrameReuseCount,
|
||||||
|
PackedClassificationAnimatedCount:
|
||||||
|
_packedClassification.AnimatedClassificationCount,
|
||||||
|
PackedClassificationRetiredCount:
|
||||||
|
_packedClassification.RetiredProjectionCount,
|
||||||
|
PackedClassificationRetainedPayloadBytes:
|
||||||
|
_packedClassification.RetainedPayloadBytes,
|
||||||
ProductCounts: view.DiagnosticCounts);
|
ProductCounts: view.DiagnosticCounts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -351,6 +392,8 @@ internal sealed class RenderScenePViewFrameProductController :
|
||||||
in view,
|
in view,
|
||||||
tupleLandblockId,
|
tupleLandblockId,
|
||||||
cameraWorldPosition);
|
cameraWorldPosition);
|
||||||
|
_packedClassification =
|
||||||
|
dispatcher.PackedClassificationSnapshot;
|
||||||
IReadOnlyList<CurrentRenderDispatcherSubmission> expected =
|
IReadOnlyList<CurrentRenderDispatcherSubmission> expected =
|
||||||
_current.DispatcherSubmissions;
|
_current.DispatcherSubmissions;
|
||||||
IReadOnlyList<CurrentRenderDispatcherSubmission> actual =
|
IReadOnlyList<CurrentRenderDispatcherSubmission> actual =
|
||||||
|
|
@ -420,6 +463,7 @@ internal sealed class RenderScenePViewFrameProductController :
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ClassifiedOutputComparison(
|
return new ClassifiedOutputComparison(
|
||||||
|
Successful: mismatch is null && selectionMismatch is null,
|
||||||
expected.Count,
|
expected.Count,
|
||||||
actual.Count,
|
actual.Count,
|
||||||
expectedDigest,
|
expectedDigest,
|
||||||
|
|
@ -661,7 +705,10 @@ internal sealed class RenderScenePViewFrameProductController :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new PackedInputComparison(expectedDigest, actualDigest);
|
return new PackedInputComparison(
|
||||||
|
Successful: mismatch is null,
|
||||||
|
expectedDigest,
|
||||||
|
actualDigest);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static uint DispatcherCacheLandblock(
|
private static uint DispatcherCacheLandblock(
|
||||||
|
|
@ -780,10 +827,12 @@ internal sealed class RenderScenePViewFrameProductController :
|
||||||
RenderProjectionId ProjectionId);
|
RenderProjectionId ProjectionId);
|
||||||
|
|
||||||
private readonly record struct PackedInputComparison(
|
private readonly record struct PackedInputComparison(
|
||||||
|
bool Successful,
|
||||||
RenderSceneHash128 ExpectedDigest,
|
RenderSceneHash128 ExpectedDigest,
|
||||||
RenderSceneHash128 ActualDigest);
|
RenderSceneHash128 ActualDigest);
|
||||||
|
|
||||||
private readonly record struct ClassifiedOutputComparison(
|
private readonly record struct ClassifiedOutputComparison(
|
||||||
|
bool Successful,
|
||||||
int ExpectedDrawCount,
|
int ExpectedDrawCount,
|
||||||
int ActualDrawCount,
|
int ActualDrawCount,
|
||||||
RenderSceneHash128 ExpectedDigest,
|
RenderSceneHash128 ExpectedDigest,
|
||||||
|
|
|
||||||
|
|
@ -223,6 +223,7 @@ internal sealed class RenderSceneShadowComparisonController :
|
||||||
private readonly RenderSceneShadowRuntime _shadow;
|
private readonly RenderSceneShadowRuntime _shadow;
|
||||||
private readonly CurrentRenderSceneOracle _oracle;
|
private readonly CurrentRenderSceneOracle _oracle;
|
||||||
private readonly Action<string>? _log;
|
private readonly Action<string>? _log;
|
||||||
|
private readonly bool _acknowledgeDirty;
|
||||||
private readonly List<RenderProjectionRecord> _sceneRecords = [];
|
private readonly List<RenderProjectionRecord> _sceneRecords = [];
|
||||||
private readonly HashSet<RenderProjectionId> _expectedIds = [];
|
private readonly HashSet<RenderProjectionId> _expectedIds = [];
|
||||||
private ulong _renderFrameSequence;
|
private ulong _renderFrameSequence;
|
||||||
|
|
@ -236,11 +237,13 @@ internal sealed class RenderSceneShadowComparisonController :
|
||||||
public RenderSceneShadowComparisonController(
|
public RenderSceneShadowComparisonController(
|
||||||
RenderSceneShadowRuntime shadow,
|
RenderSceneShadowRuntime shadow,
|
||||||
CurrentRenderSceneOracle oracle,
|
CurrentRenderSceneOracle oracle,
|
||||||
Action<string>? log = null)
|
Action<string>? log = null,
|
||||||
|
bool acknowledgeDirty = true)
|
||||||
{
|
{
|
||||||
_shadow = shadow ?? throw new ArgumentNullException(nameof(shadow));
|
_shadow = shadow ?? throw new ArgumentNullException(nameof(shadow));
|
||||||
_oracle = oracle ?? throw new ArgumentNullException(nameof(oracle));
|
_oracle = oracle ?? throw new ArgumentNullException(nameof(oracle));
|
||||||
_log = log;
|
_log = log;
|
||||||
|
_acknowledgeDirty = acknowledgeDirty;
|
||||||
Snapshot = BuildSnapshot();
|
Snapshot = BuildSnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -300,7 +303,8 @@ internal sealed class RenderSceneShadowComparisonController :
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_shadow.ClearDirty();
|
if (_acknowledgeDirty && mismatch is null)
|
||||||
|
_shadow.ClearDirty();
|
||||||
Snapshot = BuildSnapshot();
|
Snapshot = BuildSnapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,251 @@
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using AcDream.App.Rendering.Scene;
|
||||||
|
|
||||||
|
namespace AcDream.App.Rendering.Wb;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The appearance-only identity of one retained scene projection. Root
|
||||||
|
/// transform, clip slot, light selection, and selection lighting deliberately
|
||||||
|
/// stay outside this key: G3 updates those instance fields without rebuilding
|
||||||
|
/// mesh/material classification.
|
||||||
|
/// </summary>
|
||||||
|
internal readonly record struct PackedClassificationIdentity(
|
||||||
|
RenderOwnerIncarnation Incarnation,
|
||||||
|
RenderMeshSet MeshSet,
|
||||||
|
RenderMaterialVariant Material,
|
||||||
|
RenderDegradeState DegradeState,
|
||||||
|
RenderSceneHash128 Geometry,
|
||||||
|
RenderSceneHash128 Appearance)
|
||||||
|
{
|
||||||
|
public static PackedClassificationIdentity From(
|
||||||
|
in RenderProjectionRecord projection) =>
|
||||||
|
new(
|
||||||
|
projection.OwnerIncarnation,
|
||||||
|
projection.MeshSet,
|
||||||
|
projection.Material,
|
||||||
|
projection.DegradeState,
|
||||||
|
projection.Source.GeometryFingerprint,
|
||||||
|
projection.Source.AppearanceFingerprint);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal readonly record struct PackedClassifiedBatch(
|
||||||
|
GroupKey Key,
|
||||||
|
Matrix4x4 RestPose,
|
||||||
|
Vector3 LocalSortCenter);
|
||||||
|
|
||||||
|
internal readonly record struct PackedClassifiedSelectionPart(
|
||||||
|
int PartIndex,
|
||||||
|
uint GfxObjId,
|
||||||
|
Matrix4x4 RestPose);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reusable variable-size payload for one projection. Lists grow only when an
|
||||||
|
/// appearance first exposes a higher batch/part count and are cleared in
|
||||||
|
/// place on the next appearance rebuild.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class PackedProjectionClassificationEntry
|
||||||
|
{
|
||||||
|
public PackedClassificationIdentity Identity;
|
||||||
|
public readonly List<PackedClassifiedBatch> Batches = [];
|
||||||
|
public readonly List<PackedClassifiedSelectionPart> SelectionParts = [];
|
||||||
|
public long LastBuiltFrame;
|
||||||
|
public long LastSeenFrame;
|
||||||
|
public bool ReusableAcrossFrames;
|
||||||
|
public long AccountedPayloadBytes;
|
||||||
|
|
||||||
|
public void BeginRebuild(
|
||||||
|
in PackedClassificationIdentity identity,
|
||||||
|
long frame)
|
||||||
|
{
|
||||||
|
Identity = identity;
|
||||||
|
Batches.Clear();
|
||||||
|
SelectionParts.Clear();
|
||||||
|
LastBuiltFrame = frame;
|
||||||
|
LastSeenFrame = frame;
|
||||||
|
ReusableAcrossFrames = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ReleaseStorage()
|
||||||
|
{
|
||||||
|
Batches.Clear();
|
||||||
|
SelectionParts.Clear();
|
||||||
|
Batches.TrimExcess();
|
||||||
|
SelectionParts.TrimExcess();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal readonly record struct PackedClassificationCacheSnapshot(
|
||||||
|
int ProjectionCount,
|
||||||
|
int StaticRebuildCount,
|
||||||
|
int SameFrameReuseCount,
|
||||||
|
int CrossFrameReuseCount,
|
||||||
|
int AnimatedClassificationCount,
|
||||||
|
int RetiredProjectionCount,
|
||||||
|
long RetainedPayloadBytes);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// G3 retained classification owner. Structural classification is rebuilt
|
||||||
|
/// only for first sight, incarnation/appearance changes, incomplete resource
|
||||||
|
/// readiness, or active animation. Warm unchanged projections replay their
|
||||||
|
/// retained batch templates while current transforms, lights, clip slots, and
|
||||||
|
/// selection lighting are refreshed.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class PackedProjectionClassificationCache
|
||||||
|
{
|
||||||
|
private const RenderDirtyMask ClassificationDirtyMask =
|
||||||
|
RenderDirtyMask.Appearance;
|
||||||
|
|
||||||
|
private readonly Dictionary<
|
||||||
|
RenderProjectionId,
|
||||||
|
PackedProjectionClassificationEntry> _entries = [];
|
||||||
|
private readonly List<RenderProjectionId> _retired = [];
|
||||||
|
private RenderSceneGeneration _generation;
|
||||||
|
private bool _generationSet;
|
||||||
|
private long _frame;
|
||||||
|
private int _staticRebuildCount;
|
||||||
|
private int _sameFrameReuseCount;
|
||||||
|
private int _crossFrameReuseCount;
|
||||||
|
private int _animatedClassificationCount;
|
||||||
|
private int _retiredProjectionCount;
|
||||||
|
private long _retainedPayloadBytes;
|
||||||
|
|
||||||
|
public long Frame => _frame;
|
||||||
|
|
||||||
|
public PackedClassificationCacheSnapshot Snapshot =>
|
||||||
|
new(
|
||||||
|
_entries.Count,
|
||||||
|
_staticRebuildCount,
|
||||||
|
_sameFrameReuseCount,
|
||||||
|
_crossFrameReuseCount,
|
||||||
|
_animatedClassificationCount,
|
||||||
|
_retiredProjectionCount,
|
||||||
|
_retainedPayloadBytes);
|
||||||
|
|
||||||
|
public void BeginFrame(RenderSceneGeneration generation)
|
||||||
|
{
|
||||||
|
if (_frame == long.MaxValue)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Packed classification frame identity was exhausted.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_generationSet || generation != _generation)
|
||||||
|
{
|
||||||
|
Clear();
|
||||||
|
_generation = generation;
|
||||||
|
_generationSet = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_frame++;
|
||||||
|
_staticRebuildCount = 0;
|
||||||
|
_sameFrameReuseCount = 0;
|
||||||
|
_crossFrameReuseCount = 0;
|
||||||
|
_animatedClassificationCount = 0;
|
||||||
|
_retiredProjectionCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetReusable(
|
||||||
|
RenderProjectionId id,
|
||||||
|
in PackedClassificationIdentity identity,
|
||||||
|
RenderDirtyMask dirtyMask,
|
||||||
|
out PackedProjectionClassificationEntry? entry)
|
||||||
|
{
|
||||||
|
if (!_entries.TryGetValue(id, out entry)
|
||||||
|
|| entry.Identity != identity)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.LastSeenFrame = _frame;
|
||||||
|
if (entry.LastBuiltFrame == _frame)
|
||||||
|
{
|
||||||
|
_sameFrameReuseCount++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!entry.ReusableAcrossFrames
|
||||||
|
|| (dirtyMask & ClassificationDirtyMask) != 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_crossFrameReuseCount++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PackedProjectionClassificationEntry BeginRebuild(
|
||||||
|
RenderProjectionId id,
|
||||||
|
in PackedClassificationIdentity identity)
|
||||||
|
{
|
||||||
|
if (!_entries.TryGetValue(id, out var entry))
|
||||||
|
{
|
||||||
|
entry = new PackedProjectionClassificationEntry();
|
||||||
|
_entries.Add(id, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.BeginRebuild(in identity, _frame);
|
||||||
|
_staticRebuildCount++;
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void CompleteRebuild(
|
||||||
|
PackedProjectionClassificationEntry entry,
|
||||||
|
bool reusableAcrossFrames)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(entry);
|
||||||
|
if (entry.LastBuiltFrame != _frame)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"Only an entry rebuilt in the active frame can be completed.");
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.ReusableAcrossFrames = reusableAcrossFrames;
|
||||||
|
long payloadBytes = checked(
|
||||||
|
(long)entry.Batches.Capacity
|
||||||
|
* Unsafe.SizeOf<PackedClassifiedBatch>()
|
||||||
|
+ (long)entry.SelectionParts.Capacity
|
||||||
|
* Unsafe.SizeOf<PackedClassifiedSelectionPart>());
|
||||||
|
_retainedPayloadBytes = checked(
|
||||||
|
_retainedPayloadBytes
|
||||||
|
- entry.AccountedPayloadBytes
|
||||||
|
+ payloadBytes);
|
||||||
|
entry.AccountedPayloadBytes = payloadBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecordAnimatedClassification() =>
|
||||||
|
_animatedClassificationCount++;
|
||||||
|
|
||||||
|
public void EndFrame()
|
||||||
|
{
|
||||||
|
_retired.Clear();
|
||||||
|
foreach ((RenderProjectionId id, var entry) in _entries)
|
||||||
|
{
|
||||||
|
if (entry.LastSeenFrame != _frame)
|
||||||
|
{
|
||||||
|
_retainedPayloadBytes -= entry.AccountedPayloadBytes;
|
||||||
|
entry.AccountedPayloadBytes = 0;
|
||||||
|
entry.ReleaseStorage();
|
||||||
|
_retired.Add(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < _retired.Count; i++)
|
||||||
|
_entries.Remove(_retired[i]);
|
||||||
|
_retiredProjectionCount = _retired.Count;
|
||||||
|
_retired.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
foreach (PackedProjectionClassificationEntry entry
|
||||||
|
in _entries.Values)
|
||||||
|
{
|
||||||
|
entry.ReleaseStorage();
|
||||||
|
}
|
||||||
|
_entries.Clear();
|
||||||
|
_retired.Clear();
|
||||||
|
_frame = 0;
|
||||||
|
_retainedPayloadBytes = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -30,6 +30,8 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
private readonly List<CurrentRenderSelectionFingerprint>
|
private readonly List<CurrentRenderSelectionFingerprint>
|
||||||
_packedSelectionParts = [];
|
_packedSelectionParts = [];
|
||||||
private readonly HashSet<PackedSelectionKey> _packedSelectionKeys = [];
|
private readonly HashSet<PackedSelectionKey> _packedSelectionKeys = [];
|
||||||
|
private readonly PackedProjectionClassificationCache
|
||||||
|
_packedClassificationCache = new();
|
||||||
private long _packedGroupFrame;
|
private long _packedGroupFrame;
|
||||||
private long _nextPackedGroupRegistration = 1;
|
private long _nextPackedGroupRegistration = 1;
|
||||||
private int _nextPackedInstanceSubmissionOrder;
|
private int _nextPackedInstanceSubmissionOrder;
|
||||||
|
|
@ -40,6 +42,10 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
internal IReadOnlyList<CurrentRenderSelectionFingerprint>
|
internal IReadOnlyList<CurrentRenderSelectionFingerprint>
|
||||||
PackedSelectionParts => _packedSelectionParts;
|
PackedSelectionParts => _packedSelectionParts;
|
||||||
|
|
||||||
|
internal PackedClassificationCacheSnapshot
|
||||||
|
PackedClassificationSnapshot =>
|
||||||
|
_packedClassificationCache.Snapshot;
|
||||||
|
|
||||||
internal void BuildPackedDispatcherOracle(
|
internal void BuildPackedDispatcherOracle(
|
||||||
in RenderFrameView view,
|
in RenderFrameView view,
|
||||||
uint tupleLandblockId,
|
uint tupleLandblockId,
|
||||||
|
|
@ -60,6 +66,7 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
_packedSubmissions.Clear();
|
_packedSubmissions.Clear();
|
||||||
_packedSelectionParts.Clear();
|
_packedSelectionParts.Clear();
|
||||||
_packedSelectionKeys.Clear();
|
_packedSelectionKeys.Clear();
|
||||||
|
_packedClassificationCache.BeginFrame(view.Generation);
|
||||||
|
|
||||||
ReadOnlySpan<RenderFrameEntityCandidate> entities =
|
ReadOnlySpan<RenderFrameEntityCandidate> entities =
|
||||||
view.EntityCandidates;
|
view.EntityCandidates;
|
||||||
|
|
@ -132,6 +139,7 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
in source,
|
in source,
|
||||||
tupleLandblockId);
|
tupleLandblockId);
|
||||||
ClassifyPackedEntity(
|
ClassifyPackedEntity(
|
||||||
|
in projection,
|
||||||
in candidate,
|
in candidate,
|
||||||
meshParts.Slice(
|
meshParts.Slice(
|
||||||
source.MeshPartOffset,
|
source.MeshPartOffset,
|
||||||
|
|
@ -164,9 +172,12 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
cameraWorldPosition,
|
cameraWorldPosition,
|
||||||
_packedAlphaFingerprintScratch));
|
_packedAlphaFingerprintScratch));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_packedClassificationCache.EndFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ClassifyPackedEntity(
|
private void ClassifyPackedEntity(
|
||||||
|
in RenderProjectionRecord projection,
|
||||||
in RenderInstanceCandidate entity,
|
in RenderInstanceCandidate entity,
|
||||||
ReadOnlySpan<RenderFrameMeshPart> meshParts,
|
ReadOnlySpan<RenderFrameMeshPart> meshParts,
|
||||||
ref uint anyVao)
|
ref uint anyVao)
|
||||||
|
|
@ -194,6 +205,45 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
lighting.Luminosity,
|
lighting.Luminosity,
|
||||||
lighting.Diffuse)
|
lighting.Diffuse)
|
||||||
: new Vector2(0f, 1f);
|
: new Vector2(0f, 1f);
|
||||||
|
|
||||||
|
PackedProjectionClassificationEntry? cacheEntry = null;
|
||||||
|
if (!entity.Animated)
|
||||||
|
{
|
||||||
|
PackedClassificationIdentity identity =
|
||||||
|
PackedClassificationIdentity.From(in projection);
|
||||||
|
if (_packedClassificationCache.TryGetReusable(
|
||||||
|
entity.ProjectionId,
|
||||||
|
in identity,
|
||||||
|
projection.DirtyMask,
|
||||||
|
out cacheEntry))
|
||||||
|
{
|
||||||
|
if (anyVao == 0 && !meshParts.IsEmpty)
|
||||||
|
{
|
||||||
|
ObjectRenderData? firstRenderData =
|
||||||
|
_meshAdapter.TryGetRenderData(
|
||||||
|
meshParts[0].MeshRef.GfxObjId);
|
||||||
|
if (firstRenderData is not null)
|
||||||
|
anyVao = firstRenderData.VAO;
|
||||||
|
}
|
||||||
|
ReplayPackedClassification(
|
||||||
|
cacheEntry!,
|
||||||
|
in entity,
|
||||||
|
slot,
|
||||||
|
lights,
|
||||||
|
indoor,
|
||||||
|
selectionLighting);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheEntry = _packedClassificationCache.BeginRebuild(
|
||||||
|
entity.ProjectionId,
|
||||||
|
in identity);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_packedClassificationCache.RecordAnimatedClassification();
|
||||||
|
}
|
||||||
|
|
||||||
PaletteCompositeIdentity paletteIdentity = default;
|
PaletteCompositeIdentity paletteIdentity = default;
|
||||||
if (entity.PaletteOverride is not null)
|
if (entity.PaletteOverride is not null)
|
||||||
{
|
{
|
||||||
|
|
@ -201,6 +251,7 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
entity.PaletteOverride);
|
entity.PaletteOverride);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool reusableAcrossFrames = !entity.Animated;
|
||||||
for (int meshIndex = 0;
|
for (int meshIndex = 0;
|
||||||
meshIndex < meshParts.Length;
|
meshIndex < meshParts.Length;
|
||||||
meshIndex++)
|
meshIndex++)
|
||||||
|
|
@ -219,7 +270,10 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
ObjectRenderData? renderData =
|
ObjectRenderData? renderData =
|
||||||
_meshAdapter.TryGetRenderData(meshRef.GfxObjId);
|
_meshAdapter.TryGetRenderData(meshRef.GfxObjId);
|
||||||
if (renderData is null)
|
if (renderData is null)
|
||||||
|
{
|
||||||
|
reusableAcrossFrames = false;
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
if (anyVao == 0)
|
if (anyVao == 0)
|
||||||
anyVao = renderData.VAO;
|
anyVao = renderData.VAO;
|
||||||
|
|
||||||
|
|
@ -235,20 +289,26 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
ObjectRenderData? partData =
|
ObjectRenderData? partData =
|
||||||
_meshAdapter.TryGetRenderData(gfxObjId);
|
_meshAdapter.TryGetRenderData(gfxObjId);
|
||||||
if (partData is null)
|
if (partData is null)
|
||||||
|
{
|
||||||
|
reusableAcrossFrames = false;
|
||||||
continue;
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
float opacity = PackedPartOpacity(
|
float opacity = PackedPartOpacity(
|
||||||
entity.LocalEntityId,
|
entity.LocalEntityId,
|
||||||
(uint)setupPartIndex);
|
(uint)setupPartIndex);
|
||||||
|
if (opacity < 1f)
|
||||||
|
reusableAcrossFrames = false;
|
||||||
if (opacity <= 0f)
|
if (opacity <= 0f)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
Matrix4x4 model = ComposePartWorldMatrix(
|
Matrix4x4 restPose =
|
||||||
entity.RootWorld,
|
partTransform * meshRef.PartTransform;
|
||||||
meshRef.PartTransform,
|
Matrix4x4 model =
|
||||||
partTransform);
|
restPose * entity.RootWorld;
|
||||||
ClassifyPackedBatches(
|
if (!ClassifyPackedBatches(
|
||||||
partData,
|
partData,
|
||||||
|
restPose,
|
||||||
model,
|
model,
|
||||||
in entity,
|
in entity,
|
||||||
meshRef,
|
meshRef,
|
||||||
|
|
@ -257,12 +317,22 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
lights,
|
lights,
|
||||||
indoor,
|
indoor,
|
||||||
selectionLighting,
|
selectionLighting,
|
||||||
opacity);
|
opacity,
|
||||||
|
cacheEntry))
|
||||||
|
{
|
||||||
|
reusableAcrossFrames = false;
|
||||||
|
}
|
||||||
|
int selectionPartIndex = unchecked(
|
||||||
|
(partIndex << 16)
|
||||||
|
| (setupPartIndex & 0xFFFF));
|
||||||
|
cacheEntry?.SelectionParts.Add(
|
||||||
|
new PackedClassifiedSelectionPart(
|
||||||
|
selectionPartIndex,
|
||||||
|
(uint)gfxObjId,
|
||||||
|
restPose));
|
||||||
AddPackedSelectionPart(
|
AddPackedSelectionPart(
|
||||||
in entity,
|
in entity,
|
||||||
unchecked(
|
selectionPartIndex,
|
||||||
(partIndex << 16)
|
|
||||||
| (setupPartIndex & 0xFFFF)),
|
|
||||||
(uint)gfxObjId,
|
(uint)gfxObjId,
|
||||||
model);
|
model);
|
||||||
}
|
}
|
||||||
|
|
@ -272,13 +342,16 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
float opacity = PackedPartOpacity(
|
float opacity = PackedPartOpacity(
|
||||||
entity.LocalEntityId,
|
entity.LocalEntityId,
|
||||||
0u);
|
0u);
|
||||||
|
if (opacity < 1f)
|
||||||
|
reusableAcrossFrames = false;
|
||||||
if (opacity <= 0f)
|
if (opacity <= 0f)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
Matrix4x4 model =
|
Matrix4x4 restPose = meshRef.PartTransform;
|
||||||
meshRef.PartTransform * entity.RootWorld;
|
Matrix4x4 model = restPose * entity.RootWorld;
|
||||||
ClassifyPackedBatches(
|
if (!ClassifyPackedBatches(
|
||||||
renderData,
|
renderData,
|
||||||
|
restPose,
|
||||||
model,
|
model,
|
||||||
in entity,
|
in entity,
|
||||||
meshRef,
|
meshRef,
|
||||||
|
|
@ -287,7 +360,16 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
lights,
|
lights,
|
||||||
indoor,
|
indoor,
|
||||||
selectionLighting,
|
selectionLighting,
|
||||||
opacity);
|
opacity,
|
||||||
|
cacheEntry))
|
||||||
|
{
|
||||||
|
reusableAcrossFrames = false;
|
||||||
|
}
|
||||||
|
cacheEntry?.SelectionParts.Add(
|
||||||
|
new PackedClassifiedSelectionPart(
|
||||||
|
partIndex,
|
||||||
|
meshRef.GfxObjId,
|
||||||
|
restPose));
|
||||||
AddPackedSelectionPart(
|
AddPackedSelectionPart(
|
||||||
in entity,
|
in entity,
|
||||||
partIndex,
|
partIndex,
|
||||||
|
|
@ -295,6 +377,13 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
model);
|
model);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cacheEntry is not null)
|
||||||
|
{
|
||||||
|
_packedClassificationCache.CompleteRebuild(
|
||||||
|
cacheEntry,
|
||||||
|
reusableAcrossFrames);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
@ -324,8 +413,9 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
: 1f - translucency;
|
: 1f - translucency;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ClassifyPackedBatches(
|
private bool ClassifyPackedBatches(
|
||||||
ObjectRenderData renderData,
|
ObjectRenderData renderData,
|
||||||
|
Matrix4x4 restPose,
|
||||||
Matrix4x4 model,
|
Matrix4x4 model,
|
||||||
in RenderInstanceCandidate entity,
|
in RenderInstanceCandidate entity,
|
||||||
MeshRef meshRef,
|
MeshRef meshRef,
|
||||||
|
|
@ -334,8 +424,10 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
InstanceLightSet lights,
|
InstanceLightSet lights,
|
||||||
bool indoor,
|
bool indoor,
|
||||||
Vector2 selectionLighting,
|
Vector2 selectionLighting,
|
||||||
float opacity)
|
float opacity,
|
||||||
|
PackedProjectionClassificationEntry? cacheEntry)
|
||||||
{
|
{
|
||||||
|
bool reusableAcrossFrames = true;
|
||||||
for (int batchIndex = 0;
|
for (int batchIndex = 0;
|
||||||
batchIndex < renderData.Batches.Count;
|
batchIndex < renderData.Batches.Count;
|
||||||
batchIndex++)
|
batchIndex++)
|
||||||
|
|
@ -351,7 +443,9 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
meshRef,
|
meshRef,
|
||||||
batch,
|
batch,
|
||||||
paletteIdentity,
|
paletteIdentity,
|
||||||
out _);
|
out bool compositePending);
|
||||||
|
if (compositePending)
|
||||||
|
reusableAcrossFrames = false;
|
||||||
if (texture.Handle == 0)
|
if (texture.Handle == 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
|
@ -363,17 +457,78 @@ public sealed unsafe partial class WbDrawDispatcher
|
||||||
texture.Layer,
|
texture.Layer,
|
||||||
translucency,
|
translucency,
|
||||||
batch.CullMode);
|
batch.CullMode);
|
||||||
InstanceGroup group = GetOrCreatePackedGroup(key);
|
var classified = new PackedClassifiedBatch(
|
||||||
group.Matrices.Add(model);
|
key,
|
||||||
group.LocalSortCenters.Add(renderData.SortCenter);
|
restPose,
|
||||||
group.SubmissionOrders.Add(
|
renderData.SortCenter);
|
||||||
_nextPackedInstanceSubmissionOrder++);
|
cacheEntry?.Batches.Add(classified);
|
||||||
group.Slots.Add(slot);
|
AppendPackedClassification(
|
||||||
group.LightSets.Add(lights);
|
in classified,
|
||||||
group.IndoorFlags.Add(indoor ? 1u : 0u);
|
model,
|
||||||
group.Opacities.Add(opacity);
|
slot,
|
||||||
group.SelectionLighting.Add(selectionLighting);
|
lights,
|
||||||
|
indoor,
|
||||||
|
opacity,
|
||||||
|
selectionLighting);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return reusableAcrossFrames;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReplayPackedClassification(
|
||||||
|
PackedProjectionClassificationEntry entry,
|
||||||
|
in RenderInstanceCandidate entity,
|
||||||
|
uint slot,
|
||||||
|
InstanceLightSet lights,
|
||||||
|
bool indoor,
|
||||||
|
Vector2 selectionLighting)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < entry.Batches.Count; i++)
|
||||||
|
{
|
||||||
|
PackedClassifiedBatch classified = entry.Batches[i];
|
||||||
|
AppendPackedClassification(
|
||||||
|
in classified,
|
||||||
|
classified.RestPose * entity.RootWorld,
|
||||||
|
slot,
|
||||||
|
lights,
|
||||||
|
indoor,
|
||||||
|
opacity: 1f,
|
||||||
|
selectionLighting);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < entry.SelectionParts.Count; i++)
|
||||||
|
{
|
||||||
|
PackedClassifiedSelectionPart part =
|
||||||
|
entry.SelectionParts[i];
|
||||||
|
AddPackedSelectionPart(
|
||||||
|
in entity,
|
||||||
|
part.PartIndex,
|
||||||
|
part.GfxObjId,
|
||||||
|
part.RestPose * entity.RootWorld);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AppendPackedClassification(
|
||||||
|
in PackedClassifiedBatch classified,
|
||||||
|
Matrix4x4 model,
|
||||||
|
uint slot,
|
||||||
|
InstanceLightSet lights,
|
||||||
|
bool indoor,
|
||||||
|
float opacity,
|
||||||
|
Vector2 selectionLighting)
|
||||||
|
{
|
||||||
|
InstanceGroup group =
|
||||||
|
GetOrCreatePackedGroup(classified.Key);
|
||||||
|
group.Matrices.Add(model);
|
||||||
|
group.LocalSortCenters.Add(
|
||||||
|
classified.LocalSortCenter);
|
||||||
|
group.SubmissionOrders.Add(
|
||||||
|
_nextPackedInstanceSubmissionOrder++);
|
||||||
|
group.Slots.Add(slot);
|
||||||
|
group.LightSets.Add(lights);
|
||||||
|
group.IndoorFlags.Add(indoor ? 1u : 0u);
|
||||||
|
group.Opacities.Add(opacity);
|
||||||
|
group.SelectionLighting.Add(selectionLighting);
|
||||||
}
|
}
|
||||||
|
|
||||||
private InstanceGroup GetOrCreatePackedGroup(GroupKey key)
|
private InstanceGroup GetOrCreatePackedGroup(GroupKey key)
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,26 @@ public sealed class RenderSceneShadowRuntimeTests
|
||||||
Assert.Contains("sourceChannel=static", snapshot.FirstMismatch);
|
Assert.Contains("sourceChannel=static", snapshot.FirstMismatch);
|
||||||
Assert.Contains("field=transform", snapshot.FirstMismatch);
|
Assert.Contains("field=transform", snapshot.FirstMismatch);
|
||||||
Assert.Single(logs);
|
Assert.Single(logs);
|
||||||
|
Assert.Equal(1, snapshot.SceneIndexCounts.Dirty);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PackedProductOwnership_LeavesDirtyAcknowledgementToProduct()
|
||||||
|
{
|
||||||
|
WorldEntity entity = Entity(23, serverGuid: 0, parentCell: null);
|
||||||
|
CurrentRenderSceneOracle oracle = Capture(entity, []);
|
||||||
|
using var shadow = new RenderSceneShadowRuntime(Generation(1));
|
||||||
|
Register(shadow, entity, RenderProjectionClass.OutdoorStatic);
|
||||||
|
shadow.DrainUpdateBoundary();
|
||||||
|
var controller = new RenderSceneShadowComparisonController(
|
||||||
|
shadow,
|
||||||
|
oracle,
|
||||||
|
acknowledgeDirty: false);
|
||||||
|
|
||||||
|
controller.CompareNow();
|
||||||
|
|
||||||
|
Assert.Equal(1uL, controller.Snapshot.SuccessfulComparisonCount);
|
||||||
|
Assert.Equal(1, controller.Snapshot.SceneIndexCounts.Dirty);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,221 @@
|
||||||
|
using AcDream.App.Rendering.Scene;
|
||||||
|
using AcDream.App.Rendering.Wb;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.Rendering.Wb;
|
||||||
|
|
||||||
|
public sealed class PackedProjectionClassificationCacheTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void UnchangedProjection_ReusesClassificationAcrossFrames()
|
||||||
|
{
|
||||||
|
var cache = new PackedProjectionClassificationCache();
|
||||||
|
RenderSceneGeneration generation =
|
||||||
|
RenderSceneGeneration.FromRaw(1);
|
||||||
|
RenderProjectionId id = RenderProjectionId.FromRaw(0x1234);
|
||||||
|
PackedClassificationIdentity identity = Identity(7);
|
||||||
|
|
||||||
|
cache.BeginFrame(generation);
|
||||||
|
PackedProjectionClassificationEntry entry =
|
||||||
|
cache.BeginRebuild(id, in identity);
|
||||||
|
cache.CompleteRebuild(entry, reusableAcrossFrames: true);
|
||||||
|
cache.EndFrame();
|
||||||
|
|
||||||
|
cache.BeginFrame(generation);
|
||||||
|
Assert.True(cache.TryGetReusable(
|
||||||
|
id,
|
||||||
|
in identity,
|
||||||
|
RenderDirtyMask.Transform,
|
||||||
|
out PackedProjectionClassificationEntry? reused));
|
||||||
|
cache.EndFrame();
|
||||||
|
|
||||||
|
Assert.Same(entry, reused);
|
||||||
|
Assert.Equal(0, cache.Snapshot.StaticRebuildCount);
|
||||||
|
Assert.Equal(1, cache.Snapshot.CrossFrameReuseCount);
|
||||||
|
Assert.Equal(1, cache.Snapshot.ProjectionCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AppearanceDirty_RequiresRebuildButTransformDirtyDoesNot()
|
||||||
|
{
|
||||||
|
var cache = new PackedProjectionClassificationCache();
|
||||||
|
RenderSceneGeneration generation =
|
||||||
|
RenderSceneGeneration.FromRaw(1);
|
||||||
|
RenderProjectionId id = RenderProjectionId.FromRaw(0x1234);
|
||||||
|
PackedClassificationIdentity identity = Identity(7);
|
||||||
|
|
||||||
|
cache.BeginFrame(generation);
|
||||||
|
PackedProjectionClassificationEntry entry =
|
||||||
|
cache.BeginRebuild(id, in identity);
|
||||||
|
cache.CompleteRebuild(entry, reusableAcrossFrames: true);
|
||||||
|
cache.EndFrame();
|
||||||
|
|
||||||
|
cache.BeginFrame(generation);
|
||||||
|
Assert.False(cache.TryGetReusable(
|
||||||
|
id,
|
||||||
|
in identity,
|
||||||
|
RenderDirtyMask.Appearance,
|
||||||
|
out _));
|
||||||
|
PackedProjectionClassificationEntry rebuilt =
|
||||||
|
cache.BeginRebuild(id, in identity);
|
||||||
|
cache.CompleteRebuild(rebuilt, reusableAcrossFrames: true);
|
||||||
|
|
||||||
|
Assert.Same(entry, rebuilt);
|
||||||
|
Assert.Equal(1, cache.Snapshot.StaticRebuildCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IncompleteBuild_CanReplayDuplicateRouteOnlyInSameFrame()
|
||||||
|
{
|
||||||
|
var cache = new PackedProjectionClassificationCache();
|
||||||
|
RenderSceneGeneration generation =
|
||||||
|
RenderSceneGeneration.FromRaw(1);
|
||||||
|
RenderProjectionId id = RenderProjectionId.FromRaw(0x1234);
|
||||||
|
PackedClassificationIdentity identity = Identity(7);
|
||||||
|
|
||||||
|
cache.BeginFrame(generation);
|
||||||
|
PackedProjectionClassificationEntry entry =
|
||||||
|
cache.BeginRebuild(id, in identity);
|
||||||
|
cache.CompleteRebuild(entry, reusableAcrossFrames: false);
|
||||||
|
|
||||||
|
Assert.True(cache.TryGetReusable(
|
||||||
|
id,
|
||||||
|
in identity,
|
||||||
|
RenderDirtyMask.All,
|
||||||
|
out _));
|
||||||
|
Assert.Equal(1, cache.Snapshot.SameFrameReuseCount);
|
||||||
|
cache.EndFrame();
|
||||||
|
|
||||||
|
cache.BeginFrame(generation);
|
||||||
|
Assert.False(cache.TryGetReusable(
|
||||||
|
id,
|
||||||
|
in identity,
|
||||||
|
RenderDirtyMask.None,
|
||||||
|
out _));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IncarnationOrAppearanceChange_CannotReuseRetainedPayload()
|
||||||
|
{
|
||||||
|
var cache = new PackedProjectionClassificationCache();
|
||||||
|
RenderSceneGeneration generation =
|
||||||
|
RenderSceneGeneration.FromRaw(1);
|
||||||
|
RenderProjectionId id = RenderProjectionId.FromRaw(0x1234);
|
||||||
|
PackedClassificationIdentity original = Identity(7);
|
||||||
|
|
||||||
|
cache.BeginFrame(generation);
|
||||||
|
PackedProjectionClassificationEntry entry =
|
||||||
|
cache.BeginRebuild(id, in original);
|
||||||
|
cache.CompleteRebuild(entry, reusableAcrossFrames: true);
|
||||||
|
cache.EndFrame();
|
||||||
|
|
||||||
|
cache.BeginFrame(generation);
|
||||||
|
PackedClassificationIdentity replacement = Identity(17);
|
||||||
|
Assert.False(cache.TryGetReusable(
|
||||||
|
id,
|
||||||
|
in replacement,
|
||||||
|
RenderDirtyMask.None,
|
||||||
|
out _));
|
||||||
|
PackedProjectionClassificationEntry rebuilt =
|
||||||
|
cache.BeginRebuild(id, in replacement);
|
||||||
|
cache.CompleteRebuild(rebuilt, reusableAcrossFrames: true);
|
||||||
|
cache.EndFrame();
|
||||||
|
|
||||||
|
Assert.Same(entry, rebuilt);
|
||||||
|
Assert.Equal(replacement, rebuilt.Identity);
|
||||||
|
Assert.Equal(1, cache.Snapshot.StaticRebuildCount);
|
||||||
|
Assert.Equal(0, cache.Snapshot.CrossFrameReuseCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GenerationReplacementAndUnseenProjectionReleaseRetainedEntries()
|
||||||
|
{
|
||||||
|
var cache = new PackedProjectionClassificationCache();
|
||||||
|
RenderProjectionId id = RenderProjectionId.FromRaw(0x1234);
|
||||||
|
PackedClassificationIdentity identity = Identity(7);
|
||||||
|
|
||||||
|
cache.BeginFrame(RenderSceneGeneration.FromRaw(1));
|
||||||
|
PackedProjectionClassificationEntry entry =
|
||||||
|
cache.BeginRebuild(id, in identity);
|
||||||
|
entry.Batches.Capacity = 32;
|
||||||
|
entry.SelectionParts.Capacity = 16;
|
||||||
|
cache.CompleteRebuild(entry, reusableAcrossFrames: true);
|
||||||
|
cache.EndFrame();
|
||||||
|
Assert.True(cache.Snapshot.RetainedPayloadBytes > 0);
|
||||||
|
|
||||||
|
cache.BeginFrame(RenderSceneGeneration.FromRaw(1));
|
||||||
|
cache.EndFrame();
|
||||||
|
|
||||||
|
Assert.Equal(0, cache.Snapshot.ProjectionCount);
|
||||||
|
Assert.Equal(1, cache.Snapshot.RetiredProjectionCount);
|
||||||
|
Assert.Equal(0, entry.Batches.Capacity);
|
||||||
|
Assert.Equal(0, entry.SelectionParts.Capacity);
|
||||||
|
Assert.Equal(0, cache.Snapshot.RetainedPayloadBytes);
|
||||||
|
|
||||||
|
cache.BeginFrame(RenderSceneGeneration.FromRaw(2));
|
||||||
|
Assert.Equal(0, cache.Snapshot.ProjectionCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WarmUnchangedProjectionCache_AllocatesZeroBytes()
|
||||||
|
{
|
||||||
|
var cache = new PackedProjectionClassificationCache();
|
||||||
|
RenderSceneGeneration generation =
|
||||||
|
RenderSceneGeneration.FromRaw(1);
|
||||||
|
RenderProjectionId id = RenderProjectionId.FromRaw(0x1234);
|
||||||
|
PackedClassificationIdentity identity = Identity(7);
|
||||||
|
|
||||||
|
cache.BeginFrame(generation);
|
||||||
|
PackedProjectionClassificationEntry entry =
|
||||||
|
cache.BeginRebuild(id, in identity);
|
||||||
|
cache.CompleteRebuild(entry, reusableAcrossFrames: true);
|
||||||
|
cache.EndFrame();
|
||||||
|
|
||||||
|
for (int i = 0; i < 100; i++)
|
||||||
|
Hit(cache, generation, id, identity);
|
||||||
|
|
||||||
|
long before = GC.GetAllocatedBytesForCurrentThread();
|
||||||
|
for (int i = 0; i < 1_000; i++)
|
||||||
|
Hit(cache, generation, id, identity);
|
||||||
|
long allocated =
|
||||||
|
GC.GetAllocatedBytesForCurrentThread() - before;
|
||||||
|
|
||||||
|
Assert.Equal(0, allocated);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Hit(
|
||||||
|
PackedProjectionClassificationCache cache,
|
||||||
|
RenderSceneGeneration generation,
|
||||||
|
RenderProjectionId id,
|
||||||
|
PackedClassificationIdentity identity)
|
||||||
|
{
|
||||||
|
cache.BeginFrame(generation);
|
||||||
|
if (!cache.TryGetReusable(
|
||||||
|
id,
|
||||||
|
in identity,
|
||||||
|
RenderDirtyMask.None,
|
||||||
|
out _))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"The warmed projection classification was not reusable.");
|
||||||
|
}
|
||||||
|
cache.EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PackedClassificationIdentity Identity(ulong seed) =>
|
||||||
|
new(
|
||||||
|
RenderOwnerIncarnation.FromRaw(seed),
|
||||||
|
new RenderMeshSet(
|
||||||
|
RenderAssetHandle.FromRaw(seed + 1),
|
||||||
|
MeshCount: 2,
|
||||||
|
Revision: seed + 2),
|
||||||
|
new RenderMaterialVariant(
|
||||||
|
PaletteKey: seed + 3,
|
||||||
|
TextureReplacementKey: seed + 4,
|
||||||
|
Opacity: 1f),
|
||||||
|
new RenderDegradeState(
|
||||||
|
Level: 0,
|
||||||
|
Revision: (uint)(seed + 5)),
|
||||||
|
new RenderSceneHash128(seed + 6, seed + 7),
|
||||||
|
new RenderSceneHash128(seed + 8, seed + 9));
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue