From e991eeca344648a5ef3b48a7ccf6e1dab0ae6510 Mon Sep 17 00:00:00 2001 From: Erik Date: Fri, 24 Jul 2026 19:54:40 +0200 Subject: [PATCH] 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 --- .../Rendering/Wb/WbDrawDispatcher.cs | 49 ++++++++++++++++--- .../WbDrawDispatcherCompositeWarmupTests.cs | 26 +++++++--- 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs index a6f0b914..a887a0f5 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs @@ -114,6 +114,10 @@ public sealed unsafe class WbDrawDispatcher : IDisposable internal int LastCompositeWarmupPendingCount { get; private set; } internal const int MaximumCompositeWarmupEntitiesPerFrame = 128; private readonly Queue _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 _compositeWarmupTracked = []; private IReadOnlyList? _compositeWarmupSource; private ulong _compositeWarmupSourceGeneration; private uint _compositeWarmupDestinationCell; @@ -133,6 +137,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable CompositeTexturesReady = false; LastCompositeWarmupPendingCount = 1; _compositeWarmupQueue.Clear(); + _compositeWarmupTracked.Clear(); _compositeWarmupSource = null; _compositeWarmupSourceGeneration = 0; _compositeWarmupDestinationCell = 0; @@ -159,9 +164,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable _compositeWarmupSource, _compositeWarmupDestinationCell, _compositeWarmupRadius, - _compositeWarmupSourceGeneration, entities, - entityGeneration, destinationCell, radius)) { @@ -171,19 +174,38 @@ public sealed unsafe class WbDrawDispatcher : IDisposable destinationCell, radius); } + else if (ShouldBeginCompositeWarmupRescan( + _compositeWarmupScanComplete, + _compositeWarmupSourceGeneration, + entityGeneration)) + { + BeginCompositeWarmupRescan(entities.Count, entityGeneration); + } if (CompositeTexturesReady) return; + _compositeWarmupScanIndex = + Math.Min(_compositeWarmupScanIndex, entities.Count); int scanEnd = Math.Min( entities.Count, _compositeWarmupScanIndex + MaximumCompositeWarmupEntitiesPerFrame); for (; _compositeWarmupScanIndex < scanEnd; _compositeWarmupScanIndex++) { WorldEntity entity = entities[_compositeWarmupScanIndex]; - if (IsCompositeWarmupCandidate(entity, destinationCell, radius)) + if (IsCompositeWarmupCandidate(entity, destinationCell, radius) + && _compositeWarmupTracked.Add(entity)) + { _compositeWarmupQueue.Enqueue(entity); + } } _compositeWarmupScanComplete = _compositeWarmupScanIndex == entities.Count; + if (ShouldBeginCompositeWarmupRescan( + _compositeWarmupScanComplete, + _compositeWarmupSourceGeneration, + entityGeneration)) + { + BeginCompositeWarmupRescan(entities.Count, entityGeneration); + } int candidatesThisPass = Math.Min( _compositeWarmupQueue.Count, @@ -208,16 +230,19 @@ public sealed unsafe class WbDrawDispatcher : IDisposable IReadOnlyList? currentSource, uint currentDestinationCell, int currentRadius, - ulong currentGeneration, IReadOnlyList nextSource, - ulong nextGeneration, uint nextDestinationCell, int nextRadius) => !ReferenceEquals(currentSource, nextSource) - || currentGeneration != nextGeneration || currentDestinationCell != nextDestinationCell || currentRadius != nextRadius; + internal static bool ShouldBeginCompositeWarmupRescan( + bool scanComplete, + ulong scanGeneration, + ulong entityGeneration) => + scanComplete && scanGeneration != entityGeneration; + private void RebuildCompositeWarmupQueue( IReadOnlyList entities, ulong entityGeneration, @@ -225,6 +250,7 @@ public sealed unsafe class WbDrawDispatcher : IDisposable int radius) { _compositeWarmupQueue.Clear(); + _compositeWarmupTracked.Clear(); _compositeWarmupSource = entities; _compositeWarmupSourceGeneration = entityGeneration; _compositeWarmupDestinationCell = destinationCell; @@ -235,6 +261,17 @@ public sealed unsafe class WbDrawDispatcher : IDisposable 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( WorldEntity entity, uint destinationCell, diff --git a/tests/AcDream.App.Tests/Rendering/Wb/WbDrawDispatcherCompositeWarmupTests.cs b/tests/AcDream.App.Tests/Rendering/Wb/WbDrawDispatcherCompositeWarmupTests.cs index 09bbd20a..b698e96d 100644 --- a/tests/AcDream.App.Tests/Rendering/Wb/WbDrawDispatcherCompositeWarmupTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Wb/WbDrawDispatcherCompositeWarmupTests.cs @@ -19,29 +19,43 @@ public sealed class WbDrawDispatcherCompositeWarmupTests emptyReadySnapshot, Destination, currentRadius: 0, - currentGeneration: 0, publishedDestinationSnapshot, - nextGeneration: 1, Destination, nextRadius: 0)); } [Fact] - public void MutatedStableEntityViewRequiresRebuildWhenGenerationChanges() + public void MutatedStableEntityViewRetainsScopeWhenGenerationChanges() { IReadOnlyList stableView = new List(); - Assert.True(WbDrawDispatcher.RequiresCompositeWarmupRebuild( + Assert.False(WbDrawDispatcher.RequiresCompositeWarmupRebuild( stableView, Destination, currentRadius: 0, - currentGeneration: 41, stableView, - nextGeneration: 42, Destination, 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] public void PaletteEntityInsideDestinationRadiusIsCandidate() {