Merge campaign-hover-ui-round: #418 hold-scoped streaming burst — portal/recall holds at retail-feel
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / portable-launcher (ubuntu-latest) (push) Waiting to run
Headless portability / portable-launcher (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
Some checks are pending
Headless portability / portable-headless (ubuntu-latest) (push) Waiting to run
Headless portability / portable-headless (windows-latest) (push) Waiting to run
Headless portability / portable-launcher (ubuntu-latest) (push) Waiting to run
Headless portability / portable-launcher (windows-latest) (push) Waiting to run
Headless portability / linux-graphical (push) Waiting to run
Headless portability / linux-vulkan (push) Waiting to run
While a destination reservation hides the world behind the tunnel, the frame meter runs on a hold-widened profile (2 ms -> 8 ms destination lane, non-destination lane absolute caps unchanged), reverting the frame the reservation ends. Measured: warm-process portal hold gate-ready 3.6 s / total 8.7 s (retail-feel band); login drip 6-7 s -> 3.0 s with the remaining login floor now attributed to process cold-start warmup (~8 s concurrent phase), the next #418 lever. Tunnel held 64-66 fps through the burst; mid-game no-hold path bit-identical (test-pinned). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
commit
d504a12db5
5 changed files with 420 additions and 7 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -70,6 +70,56 @@ public readonly record struct StreamingWorkBudget
|
|||
nameof(StreamingWorkBudget));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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 <paramref name="ceilingMilliseconds"/> 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
|
||||
/// <c>floor(256 x 0.0625) = floor(64 x 0.25) = 16</c>); 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
/// <para><paramref name="HoldDestinationCeilingMilliseconds"/> (#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 <see cref="StreamingWorkBudget.WidenForDestinationHold"/> 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
|
||||
/// (<c>ACDREAM_STREAM_WORK_HOLD_DEST_MS</c>) exists for A/B measurement
|
||||
/// only, matching the rest of the <c>ACDREAM_STREAM_WORK_*</c> family.</para>
|
||||
/// </summary>
|
||||
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<string, string?> 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.
|
||||
/// <see cref="HoldDestinationCeilingMilliseconds"/> 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.
|
||||
/// </summary>
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue