fix(render): make reveal warmup mutation-resumable

Retain composite candidate progress across live membership generations and run a stable follow-up pass after churn, preventing ACE object-stream updates from resetting portal readiness forever.

Co-authored-by: Erik Nilsson <erikn@users.noreply.github.com>
This commit is contained in:
Erik 2026-07-24 19:54:40 +02:00
parent a77ba06722
commit e991eeca34
2 changed files with 63 additions and 12 deletions

View file

@ -114,6 +114,10 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
internal int LastCompositeWarmupPendingCount { get; private set; } internal int LastCompositeWarmupPendingCount { get; private set; }
internal const int MaximumCompositeWarmupEntitiesPerFrame = 128; internal const int MaximumCompositeWarmupEntitiesPerFrame = 128;
private readonly Queue<WorldEntity> _compositeWarmupQueue = new(); private readonly Queue<WorldEntity> _compositeWarmupQueue = new();
// Membership may change while ACE is streaming the destination object set.
// Retain exact candidate progress and schedule a follow-up pass instead of
// restarting at index zero on every generation edge.
private readonly HashSet<WorldEntity> _compositeWarmupTracked = [];
private IReadOnlyList<WorldEntity>? _compositeWarmupSource; private IReadOnlyList<WorldEntity>? _compositeWarmupSource;
private ulong _compositeWarmupSourceGeneration; private ulong _compositeWarmupSourceGeneration;
private uint _compositeWarmupDestinationCell; private uint _compositeWarmupDestinationCell;
@ -133,6 +137,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
CompositeTexturesReady = false; CompositeTexturesReady = false;
LastCompositeWarmupPendingCount = 1; LastCompositeWarmupPendingCount = 1;
_compositeWarmupQueue.Clear(); _compositeWarmupQueue.Clear();
_compositeWarmupTracked.Clear();
_compositeWarmupSource = null; _compositeWarmupSource = null;
_compositeWarmupSourceGeneration = 0; _compositeWarmupSourceGeneration = 0;
_compositeWarmupDestinationCell = 0; _compositeWarmupDestinationCell = 0;
@ -159,9 +164,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
_compositeWarmupSource, _compositeWarmupSource,
_compositeWarmupDestinationCell, _compositeWarmupDestinationCell,
_compositeWarmupRadius, _compositeWarmupRadius,
_compositeWarmupSourceGeneration,
entities, entities,
entityGeneration,
destinationCell, destinationCell,
radius)) radius))
{ {
@ -171,19 +174,38 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
destinationCell, destinationCell,
radius); radius);
} }
else if (ShouldBeginCompositeWarmupRescan(
_compositeWarmupScanComplete,
_compositeWarmupSourceGeneration,
entityGeneration))
{
BeginCompositeWarmupRescan(entities.Count, entityGeneration);
}
if (CompositeTexturesReady) if (CompositeTexturesReady)
return; return;
_compositeWarmupScanIndex =
Math.Min(_compositeWarmupScanIndex, entities.Count);
int scanEnd = Math.Min( int scanEnd = Math.Min(
entities.Count, entities.Count,
_compositeWarmupScanIndex + MaximumCompositeWarmupEntitiesPerFrame); _compositeWarmupScanIndex + MaximumCompositeWarmupEntitiesPerFrame);
for (; _compositeWarmupScanIndex < scanEnd; _compositeWarmupScanIndex++) for (; _compositeWarmupScanIndex < scanEnd; _compositeWarmupScanIndex++)
{ {
WorldEntity entity = entities[_compositeWarmupScanIndex]; WorldEntity entity = entities[_compositeWarmupScanIndex];
if (IsCompositeWarmupCandidate(entity, destinationCell, radius)) if (IsCompositeWarmupCandidate(entity, destinationCell, radius)
&& _compositeWarmupTracked.Add(entity))
{
_compositeWarmupQueue.Enqueue(entity); _compositeWarmupQueue.Enqueue(entity);
}
} }
_compositeWarmupScanComplete = _compositeWarmupScanIndex == entities.Count; _compositeWarmupScanComplete = _compositeWarmupScanIndex == entities.Count;
if (ShouldBeginCompositeWarmupRescan(
_compositeWarmupScanComplete,
_compositeWarmupSourceGeneration,
entityGeneration))
{
BeginCompositeWarmupRescan(entities.Count, entityGeneration);
}
int candidatesThisPass = Math.Min( int candidatesThisPass = Math.Min(
_compositeWarmupQueue.Count, _compositeWarmupQueue.Count,
@ -208,16 +230,19 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
IReadOnlyList<WorldEntity>? currentSource, IReadOnlyList<WorldEntity>? currentSource,
uint currentDestinationCell, uint currentDestinationCell,
int currentRadius, int currentRadius,
ulong currentGeneration,
IReadOnlyList<WorldEntity> nextSource, IReadOnlyList<WorldEntity> nextSource,
ulong nextGeneration,
uint nextDestinationCell, uint nextDestinationCell,
int nextRadius) => int nextRadius) =>
!ReferenceEquals(currentSource, nextSource) !ReferenceEquals(currentSource, nextSource)
|| currentGeneration != nextGeneration
|| currentDestinationCell != nextDestinationCell || currentDestinationCell != nextDestinationCell
|| currentRadius != nextRadius; || currentRadius != nextRadius;
internal static bool ShouldBeginCompositeWarmupRescan(
bool scanComplete,
ulong scanGeneration,
ulong entityGeneration) =>
scanComplete && scanGeneration != entityGeneration;
private void RebuildCompositeWarmupQueue( private void RebuildCompositeWarmupQueue(
IReadOnlyList<WorldEntity> entities, IReadOnlyList<WorldEntity> entities,
ulong entityGeneration, ulong entityGeneration,
@ -225,6 +250,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
int radius) int radius)
{ {
_compositeWarmupQueue.Clear(); _compositeWarmupQueue.Clear();
_compositeWarmupTracked.Clear();
_compositeWarmupSource = entities; _compositeWarmupSource = entities;
_compositeWarmupSourceGeneration = entityGeneration; _compositeWarmupSourceGeneration = entityGeneration;
_compositeWarmupDestinationCell = destinationCell; _compositeWarmupDestinationCell = destinationCell;
@ -235,6 +261,17 @@ public sealed unsafe class WbDrawDispatcher : IDisposable
CompositeTexturesReady = _compositeWarmupScanComplete; CompositeTexturesReady = _compositeWarmupScanComplete;
} }
private void BeginCompositeWarmupRescan(
int entityCount,
ulong entityGeneration)
{
_compositeWarmupSourceGeneration = entityGeneration;
_compositeWarmupScanIndex = 0;
_compositeWarmupScanComplete = entityCount == 0;
CompositeTexturesReady =
_compositeWarmupScanComplete && _compositeWarmupQueue.Count == 0;
}
internal static bool IsCompositeWarmupCandidate( internal static bool IsCompositeWarmupCandidate(
WorldEntity entity, WorldEntity entity,
uint destinationCell, uint destinationCell,

View file

@ -19,29 +19,43 @@ public sealed class WbDrawDispatcherCompositeWarmupTests
emptyReadySnapshot, emptyReadySnapshot,
Destination, Destination,
currentRadius: 0, currentRadius: 0,
currentGeneration: 0,
publishedDestinationSnapshot, publishedDestinationSnapshot,
nextGeneration: 1,
Destination, Destination,
nextRadius: 0)); nextRadius: 0));
} }
[Fact] [Fact]
public void MutatedStableEntityViewRequiresRebuildWhenGenerationChanges() public void MutatedStableEntityViewRetainsScopeWhenGenerationChanges()
{ {
IReadOnlyList<WorldEntity> stableView = new List<WorldEntity>(); IReadOnlyList<WorldEntity> stableView = new List<WorldEntity>();
Assert.True(WbDrawDispatcher.RequiresCompositeWarmupRebuild( Assert.False(WbDrawDispatcher.RequiresCompositeWarmupRebuild(
stableView, stableView,
Destination, Destination,
currentRadius: 0, currentRadius: 0,
currentGeneration: 41,
stableView, stableView,
nextGeneration: 42,
Destination, Destination,
nextRadius: 0)); nextRadius: 0));
} }
[Fact]
public void MembershipMutationDoesNotRestartAnIncompletePass()
{
Assert.False(WbDrawDispatcher.ShouldBeginCompositeWarmupRescan(
scanComplete: false,
scanGeneration: 41,
entityGeneration: 42));
}
[Fact]
public void MembershipMutationSchedulesFollowUpAfterPassCompletes()
{
Assert.True(WbDrawDispatcher.ShouldBeginCompositeWarmupRescan(
scanComplete: true,
scanGeneration: 41,
entityGeneration: 42));
}
[Fact] [Fact]
public void PaletteEntityInsideDestinationRadiusIsCandidate() public void PaletteEntityInsideDestinationRadiusIsCandidate()
{ {