From a1d15a82dd4880880bfb55aafc918b50fe04b032 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 17 Aug 2026 20:55:43 +0200 Subject: [PATCH] perf #418: widen the destination-lane budget only while the reveal hold is active While a destination reservation hides the world behind the authored tunnel, the streaming frame meter now runs a hold-widened profile (StreamingWorkBudget.WidenForDestinationHold): the time ceiling rises from the authored 2 ms to an absolute 8 ms default (ACDREAM_STREAM_WORK_HOLD_DEST_MS is a measurement-only override), every count/byte dimension scales by the same factor so elapsed time stays the authoritative guard (the measured binder is Time at both ceilings), and the reserve fraction is re-derived (0.75 -> 0.9375) so the NON-destination lane's absolute per-frame caps are unchanged. The widening keys off the existing BeginDestinationReservation/ EndDestinationReservation bracket only, is derived per-Tick from the CURRENT budget (mid-hold quality swaps compose), and a frame with no reservation uses the authored budget verbatim (test-pinned). Portal holds ride the same bracket as login holds by construction - intended, and pinned by a kind-parity test through the real coordinator plus a live @telepoi portal hold (kind=portal gate-ready 3589 ms). Why: issue #418's next-hypothesis (1). Measured result: the ~5 s publication drip collapsed to ~2 s (loaded 625/625 at ~3.0 s, tunnel at 64-66 fps), the portal-hold gate-ready fell to ~3.6 s - and login gate-ready/total stayed at 8.4-8.8 s / 12.6-12.7 s, exposing the real remaining pacer: the login-cold render-thread upload/registration barrier behind GpuWorldState.IsRenderReady, which ran concurrently under the old drip. Full attribution appended to docs/ISSUES.md #418; no divergence-register row (the streamed result and reveal gate are byte-identical; only the scheduling rate during a hidden hold changed). Co-Authored-By: Claude Fable 5 --- docs/ISSUES.md | 54 ++++ .../Streaming/StreamingController.cs | 16 +- .../Streaming/StreamingWorkBudget.cs | 50 ++++ .../Streaming/StreamingWorkBudgetOptions.cs | 29 +- .../Streaming/StreamingWorkBudgetTests.cs | 278 ++++++++++++++++++ 5 files changed, 420 insertions(+), 7 deletions(-) diff --git a/docs/ISSUES.md b/docs/ISSUES.md index 4df0c10c..c3441905 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -135,6 +135,60 @@ choreography (TunnelContinue 2–5 s + two 1 s fades) is retail behavior and is NOT a tuning target. Predicted result: gate-ready ~2–3 s + authored exit ≈ 5–8 s total — retail-feel at 2.2x retail's window. +**2026-08-17 (hold-widening round, this commit): hypothesis (1) LANDED — +the drip collapsed exactly as predicted (5 s → 2 s) — and the login total +did NOT move, which is itself the round's finding: a concurrent +render-thread phase was hiding under the drip and is now the exposed long +pole.** The frame meter now runs a hold-widened profile ONLY while a +destination reservation is active +(`StreamingWorkBudget.WidenForDestinationHold`, keyed off the existing +`BeginDestinationReservation`/`EndDestinationReservation` bracket — no new +flags — and derived per-Tick from the CURRENT budget so a mid-hold quality +swap composes): destination-lane time ceiling 2 ms → 8 ms (absolute +default; measurement-only env `ACDREAM_STREAM_WORK_HOLD_DEST_MS`), every +count/byte dimension scaled by the same factor (the measured binder is +TIME — `[stream-tick] yieldReasons=publication-*/Timex…` at both the 2 ms +and the 8 ms ceiling; proportional scaling keeps time authoritative +instead of letting a byte cap become the accidental binder), and the +reserve fraction re-derived (0.75 → 0.9375) so the NON-destination lane's +absolute caps are unchanged (floor-exact at defaults: +floor(256×0.0625) = floor(64×0.25) = 16). No-hold frames use the authored +budget verbatim (`NoReservation_UsesTheAuthoredBudgetUnchanged` pins it); +portal holds ride the same bracket by construction +(`HoldWidening_AppliesToLoginAndPortalRevealsAlikeAndRevertsAtEnd` pins +kind parity through the real coordinator + controller) and were PROVEN +live: a @telepoi-yaraq portal hold in the same session recorded +`kind=portal` gate-ready **3589 ms** / totalMs **8681 ms** at 64–66 fps. +**Measured (two login A/B runs + one login-then-portal run, this +binary): loaded 625/625 at 3062/3064/3023 ms (was ~6–7 s), tunnel frames +64–66 fps through the widened burst (the 28–29 fps first-second window is +process/session start, present at baseline) — yet gate-ready +8594/8752/8368 ms and totalMs 12634/12727/12726 (baseline 12689/12734): +UNCHANGED.** Attribution: after the last publication (~3.0 s) the +streaming meter is fully idle (`[stream-tick] ops=0 yields=0`) for +~4.7 s until render-ready (7941/8099/7715 ms) — the pacer is the +render-thread upload/registration barrier behind +`GpuWorldState.IsRenderReady` (terrain upload crossing the render-thread +barrier + spawn-adapter registration, +`StreamingController.IsRenderNeighborhoodResident`), which ran +CONCURRENTLY under the old drip — that is why the old breakdown showed it +as only a ~0.4–1.2 s TAIL (the probe doc's serial-edge caveat) — and +takes ~7.7–8.1 s from process start regardless of the drip. The +warmed-process portal run replaces the SAME 625-block window to +render-ready in ~3.0 s, so the login cost is dominated by cold-start +work (first-touch DAT decode / texture and mesh cache fill / driver + +JIT warmup), not by window size or queue capacity. Login acceptance +(gate-ready < 3500 / total < 10000) NOT met; the widening stays — it +removes the drip as a pacer everywhere, collapses the portal-hold +gate-ready to ~3.6 s, and is what exposed the real limiter. No register +row: the streamed result and the reveal gate are byte-identical; only the +scheduling rate during a hidden hold changed (same precedent as the +previous round). **Ladder: 27.4 → 12.7 → login 12.6–12.7 (unchanged, now +attributed to the cold render-thread barrier); portal-hold gate-ready +~3.6 s / total ~8.7 s. Next lever: instrument and cut the login-cold +render-thread upload/registration phase (t≈1–8 s, concurrent) — budgets +are exonerated three times over.** + ## #417 — World ambience keeps playing (and re-firing) on the character-select screen after the in-world logoff **Status:** ✅ FIXED 2026-08-17 (logout-audio round; fix + tests in the same diff --git a/src/AcDream.App/Streaming/StreamingController.cs b/src/AcDream.App/Streaming/StreamingController.cs index 998410db..9cd398cf 100644 --- a/src/AcDream.App/Streaming/StreamingController.cs +++ b/src/AcDream.App/Streaming/StreamingController.cs @@ -665,9 +665,21 @@ public sealed class StreamingController throw new InvalidOperationException( "StreamingController.Tick cannot be reentered."); + // #418: while the destination reservation hides the world behind the + // authored tunnel, the frame runs on the hold-widened profile so the + // reveal-critical drip is not paced by the authored mid-game budget. + // Keyed off the existing reservation bracket only, derived from the + // CURRENT budget every frame (so a mid-hold quality swap composes), + // and reverting the moment EndDestinationReservation clears the + // reservation. A frame with no reservation uses _workBudget verbatim. + bool destinationHold = _destinationReservation is not null; var meter = new StreamingWorkMeter( - _workBudget, - destinationReservationActive: _destinationReservation is not null); + destinationHold + ? _workBudget.WidenForDestinationHold( + _configuredWorkBudgetOptions + .HoldDestinationCeilingMilliseconds) + : _workBudget, + destinationReservationActive: destinationHold); _activeWorkMeter = meter; try { diff --git a/src/AcDream.App/Streaming/StreamingWorkBudget.cs b/src/AcDream.App/Streaming/StreamingWorkBudget.cs index 1d04f352..4918b835 100644 --- a/src/AcDream.App/Streaming/StreamingWorkBudget.cs +++ b/src/AcDream.App/Streaming/StreamingWorkBudget.cs @@ -70,6 +70,56 @@ public readonly record struct StreamingWorkBudget nameof(StreamingWorkBudget)); } } + + /// + /// Hold-scoped destination-lane widening (#418). While a destination + /// reservation is active the world behind the reveal gate is hidden by + /// the authored tunnel, so destination publication may spend more + /// update-thread time per frame without any visible pop-in. This scales + /// the WHOLE profile by over the + /// current time ceiling — every count/byte dimension by the same factor, + /// so elapsed time remains the authoritative guard exactly as the + /// authored profile intends (measured: the hold's meter yields are + /// Time-limit yields; the count/byte dimensions never bound first) — + /// and re-derives the reserve fraction so the NON-destination lane's + /// absolute per-frame caps are unchanged: with the shipped profile the + /// widened caps are floor-exact (e.g. completions + /// floor(256 x 0.0625) = floor(64 x 0.25) = 16); a fractional + /// measurement override can drift a lane floor by one unit. + /// A ceiling at or below the current time ceiling returns this profile + /// unchanged — widening never shrinks, and a no-hold frame never calls + /// this at all. + /// + public StreamingWorkBudget WidenForDestinationHold( + double ceilingMilliseconds) + { + if (!double.IsFinite(ceilingMilliseconds) || ceilingMilliseconds <= 0) + return this; + + double scale = + ceilingMilliseconds / MaxUpdateTime.TotalMilliseconds; + if (scale <= 1.0) + return this; + + // The unreserved (non-destination) share shrinks by the same factor + // the totals grow, keeping the non-destination lane's absolute caps + // at the authored values. Extreme measurement overrides could round + // the float to 1.0, which the ctor rejects; the largest float below + // one keeps the lane at its Max(1, ...) floor instead. + float widenedReserve = (float)( + 1.0 - (1.0 - DestinationReserveFraction) / scale); + if (widenedReserve >= 1f) + widenedReserve = MathF.BitDecrement(1f); + + return new StreamingWorkBudget( + TimeSpan.FromMilliseconds(ceilingMilliseconds), + StreamingWorkBudgetOptions.Scale(MaxCompletionAdmissions, scale), + StreamingWorkBudgetOptions.Scale(MaxAdoptedCpuBytes, scale), + StreamingWorkBudgetOptions.Scale(MaxEntityOperations, scale), + StreamingWorkBudgetOptions.Scale(MaxGpuUploadBytes, scale), + StreamingWorkBudgetOptions.Scale(MaxGlRetireOperations, scale), + widenedReserve); + } } /// diff --git a/src/AcDream.App/Streaming/StreamingWorkBudgetOptions.cs b/src/AcDream.App/Streaming/StreamingWorkBudgetOptions.cs index edc4800c..7d3f07c3 100644 --- a/src/AcDream.App/Streaming/StreamingWorkBudgetOptions.cs +++ b/src/AcDream.App/Streaming/StreamingWorkBudgetOptions.cs @@ -6,6 +6,16 @@ namespace AcDream.App.Streaming; /// Startup-time scheduling ceilings for update-thread streaming work. /// These values limit work per frame; they never reduce the amount or /// quality of content that eventually becomes resident. +/// (#418) is the +/// hold-scoped destination-lane time ceiling: while a destination +/// reservation hides the world behind the authored tunnel, the frame meter +/// runs on of the +/// current profile instead of the profile itself. It is an absolute ceiling +/// (not scaled by the legacy quality selector), it never shrinks a profile +/// whose own ceiling is already at least as large, and it is NOT a user +/// setting — the environment override +/// (ACDREAM_STREAM_WORK_HOLD_DEST_MS) exists for A/B measurement +/// only, matching the rest of the ACDREAM_STREAM_WORK_* family. /// public sealed record StreamingWorkBudgetOptions( double MaxUpdateMilliseconds, @@ -14,7 +24,8 @@ public sealed record StreamingWorkBudgetOptions( int MaxEntityOperations, long MaxGpuUploadBytes, int MaxGlRetireOperations, - float DestinationReserveFraction) + float DestinationReserveFraction, + double HoldDestinationCeilingMilliseconds = 8.0) { public const long MiB = 1024L * 1024L; @@ -30,7 +41,8 @@ public sealed record StreamingWorkBudgetOptions( MaxEntityOperations: 4_096, MaxGpuUploadBytes: 8 * MiB, MaxGlRetireOperations: 64, - DestinationReserveFraction: 0.75f); + DestinationReserveFraction: 0.75f, + HoldDestinationCeilingMilliseconds: 8.0); internal static StreamingWorkBudgetOptions Parse( Func env) @@ -58,7 +70,10 @@ public sealed record StreamingWorkBudgetOptions( defaults.MaxGlRetireOperations), DestinationReserveFraction: ParseReservePercent( env("ACDREAM_STREAM_WORK_DEST_RESERVE_PERCENT"), - defaults.DestinationReserveFraction)); + defaults.DestinationReserveFraction), + HoldDestinationCeilingMilliseconds: ParsePositiveDouble( + env("ACDREAM_STREAM_WORK_HOLD_DEST_MS"), + defaults.HoldDestinationCeilingMilliseconds)); } public StreamingWorkBudget ToBudget() => new( @@ -75,6 +90,10 @@ public sealed record StreamingWorkBudgetOptions( /// landblock count now selects a complete work profile instead of acting /// as a second hidden execution throttle. Four is the historical High /// profile and therefore preserves this configured profile exactly. + /// deliberately rides + /// through unscaled: it is an absolute hold-scoped ceiling, so a smaller + /// quality profile widens further toward the same ceiling during a hold + /// and a profile already at or above it widens not at all. /// public StreamingWorkBudgetOptions ScaleForLegacyCompletionCount(int count) { @@ -149,7 +168,7 @@ public sealed record StreamingWorkBudgetOptions( return percent / 100f; } - private static int Scale(int value, double scale) + internal static int Scale(int value, double scale) { if (scale >= int.MaxValue / (double)value) return int.MaxValue; @@ -158,7 +177,7 @@ public sealed record StreamingWorkBudgetOptions( (int)Math.Round(value * scale, MidpointRounding.AwayFromZero)); } - private static long Scale(long value, double scale) + internal static long Scale(long value, double scale) { if (scale >= long.MaxValue / (double)value) return long.MaxValue; diff --git a/tests/AcDream.App.Tests/Streaming/StreamingWorkBudgetTests.cs b/tests/AcDream.App.Tests/Streaming/StreamingWorkBudgetTests.cs index 6ef578a4..71d15663 100644 --- a/tests/AcDream.App.Tests/Streaming/StreamingWorkBudgetTests.cs +++ b/tests/AcDream.App.Tests/Streaming/StreamingWorkBudgetTests.cs @@ -3,8 +3,11 @@ using System.Numerics; using AcDream.App.Rendering; using AcDream.App.Rendering.Wb; using AcDream.App.Streaming; +using AcDream.Core.Physics; using AcDream.Core.Terrain; using AcDream.Core.World; +using AcDream.Runtime; +using AcDream.Runtime.World; using DatReaderWriter.DBObjs; namespace AcDream.App.Tests.Streaming; @@ -232,6 +235,107 @@ public sealed class StreamingWorkBudgetTests Assert.Equal(2, meter.Snapshot.CompletedOperations); } + [Fact] + public void WidenForDestinationHold_ScalesEveryDimensionAndReservesTheAuthoredNonDestinationShare() + { + StreamingWorkBudget widened = StreamingWorkBudgetOptions.Default + .ToBudget() + .WidenForDestinationHold(8.0); + + Assert.Equal(TimeSpan.FromMilliseconds(8), widened.MaxUpdateTime); + Assert.Equal(256, widened.MaxCompletionAdmissions); + Assert.Equal( + 32 * StreamingWorkBudgetOptions.MiB, + widened.MaxAdoptedCpuBytes); + Assert.Equal(16_384, widened.MaxEntityOperations); + Assert.Equal( + 32 * StreamingWorkBudgetOptions.MiB, + widened.MaxGpuUploadBytes); + Assert.Equal(256, widened.MaxGlRetireOperations); + // 1 - (1 - 0.75)/4: the non-destination share of the widened totals + // is the authored absolute share (floor(256 x 0.0625) = + // floor(64 x 0.25) = 16 completions, and so on per dimension). + Assert.Equal(0.9375f, widened.DestinationReserveFraction); + } + + [Fact] + public void WidenForDestinationHold_IsANoOpAtOrBelowTheCurrentCeiling() + { + StreamingWorkBudget authored = + StreamingWorkBudgetOptions.Default.ToBudget(); + + Assert.Equal(authored, authored.WidenForDestinationHold(2.0)); + Assert.Equal(authored, authored.WidenForDestinationHold(1.0)); + Assert.Equal(authored, authored.WidenForDestinationHold(0.0)); + Assert.Equal(authored, authored.WidenForDestinationHold(double.NaN)); + } + + [Fact] + public void WidenForDestinationHold_DoesNotWidenTheNonDestinationLane() + { + StreamingWorkBudget authored = + StreamingWorkBudgetOptions.Default.ToBudget(); + foreach (StreamingWorkBudget budget in new[] + { + authored, + authored.WidenForDestinationHold(8.0), + }) + { + var meter = new StreamingWorkMeter( + budget, + static () => 0, + timestampFrequency: 1_000, + destinationReservationActive: true); + + // The non-destination lane clips at the SAME absolute cap + // (16 completion admissions) whether or not the profile is + // hold-widened: widening must never hand mid-game background + // work extra capacity. + Assert.Equal( + StreamingWorkAdmission.Admitted, + meter.TryReserve( + new StreamingWorkCost(CompletionAdmissions: 16), + "ordinary")); + meter.Complete(); + Assert.Equal( + StreamingWorkAdmission.Yielded, + meter.TryReserve( + new StreamingWorkCost(CompletionAdmissions: 1), + "ordinary-over-share")); + Assert.Equal( + StreamingWorkLimit.CompletionAdmissions, + meter.Snapshot.LastLimit); + } + + // The destination lane of the widened profile owns the widened + // remainder: 240 more admissions on top of the ordinary 16. + var widenedMeter = new StreamingWorkMeter( + authored.WidenForDestinationHold(8.0), + static () => 0, + timestampFrequency: 1_000, + destinationReservationActive: true); + Assert.Equal( + StreamingWorkAdmission.Admitted, + widenedMeter.TryReserve( + new StreamingWorkCost(CompletionAdmissions: 16), + "ordinary")); + widenedMeter.Complete(); + using (widenedMeter.EnterLane(StreamingWorkLane.Destination)) + { + Assert.Equal( + StreamingWorkAdmission.Admitted, + widenedMeter.TryReserve( + new StreamingWorkCost(CompletionAdmissions: 240), + "destination")); + widenedMeter.Complete(); + Assert.Equal( + StreamingWorkAdmission.Yielded, + widenedMeter.TryReserve( + new StreamingWorkCost(CompletionAdmissions: 1), + "destination-over-widened-total")); + } + } + [Fact] public void MeterUsesInjectedMonotonicClockAndRecordsOverrunAndFailure() { @@ -598,6 +702,180 @@ public sealed class StreamingWorkBudgetTests Assert.Equal(0, source.BacklogCount); } + [Fact] + public void ActiveHold_WidensTheDestinationLaneAndRevertsWhenTheReservationEnds() + { + uint center = StreamingRegion.EncodeLandblockIdForTest(32, 32); + var source = new QueueCompletionSource( + Loaded(center), + Loaded(StreamingRegion.EncodeLandblockIdForTest(32, 33)), + Loaded(StreamingRegion.EncodeLandblockIdForTest(33, 32)), + Loaded(StreamingRegion.EncodeLandblockIdForTest(33, 33)), + Loaded(StreamingRegion.EncodeLandblockIdForTest(32, 31))); + int applied = 0; + // Authored 100 ms / hold ceiling 200 ms: a deterministic 2x widening + // that the wall clock cannot reach, so only the admission count can + // clip. 2 authored admissions -> 4 during the hold. + StreamingController controller = Controller( + source, + () => applied++, + WorkOptions(admissions: 2, cpuBytes: 1_000) with + { + HoldDestinationCeilingMilliseconds = 200, + }); + controller.BeginDestinationReservation(1, center, 1); + + controller.Tick(32, 32); + + Assert.Equal(4, applied); + Assert.Equal( + 4, + controller.WorkDiagnostics.LastFrame.Used.CompletionAdmissions); + Assert.Equal(1, source.BacklogCount); + + // The moment the reservation ends the authored profile is back: + // the same backlog now admits 2 per frame, not 4. + controller.EndDestinationReservation(1); + source.Enqueue( + Loaded(StreamingRegion.EncodeLandblockIdForTest(33, 31))); + source.Enqueue( + Loaded(StreamingRegion.EncodeLandblockIdForTest(31, 32))); + + controller.Tick(32, 32); + + Assert.Equal(6, applied); + Assert.Equal( + 2, + controller.WorkDiagnostics.LastFrame.Used.CompletionAdmissions); + Assert.Equal(1, source.BacklogCount); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void HoldWidening_AppliesToLoginAndPortalRevealsAlikeAndRevertsAtEnd( + bool portalKind) + { + // #418 kind-parity proof: BeginLogin and TryBeginPortal converge on + // the SAME BeginHostLifetime -> BeginDestinationReservation bracket + // (WorldRevealCoordinator), and the widening keys off that bracket + // alone — so a portal hold widens exactly like a login hold, and + // both revert the moment the reveal releases the reservation. The + // real StreamingController is the coordinator's scheduler here; the + // observable is the per-frame admission cap (2 authored, 4 widened + // by the 100 ms -> 200 ms hold ceiling). + uint center = StreamingRegion.EncodeLandblockIdForTest(32, 32); + const uint destinationCell = 0x20200021u; // outdoor cell of (32,32) + var source = new QueueCompletionSource( + Loaded(center), + Loaded(StreamingRegion.EncodeLandblockIdForTest(32, 33)), + Loaded(StreamingRegion.EncodeLandblockIdForTest(33, 32)), + Loaded(StreamingRegion.EncodeLandblockIdForTest(33, 33)), + Loaded(StreamingRegion.EncodeLandblockIdForTest(32, 31)), + Loaded(StreamingRegion.EncodeLandblockIdForTest(33, 31)), + Loaded(StreamingRegion.EncodeLandblockIdForTest(31, 32))); + int applied = 0; + StreamingController controller = Controller( + source, + () => applied++, + WorkOptions(admissions: 2, cpuBytes: 1_000) with + { + HoldDestinationCeilingMilliseconds = 200, + }); + var transit = new RuntimeWorldTransitState(null); + var coordinator = new WorldRevealCoordinator( + transit, + revealWindow: static () => new StreamingRevealWindow(1, 1), + isRenderNeighborhoodReady: static (_, _, _) => false, + isSpawnCellReady: static _ => false, + isTerrainNeighborhoodReady: static (_, _) => false, + areCompositeTexturesReady: static () => false, + prepareCompositeTextures: static (_, _) => { }, + invalidateCompositeTextures: static () => { }, + isSpawnClaimUnhydratable: static _ => false, + streaming: controller); + + if (portalKind) + { + Assert.True(transit.TryQueueTeleportStart(1)); + Assert.True(transit.ActivateQueuedTeleport()); + Assert.True(transit.OfferTeleportDestination( + new RuntimeTeleportDestination( + EntityGuid: 0x50000001u, + InstanceSequence: 1, + PositionSequence: 1, + TeleportSequence: 1, + ForcePositionSequence: 1, + Position: new Position( + destinationCell, + Vector3.Zero, + Quaternion.Identity)), + teleportTimestampAdvanced: true)); + Assert.True(coordinator.TryBeginPortal( + 1, + destinationCell, + out _)); + Assert.Equal( + RuntimePortalKind.Portal, + coordinator.Snapshot.Kind); + } + else + { + coordinator.BeginLogin(destinationCell); + Assert.Equal( + RuntimePortalKind.Login, + coordinator.Snapshot.Kind); + } + + controller.Tick(32, 32); + + Assert.Equal(4, applied); + Assert.Equal( + 4, + controller.WorkDiagnostics.LastFrame.Used.CompletionAdmissions); + + // Releasing the reveal releases the reservation; the very next frame + // runs the authored profile again. + coordinator.Cancel(); + controller.Tick(32, 32); + + Assert.Equal(6, applied); + Assert.Equal( + 2, + controller.WorkDiagnostics.LastFrame.Used.CompletionAdmissions); + } + + [Fact] + public void NoReservation_UsesTheAuthoredBudgetUnchanged() + { + // Constraint pin (#418): with NO destination reservation the frame + // meter runs on the authored budget verbatim — the configured hold + // ceiling must be invisible to mid-game streaming. + uint center = StreamingRegion.EncodeLandblockIdForTest(32, 32); + var source = new QueueCompletionSource( + Loaded(center), + Loaded(StreamingRegion.EncodeLandblockIdForTest(32, 33)), + Loaded(StreamingRegion.EncodeLandblockIdForTest(33, 32)), + Loaded(StreamingRegion.EncodeLandblockIdForTest(33, 33)), + Loaded(StreamingRegion.EncodeLandblockIdForTest(32, 31))); + int applied = 0; + StreamingController controller = Controller( + source, + () => applied++, + WorkOptions(admissions: 2, cpuBytes: 1_000) with + { + HoldDestinationCeilingMilliseconds = 200, + }); + + controller.Tick(32, 32); + + Assert.Equal(2, applied); + Assert.Equal( + 2, + controller.WorkDiagnostics.LastFrame.Used.CompletionAdmissions); + Assert.Equal(3, source.BacklogCount); + } + [Fact] public void IncompleteReveal_DoesNotStarveOrdinaryWorkBeforeDestinationArrives() {