From 129dd77ddde1e50869f696e0de7a1455b3a2b629 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 25 Jul 2026 04:27:17 +0200 Subject: [PATCH] fix(rendering): decouple portal warmup discovery Scan published destination candidates under a separate bounded budget from expensive mesh and composite preparation so large retained worlds cannot exhaust the reveal window before candidate discovery completes. The correction is valid on both the retained and pre-cutover draw paths, so the G4 visual rollback remains the single commit ef1d263337997bb030eadb7b8e71d73dc659907a. Co-authored-by: OpenAI Codex --- .../Rendering/Wb/WbDrawDispatcher.cs | 27 +++++++++++++++---- .../WbDrawDispatcherCompositeWarmupTests.cs | 22 +++++++++++++++ 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs index 1eca6cd5..b6d50e74 100644 --- a/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs +++ b/src/AcDream.App/Rendering/Wb/WbDrawDispatcher.cs @@ -114,7 +114,13 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable public DrawStats LastDrawStats { get; private set; } public bool CompositeTexturesReady { get; private set; } = true; internal int LastCompositeWarmupPendingCount { get; private set; } - internal const int MaximumCompositeWarmupEntitiesPerFrame = 128; + // Candidate discovery only reads already-published entity facts. Keep it + // independently bounded from the materially more expensive mesh/texture + // preparation below so a large retained Far-tier world cannot consume the + // entire portal reveal window merely proving that most entities are + // outside the destination neighborhood. + internal const int MaximumCompositeWarmupScanEntitiesPerFrame = 4096; + internal const int MaximumCompositeWarmupPrepareEntitiesPerFrame = 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 @@ -188,9 +194,9 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable _compositeWarmupScanIndex = Math.Min(_compositeWarmupScanIndex, entities.Count); - int scanEnd = Math.Min( - entities.Count, - _compositeWarmupScanIndex + MaximumCompositeWarmupEntitiesPerFrame); + int scanEnd = CompositeWarmupScanEnd( + _compositeWarmupScanIndex, + entities.Count); for (; _compositeWarmupScanIndex < scanEnd; _compositeWarmupScanIndex++) { WorldEntity entity = entities[_compositeWarmupScanIndex]; @@ -211,7 +217,7 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable int candidatesThisPass = Math.Min( _compositeWarmupQueue.Count, - MaximumCompositeWarmupEntitiesPerFrame); + MaximumCompositeWarmupPrepareEntitiesPerFrame); for (int i = 0; i < candidatesThisPass; i++) { WorldEntity entity = _compositeWarmupQueue.Dequeue(); @@ -245,6 +251,17 @@ public sealed unsafe partial class WbDrawDispatcher : IDisposable ulong entityGeneration) => scanComplete && scanGeneration != entityGeneration; + internal static int CompositeWarmupScanEnd( + int scanIndex, + int entityCount) + { + ArgumentOutOfRangeException.ThrowIfNegative(scanIndex); + ArgumentOutOfRangeException.ThrowIfNegative(entityCount); + return Math.Min( + entityCount, + scanIndex + MaximumCompositeWarmupScanEntitiesPerFrame); + } + private void RebuildCompositeWarmupQueue( IReadOnlyList entities, ulong entityGeneration, diff --git a/tests/AcDream.App.Tests/Rendering/Wb/WbDrawDispatcherCompositeWarmupTests.cs b/tests/AcDream.App.Tests/Rendering/Wb/WbDrawDispatcherCompositeWarmupTests.cs index b698e96d..ebd097d0 100644 --- a/tests/AcDream.App.Tests/Rendering/Wb/WbDrawDispatcherCompositeWarmupTests.cs +++ b/tests/AcDream.App.Tests/Rendering/Wb/WbDrawDispatcherCompositeWarmupTests.cs @@ -56,6 +56,28 @@ public sealed class WbDrawDispatcherCompositeWarmupTests entityGeneration: 42)); } + [Fact] + public void LargeRetainedWorldDiscoveryConvergesIndependentlyOfUploadBudget() + { + const int entityCount = 21_025; + int scanIndex = 0; + int frameCount = 0; + + while (scanIndex < entityCount) + { + scanIndex = WbDrawDispatcher.CompositeWarmupScanEnd( + scanIndex, + entityCount); + frameCount++; + } + + Assert.Equal(6, frameCount); + Assert.Equal(entityCount, scanIndex); + Assert.True( + WbDrawDispatcher.MaximumCompositeWarmupScanEntitiesPerFrame + > WbDrawDispatcher.MaximumCompositeWarmupPrepareEntitiesPerFrame); + } + [Fact] public void PaletteEntityInsideDestinationRadiusIsCandidate() {