The user watched far terrain visibly assemble after portal space exits.
The reveal gate was NOT missing a hold — Slice E's hold mechanism is
correct and already in place. The hold was measuring the wrong domain:
it opened at a hardcoded 3x3 landblock neighbourhood (~192 m) while the
visible world extends to the fog end (~2,189 m at the shipped High
preset, inside a 2,304 m Far window). An 11.4:1 ratio.
Retail's equivalent ratio is 1:1 BY CONSTRUCTION. `LScape` owns one
`mid_width x mid_width` array of `CLandBlock*` (`LScape::SetMidRadius`
@0x00504C00, `LScape::update_block` @0x005063A0), `mid_radius` is
assigned directly from the user's `Render.LandscapeDrawDistance`
preference (`SmartBox::SetRegion` @0x004531F0; values
`Render_LandscapeDrawDistance_Values` @0x007CA988 = {3,5,8,11,15,25},
default 8 — both byte-verified against the PDB-paired 2013 binary), and
that same square is simultaneously the prefetched set
(`LScape::PreFetchCells` @0x00505660), the drawn set (`block_draw_list`
over the same array), and the set the simulation blocks on
(`CellManager::blocking_for_cells`). There is no retail configuration in
which the client streams farther than it gates, because there is only
one number.
So the fix derives rather than duplicates. Four coupled parts, which is
why this is one commit and not four — D1 without D2 hangs the client and
D2 without D1 is dead code:
D1 `WorldRevealReadinessBarrier` takes a live `Func<StreamingRevealWindow>`
and stops being static: outdoor requires `FarRadius`, indoor still 0
(retail's `CEnvCell::PreFetchCells` @0x0052D1E0 arm). Read per
evaluation, never captured — the radii are runtime mutable through
Settings, and retail's answer to a mid-hold radius change is to reset,
re-radius, and re-arm the blocking prefetch at the NEW value
(`SmartBox::set_mid_radius` @0x00453180). `OutdoorNeighborhoodRadius`
is deleted; there is no constant left to drift.
D2 `StreamingController.IsRenderNeighborhoodResident` becomes tiered,
because acdream's loaded landscape is: inside `NearRadius`,
`IsNearTier && IsRenderReady`; out to `FarRadius`, `IsRenderReady` only.
Without this the fix cannot work at all — nothing outside the Near ring
is ever promoted, so any radius above `NearRadius` was unsatisfiable and
would have held the reveal forever. Proof obligation P1 (a Far-tier
landblock genuinely satisfies `IsRenderReady`) is now a test driven
through the real `PublicationKind.Far` pipeline against a real
`LandblockSpawnAdapter`, not an inference.
D7 `RuntimeWorldTransitState.AcknowledgeDestinationReadiness` re-derived
`indoor ? 0 : 1` and failed `invalid-readiness-shape` on any other
value, so changing the radius alone would have looked like "the fix
hangs the client". It is now a SHAPE invariant (`indoor => 0`,
`outdoor => >= 1`). Runtime does not own the graphical host's streaming
configuration and must not learn it; plumbing App radii into Runtime to
preserve the strict equality is exactly the assert-a-mechanism-that-does-
not-exist failure C5b was built to stop. Both non-graphical producers
keep emitting their centre-ring token and stay legal, annotated in place.
D6 `PhysicsEngine.IsNeighborhoodTerrainResident` rebuilt a full-map
`HashSet` on every call, every frame of every hold. At radius 1 that was
invisible; at radius 12 (625 ring members) it violates Slice I1's
0 B/resolve standard. Now an engine-owned scratch set, cleared in place;
measured at 0 bytes over 1,000 warmed radius-12 queries.
Also: the destination reservation opens at exactly the gate's radius and
reopens on the same generation when the radius changes mid-hold (retail
has one square for both, and no concept of prioritising an inner ring
differently). Composite warmup deliberately stays `NearRadius`-scoped —
the composite domain is entity-scoped and Far builds carry no entities,
so widening it would walk the outer window to warm nothing.
`ACDREAM_PROBE_REVEAL_RADIUS` is a measurement probe in a diagnostic
owner (CLAUDE.md rule 5) so the connected route can be run A/B on one
binary; it is NOT a user-facing prefetch knob, since a low setting would
reintroduce the decoupling this slice exists to close.
Register: AD-2 amended with the derived window, the two-tier split, and
the four new retail anchors. AP-149 FILED for the residual this does not
close — the outer ring accepts terrain-only publication where retail
requires LandBlockInfo and every building EnvCell, so a distant building
can still pop in at Far-ring distances. Do not let a later closeout
claim parity.
Docs: `ACDREAM_STREAM_RADIUS`'s CLAUDE.md description was wrong on every
clause (the default is unset, not 2; it forces `NearRadius`; it is
silently discarded by any Settings save) — corrected, since that is the
file every session reads. `reference_two_tier_streaming.md` corrected in
four ways, including "Far tier = terrain only": Far also publishes
terrain COLLISION, which is precisely what makes this fix viable.
#280's issue text had the right conclusion from a wrong premise (it
names a view-distance setting acdream does not have) — corrected, and
the missing Viewing Distance option filed separately as #326, with #327
(DDD progress readout) and #328 (hardcoded 5000 f far plane vs retail's
byte-verified 4000) filed alongside.
Expect LONGER holds and the "In Portal Space - Please Wait..." cue on
recalls MORE often. That is convergence toward retail, not away from it:
retail emits the byte-identical string for the whole duration of a
blocked prefetch and polls at 5 s intervals. The failure condition is
non-convergence, not duration.
Gates: Release build 0 errors. Complete suite 11,178 passed / 4 skipped
/ 0 failed, against a re-measured 11,142 / 4 / 0 baseline at 9ee9c1a1 —
+36, reconciled exactly as 36 new tests (App +23, Runtime +10, Core +3),
zero deleted, zero newly skipped. Nine discriminating tests
sabotage-verified in both directions. The connected/visual gate is
batched into C5's matrix; its recipe, its three positive artifacts, and
its required recall leg are written into the campaign plan.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
861 lines
30 KiB
C#
861 lines
30 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Streaming;
|
|
using AcDream.App.World;
|
|
using AcDream.Core.Physics;
|
|
using AcDream.Core.Selection;
|
|
using AcDream.Runtime;
|
|
using AcDream.Runtime.World;
|
|
|
|
namespace AcDream.App.Tests.Streaming;
|
|
|
|
public sealed class WorldRevealCoordinatorTests
|
|
{
|
|
private sealed class State
|
|
{
|
|
public RuntimeWorldTransitState Transit { get; private set; } = null!;
|
|
public bool RenderReady { get; set; }
|
|
public bool CollisionReady { get; set; }
|
|
public bool CompositesReady { get; set; }
|
|
public int InvalidateCount { get; private set; }
|
|
public int PrepareCount { get; private set; }
|
|
|
|
/// <summary>
|
|
/// The live streaming window (#280). Mutable so a test can change the
|
|
/// quality preset mid-hold, which retail answers by re-arming the
|
|
/// blocking prefetch at the NEW radius
|
|
/// (<c>SmartBox::set_mid_radius</c> @0x00453180).
|
|
/// </summary>
|
|
public StreamingRevealWindow Window { get; set; } =
|
|
new(NearRadius: 1, FarRadius: 1);
|
|
|
|
public WorldRevealCoordinator Build(
|
|
List<string>? logs = null,
|
|
IWorldRevealStreamingScheduler? streaming = null,
|
|
IWorldRevealRenderResourceScheduler? renderResources = null,
|
|
RuntimeWorldTransitState? transit = null)
|
|
{
|
|
transit ??= new RuntimeWorldTransitState(
|
|
logs is null ? null : new Action<string>(logs.Add));
|
|
Transit = transit;
|
|
return new WorldRevealCoordinator(
|
|
transit,
|
|
revealWindow: () => Window,
|
|
isRenderNeighborhoodReady: (_, _, _) => RenderReady,
|
|
isSpawnCellReady: _ => CollisionReady,
|
|
isTerrainNeighborhoodReady: (_, _) => CollisionReady,
|
|
areCompositeTexturesReady: () => CompositesReady,
|
|
prepareCompositeTextures: (_, _) => PrepareCount++,
|
|
invalidateCompositeTextures: () =>
|
|
{
|
|
InvalidateCount++;
|
|
CompositesReady = false;
|
|
},
|
|
isSpawnClaimUnhydratable: _ => false,
|
|
streaming: streaming,
|
|
renderResources: renderResources);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void Begin_InvalidatesPriorDestinationAndStartsOneGeneration()
|
|
{
|
|
var state = new State { CompositesReady = true };
|
|
WorldRevealCoordinator coordinator = state.Build();
|
|
|
|
long generation = coordinator.BeginLogin(0x11340021u);
|
|
|
|
Assert.Equal(1, generation);
|
|
Assert.Equal(1, state.InvalidateCount);
|
|
Assert.False(state.CompositesReady);
|
|
Assert.Equal(RuntimePortalKind.Login, coordinator.Snapshot.Kind);
|
|
}
|
|
|
|
[Fact]
|
|
public void PrepareAndEvaluate_UsesOneCanonicalSnapshotForLifecycle()
|
|
{
|
|
const uint outdoorCell = 0x11340021u;
|
|
var state = new State
|
|
{
|
|
RenderReady = true,
|
|
CollisionReady = true,
|
|
};
|
|
WorldRevealCoordinator coordinator = state.Build();
|
|
coordinator.BeginLogin(outdoorCell);
|
|
|
|
WorldRevealReadinessSnapshot first = coordinator.PrepareAndEvaluate(outdoorCell);
|
|
state.CompositesReady = true;
|
|
WorldRevealReadinessSnapshot ready = coordinator.PrepareAndEvaluate(outdoorCell);
|
|
|
|
Assert.False(first.IsReady);
|
|
Assert.True(ready.IsReady);
|
|
Assert.Equal(2, state.PrepareCount);
|
|
Assert.Equal(ready.DestinationCell, coordinator.Snapshot.DestinationCell);
|
|
Assert.Equal(ready.IsReady, coordinator.Snapshot.IsReady);
|
|
}
|
|
|
|
[Fact]
|
|
public void PortalLifecycle_CountsMaterializationAndCompletesVisibleGeneration()
|
|
{
|
|
const uint outdoorCell = 0x3032001Cu;
|
|
var logs = new List<string>();
|
|
var state = new State
|
|
{
|
|
RenderReady = true,
|
|
CollisionReady = true,
|
|
CompositesReady = true,
|
|
};
|
|
WorldRevealCoordinator coordinator = state.Build(logs);
|
|
long generation = BeginPortal(
|
|
coordinator,
|
|
state.Transit,
|
|
outdoorCell,
|
|
sequence: 1);
|
|
state.CompositesReady = true;
|
|
|
|
Assert.True(coordinator.Evaluate(outdoorCell).IsReady);
|
|
coordinator.ObserveMaterialized(generation, 1, outdoorCell);
|
|
coordinator.ObserveMaterialized(generation, 1, outdoorCell);
|
|
coordinator.ObserveWorldViewportVisible();
|
|
coordinator.Complete();
|
|
|
|
Assert.Equal(1, coordinator.PortalMaterializationCount);
|
|
Assert.True(coordinator.Snapshot.Materialized);
|
|
Assert.True(coordinator.Snapshot.WorldViewportObserved);
|
|
Assert.True(coordinator.Snapshot.Completed);
|
|
Assert.Equal(0, coordinator.Snapshot.InvariantFailureCount);
|
|
Assert.Single(logs, line => line.Contains("event=materialized", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public void PortalBegin_WrongSequenceOrCellCannotAcquireHostResources()
|
|
{
|
|
const uint destinationCell = 0x3032001Cu;
|
|
var state = new State { CompositesReady = true };
|
|
var scheduler = new RecordingDestinationScheduler();
|
|
WorldRevealCoordinator coordinator = state.Build(streaming: scheduler);
|
|
RuntimeWorldTransitTestDriver.AcceptPortalDestination(
|
|
state.Transit,
|
|
destinationCell,
|
|
sequence: 7);
|
|
|
|
Assert.False(coordinator.TryBeginPortal(
|
|
8,
|
|
destinationCell,
|
|
out _));
|
|
Assert.False(coordinator.TryBeginPortal(
|
|
7,
|
|
0x11340021u,
|
|
out _));
|
|
Assert.Empty(scheduler.Begins);
|
|
Assert.Equal(0, state.InvalidateCount);
|
|
Assert.Equal(0, coordinator.Snapshot.Generation);
|
|
|
|
Assert.True(coordinator.TryBeginPortal(
|
|
7,
|
|
destinationCell,
|
|
out long generation));
|
|
Assert.Equal(
|
|
(generation, destinationCell, 1),
|
|
Assert.Single(scheduler.Begins));
|
|
Assert.Equal(1, state.InvalidateCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void Cancel_PreventsLateDestinationWorkFromMutatingLifecycle()
|
|
{
|
|
var state = new State
|
|
{
|
|
RenderReady = true,
|
|
CollisionReady = true,
|
|
CompositesReady = true,
|
|
};
|
|
WorldRevealCoordinator coordinator = state.Build();
|
|
long generation = BeginPortal(
|
|
coordinator,
|
|
state.Transit,
|
|
0x11340021u,
|
|
sequence: 1);
|
|
coordinator.Cancel();
|
|
|
|
coordinator.PrepareAndEvaluate(0x11340021u);
|
|
coordinator.ObserveMaterialized(
|
|
generation,
|
|
1,
|
|
0x11340021u);
|
|
coordinator.ObserveWorldViewportVisible();
|
|
coordinator.Complete();
|
|
|
|
Assert.True(coordinator.Snapshot.Cancelled);
|
|
Assert.Equal(0x11340021u, coordinator.Snapshot.DestinationCell);
|
|
Assert.False(coordinator.Snapshot.IsReady);
|
|
Assert.Equal(0, coordinator.PortalMaterializationCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void RevealLifetime_QuiescesContinuouslyAndOnlyActiveGenerationReopens()
|
|
{
|
|
var transit = new RuntimeWorldTransitState();
|
|
var availability = new WorldGenerationAvailabilityState(transit);
|
|
var world = new GpuWorldState(availability: availability);
|
|
var audio = new RecordingAudioQuiescence();
|
|
var quiescence = new WorldGenerationQuiescence(
|
|
new SelectionState(),
|
|
world,
|
|
_ => null,
|
|
audio);
|
|
var coordinator = new WorldRevealCoordinator(
|
|
transit,
|
|
revealWindow: () => new StreamingRevealWindow(1, 1),
|
|
isRenderNeighborhoodReady: (_, _, _) => true,
|
|
isSpawnCellReady: _ => true,
|
|
isTerrainNeighborhoodReady: (_, _) => true,
|
|
areCompositeTexturesReady: () => true,
|
|
prepareCompositeTextures: (_, _) => { },
|
|
invalidateCompositeTextures: () => { },
|
|
isSpawnClaimUnhydratable: _ => false,
|
|
quiescence: quiescence);
|
|
|
|
Assert.Equal(1, coordinator.BeginLogin(0x11340021u));
|
|
long generation = BeginPortal(
|
|
coordinator,
|
|
transit,
|
|
0x11340021u,
|
|
sequence: 1);
|
|
Assert.Equal(2, generation);
|
|
Assert.False(availability.IsWorldAvailable);
|
|
Assert.Equal(2, availability.QuiescedGeneration);
|
|
Assert.Equal(1, audio.SuspendCalls);
|
|
|
|
Assert.True(coordinator.Evaluate(0x11340021u).IsReady);
|
|
coordinator.ObserveMaterialized(
|
|
generation,
|
|
1,
|
|
0x11340021u);
|
|
coordinator.Complete();
|
|
|
|
Assert.True(availability.IsWorldAvailable);
|
|
Assert.Equal(1, audio.ResumeCalls);
|
|
}
|
|
|
|
[Fact]
|
|
public void Materialization_ReopensWorldBeforePortalViewportRelease()
|
|
{
|
|
var transit = new RuntimeWorldTransitState();
|
|
var availability = new WorldGenerationAvailabilityState(transit);
|
|
var world = new GpuWorldState(availability: availability);
|
|
var audio = new RecordingAudioQuiescence();
|
|
var quiescence = new WorldGenerationQuiescence(
|
|
new SelectionState(),
|
|
world,
|
|
_ => null,
|
|
audio);
|
|
var scheduler = new RecordingDestinationScheduler();
|
|
var coordinator = new WorldRevealCoordinator(
|
|
transit,
|
|
revealWindow: () => new StreamingRevealWindow(1, 1),
|
|
isRenderNeighborhoodReady: (_, _, _) => true,
|
|
isSpawnCellReady: _ => true,
|
|
isTerrainNeighborhoodReady: (_, _) => true,
|
|
areCompositeTexturesReady: () => true,
|
|
prepareCompositeTextures: (_, _) => { },
|
|
invalidateCompositeTextures: () => { },
|
|
isSpawnClaimUnhydratable: _ => false,
|
|
quiescence: quiescence,
|
|
streaming: scheduler);
|
|
const uint destinationCell = 0x3032001Cu;
|
|
const ushort teleportSequence = 1;
|
|
long generation = BeginPortal(
|
|
coordinator,
|
|
transit,
|
|
destinationCell,
|
|
teleportSequence);
|
|
|
|
Assert.False(availability.IsWorldAvailable);
|
|
Assert.True(coordinator.Evaluate(destinationCell).IsReady);
|
|
|
|
coordinator.ObserveMaterialized(
|
|
generation,
|
|
teleportSequence,
|
|
destinationCell);
|
|
|
|
Assert.True(availability.IsWorldAvailable);
|
|
Assert.False(coordinator.Snapshot.WorldViewportObserved);
|
|
Assert.False(coordinator.Snapshot.Completed);
|
|
Assert.Equal(1, audio.ResumeCalls);
|
|
Assert.Empty(scheduler.Ends);
|
|
|
|
coordinator.RevealWorldViewport();
|
|
coordinator.RevealWorldViewport();
|
|
|
|
Assert.True(availability.IsWorldAvailable);
|
|
Assert.False(coordinator.Snapshot.Completed);
|
|
Assert.Equal(1, audio.ResumeCalls);
|
|
Assert.Equal([generation], scheduler.Ends);
|
|
|
|
coordinator.Complete();
|
|
|
|
Assert.True(coordinator.Snapshot.Completed);
|
|
Assert.Equal(1, audio.ResumeCalls);
|
|
Assert.Equal([generation], scheduler.Ends);
|
|
}
|
|
|
|
[Fact]
|
|
public void TunnelContinue_TicksDestinationObjectsBehindPortalViewport()
|
|
{
|
|
var transit = new RuntimeWorldTransitState();
|
|
var availability = new WorldGenerationAvailabilityState(transit);
|
|
var world = new GpuWorldState(availability: availability);
|
|
var quiescence = new WorldGenerationQuiescence(
|
|
new SelectionState(),
|
|
world,
|
|
_ => null,
|
|
audio: null);
|
|
var scheduler = new RecordingDestinationScheduler();
|
|
var coordinator = new WorldRevealCoordinator(
|
|
transit,
|
|
revealWindow: () => new StreamingRevealWindow(1, 1),
|
|
isRenderNeighborhoodReady: (_, _, _) => true,
|
|
isSpawnCellReady: _ => true,
|
|
isTerrainNeighborhoodReady: (_, _) => true,
|
|
areCompositeTexturesReady: () => true,
|
|
prepareCompositeTextures: (_, _) => { },
|
|
invalidateCompositeTextures: () => { },
|
|
isSpawnClaimUnhydratable: _ => false,
|
|
quiescence: quiescence,
|
|
streaming: scheduler);
|
|
var calls = new List<string>();
|
|
var frame = new RetailLiveFrameCoordinator(
|
|
new global::AcDream.App.Tests.TestLiveObjectFramePhase(
|
|
_ => calls.Add("objects")),
|
|
world,
|
|
new global::AcDream.App.Tests.TestLiveSessionFramePhase(
|
|
() => calls.Add("network")),
|
|
new global::AcDream.App.Tests.TestPostNetworkCommandFramePhase(
|
|
() => calls.Add("commands")),
|
|
new global::AcDream.App.Tests.TestLiveSpatialReconcilePhase(
|
|
() => calls.Add("spatial")),
|
|
availability);
|
|
|
|
const uint destinationCell = 0x3032001Cu;
|
|
long generation = BeginPortal(
|
|
coordinator,
|
|
transit,
|
|
destinationCell,
|
|
sequence: 1);
|
|
frame.Tick(1f / 60f);
|
|
Assert.Equal(["network", "commands"], calls);
|
|
|
|
calls.Clear();
|
|
Assert.True(coordinator.Evaluate(destinationCell).IsReady);
|
|
coordinator.ObserveMaterialized(
|
|
generation,
|
|
1,
|
|
destinationCell);
|
|
frame.Tick(1f / 60f);
|
|
|
|
Assert.Equal(["objects", "network", "commands", "spatial"], calls);
|
|
Assert.Empty(scheduler.Ends);
|
|
Assert.False(coordinator.Snapshot.WorldViewportObserved);
|
|
}
|
|
|
|
[Fact]
|
|
public void SessionReset_ReleasesHostEdgesAndClearsRuntimeSnapshot()
|
|
{
|
|
var transit = new RuntimeWorldTransitState();
|
|
var availability = new WorldGenerationAvailabilityState(transit);
|
|
var world = new GpuWorldState(availability: availability);
|
|
var audio = new RecordingAudioQuiescence();
|
|
var quiescence = new WorldGenerationQuiescence(
|
|
new SelectionState(),
|
|
world,
|
|
_ => null,
|
|
audio);
|
|
var scheduler = new RecordingDestinationScheduler();
|
|
var coordinator = new WorldRevealCoordinator(
|
|
transit,
|
|
revealWindow: () => new StreamingRevealWindow(1, 1),
|
|
isRenderNeighborhoodReady: (_, _, _) => false,
|
|
isSpawnCellReady: _ => false,
|
|
isTerrainNeighborhoodReady: (_, _) => false,
|
|
areCompositeTexturesReady: () => false,
|
|
prepareCompositeTextures: (_, _) => { },
|
|
invalidateCompositeTextures: () => { },
|
|
isSpawnClaimUnhydratable: _ => false,
|
|
quiescence: quiescence,
|
|
streaming: scheduler);
|
|
long generation = BeginPortal(
|
|
coordinator,
|
|
transit,
|
|
0x3032001Cu,
|
|
sequence: 1);
|
|
|
|
coordinator.ResetSession();
|
|
|
|
Assert.Equal(RuntimePortalSnapshot.Idle, coordinator.Snapshot);
|
|
Assert.True(availability.IsWorldAvailable);
|
|
Assert.Equal(1, audio.SuspendCalls);
|
|
Assert.Equal(1, audio.ResumeCalls);
|
|
Assert.Equal([generation], scheduler.Ends);
|
|
}
|
|
|
|
[Fact]
|
|
public void RevealGeneration_OwnsExactDestinationReservationUntilCompletion()
|
|
{
|
|
var state = new State();
|
|
var scheduler = new RecordingDestinationScheduler();
|
|
WorldRevealCoordinator coordinator = state.Build(streaming: scheduler);
|
|
|
|
long first = BeginPortal(
|
|
coordinator,
|
|
state.Transit,
|
|
0x11340021u,
|
|
sequence: 1);
|
|
state.Transit.EndTeleport();
|
|
long second = BeginPortal(
|
|
coordinator,
|
|
state.Transit,
|
|
0x20210123u,
|
|
sequence: 2);
|
|
|
|
Assert.Equal(
|
|
[
|
|
(first, 0x11340021u, 1),
|
|
(second, 0x20210123u, 0),
|
|
],
|
|
scheduler.Begins);
|
|
Assert.Equal([first], scheduler.Ends);
|
|
coordinator.Cancel();
|
|
Assert.Equal([first, second], scheduler.Ends);
|
|
}
|
|
|
|
[Fact]
|
|
public void RevealGeneration_OwnsRenderUploadProfileUntilViewportRelease()
|
|
{
|
|
var state = new State();
|
|
var resources = new RecordingRenderResourceScheduler();
|
|
WorldRevealCoordinator coordinator =
|
|
state.Build(renderResources: resources);
|
|
|
|
long first = BeginPortal(
|
|
coordinator,
|
|
state.Transit,
|
|
0x11340021u,
|
|
sequence: 1);
|
|
state.Transit.EndTeleport();
|
|
long second = BeginPortal(
|
|
coordinator,
|
|
state.Transit,
|
|
0x20210123u,
|
|
sequence: 2);
|
|
|
|
Assert.Equal([first, second], resources.Begins);
|
|
Assert.Equal([first], resources.Ends);
|
|
coordinator.RevealWorldViewport();
|
|
coordinator.Complete();
|
|
|
|
Assert.Equal([first, second], resources.Ends);
|
|
}
|
|
|
|
[Fact]
|
|
public void RegistrationCallback_ReentrantReplacementRetiresExactOldSuffix()
|
|
{
|
|
var state = new State();
|
|
var scheduler = new RecordingDestinationScheduler();
|
|
WorldRevealCoordinator coordinator = null!;
|
|
long replacement = 0;
|
|
scheduler.OnBegin = () =>
|
|
{
|
|
scheduler.OnBegin = null;
|
|
replacement = coordinator.BeginLogin(0x20210123u);
|
|
};
|
|
coordinator = state.Build(streaming: scheduler);
|
|
|
|
long first = coordinator.BeginLogin(0x11340021u);
|
|
|
|
Assert.NotEqual(first, replacement);
|
|
Assert.Equal(replacement, coordinator.Snapshot.Generation);
|
|
Assert.Equal(
|
|
[
|
|
(first, 0x11340021u, 1),
|
|
(replacement, 0x20210123u, 0),
|
|
],
|
|
scheduler.Begins);
|
|
Assert.Equal([first], scheduler.Ends);
|
|
Assert.Equal(1, coordinator.Ownership.HostProjectionCount);
|
|
Assert.Equal(
|
|
0,
|
|
coordinator.Ownership.PendingHostAcknowledgementCount);
|
|
|
|
coordinator.Cancel();
|
|
|
|
Assert.Equal([first, replacement], scheduler.Ends);
|
|
Assert.Equal(0, coordinator.Ownership.HostProjectionCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void RegistrationFailure_RetriesExactSuffixWithoutReplayingBegin()
|
|
{
|
|
var state = new State();
|
|
var scheduler = new RecordingDestinationScheduler();
|
|
var resources = new RecordingRenderResourceScheduler
|
|
{
|
|
ThrowBeginCount = 1,
|
|
};
|
|
WorldRevealCoordinator coordinator = state.Build(
|
|
streaming: scheduler,
|
|
renderResources: resources);
|
|
|
|
Assert.Throws<InvalidOperationException>(
|
|
() => coordinator.BeginLogin(0x11340021u));
|
|
Assert.Single(scheduler.Begins);
|
|
Assert.Equal(1, resources.BeginAttempts);
|
|
Assert.Empty(resources.Begins);
|
|
Assert.Equal(1, coordinator.Ownership.HostProjectionCount);
|
|
Assert.Equal(
|
|
1,
|
|
coordinator.Ownership.PendingHostAcknowledgementCount);
|
|
|
|
coordinator.RetryPendingHostWork();
|
|
|
|
Assert.Single(scheduler.Begins);
|
|
Assert.Equal(2, resources.BeginAttempts);
|
|
Assert.Single(resources.Begins);
|
|
Assert.Equal(
|
|
0,
|
|
coordinator.Ownership.PendingHostAcknowledgementCount);
|
|
|
|
coordinator.Cancel();
|
|
Assert.Equal(0, coordinator.Ownership.HostProjectionCount);
|
|
}
|
|
|
|
[Fact]
|
|
public void ReservationReleaseFailure_RetriesOnlyUnfinishedCallback()
|
|
{
|
|
var state = new State();
|
|
var scheduler = new RecordingDestinationScheduler();
|
|
var resources = new RecordingRenderResourceScheduler
|
|
{
|
|
ThrowEndCount = 1,
|
|
};
|
|
WorldRevealCoordinator coordinator = state.Build(
|
|
streaming: scheduler,
|
|
renderResources: resources);
|
|
long generation = coordinator.BeginLogin(0x11340021u);
|
|
|
|
Assert.Throws<InvalidOperationException>(
|
|
coordinator.RevealWorldViewport);
|
|
Assert.Equal([generation], scheduler.Ends);
|
|
Assert.Equal(1, resources.EndAttempts);
|
|
Assert.Empty(resources.Ends);
|
|
Assert.Equal(
|
|
1,
|
|
coordinator.Ownership.PendingHostAcknowledgementCount);
|
|
|
|
coordinator.RetryPendingHostWork();
|
|
|
|
Assert.Equal([generation], scheduler.Ends);
|
|
Assert.Equal(2, resources.EndAttempts);
|
|
Assert.Equal([generation], resources.Ends);
|
|
Assert.Equal(
|
|
0,
|
|
coordinator.Ownership.PendingHostAcknowledgementCount);
|
|
coordinator.Cancel();
|
|
}
|
|
|
|
[Fact]
|
|
public void ResetFailure_RetainsWithdrawalAndConvergesOnRetry()
|
|
{
|
|
var state = new State();
|
|
var scheduler = new RecordingDestinationScheduler
|
|
{
|
|
ThrowEndCount = 1,
|
|
};
|
|
WorldRevealCoordinator coordinator =
|
|
state.Build(streaming: scheduler);
|
|
coordinator.BeginLogin(0x11340021u);
|
|
|
|
Assert.Throws<InvalidOperationException>(coordinator.ResetSession);
|
|
Assert.True(coordinator.Snapshot.Cancelled);
|
|
Assert.Equal(1, coordinator.Ownership.HostProjectionCount);
|
|
Assert.True(
|
|
coordinator.Ownership.PendingHostAcknowledgementCount > 0);
|
|
|
|
coordinator.ResetSession();
|
|
|
|
Assert.Equal(RuntimePortalSnapshot.Idle, coordinator.Snapshot);
|
|
Assert.True(coordinator.Ownership.IsSessionIdle);
|
|
Assert.Equal(2, scheduler.EndAttempts);
|
|
Assert.Single(scheduler.Ends);
|
|
}
|
|
|
|
[Fact]
|
|
public void SimulationReleaseFailure_RetriesAudioWithoutReplayingMaterialization()
|
|
{
|
|
var transit = new RuntimeWorldTransitState();
|
|
var availability = new WorldGenerationAvailabilityState(transit);
|
|
var world = new GpuWorldState(availability: availability);
|
|
var audio = new RecordingAudioQuiescence
|
|
{
|
|
ThrowResumeCount = 1,
|
|
};
|
|
var quiescence = new WorldGenerationQuiescence(
|
|
new SelectionState(),
|
|
world,
|
|
_ => null,
|
|
audio);
|
|
var coordinator = new WorldRevealCoordinator(
|
|
transit,
|
|
revealWindow: () => new StreamingRevealWindow(1, 1),
|
|
isRenderNeighborhoodReady: (_, _, _) => true,
|
|
isSpawnCellReady: _ => true,
|
|
isTerrainNeighborhoodReady: (_, _) => true,
|
|
areCompositeTexturesReady: () => true,
|
|
prepareCompositeTextures: (_, _) => { },
|
|
invalidateCompositeTextures: () => { },
|
|
isSpawnClaimUnhydratable: _ => false,
|
|
quiescence: quiescence);
|
|
const uint cell = 0x3032001Cu;
|
|
long generation = BeginPortal(
|
|
coordinator,
|
|
transit,
|
|
cell,
|
|
sequence: 1);
|
|
Assert.True(coordinator.Evaluate(cell).IsReady);
|
|
|
|
Assert.Throws<InvalidOperationException>(() =>
|
|
coordinator.ObserveMaterialized(
|
|
generation,
|
|
1,
|
|
cell));
|
|
Assert.True(coordinator.Snapshot.Materialized);
|
|
Assert.Equal(1, coordinator.PortalMaterializationCount);
|
|
Assert.Equal(1, audio.ResumeAttempts);
|
|
Assert.Equal(0, audio.ResumeCalls);
|
|
Assert.Equal(
|
|
1,
|
|
coordinator.Ownership.PendingHostAcknowledgementCount);
|
|
|
|
coordinator.RetryPendingHostWork();
|
|
|
|
Assert.Equal(2, audio.ResumeAttempts);
|
|
Assert.Equal(1, audio.ResumeCalls);
|
|
Assert.Equal(1, coordinator.PortalMaterializationCount);
|
|
Assert.Equal(
|
|
0,
|
|
coordinator.Ownership.PendingHostAcknowledgementCount);
|
|
coordinator.Cancel();
|
|
}
|
|
|
|
private static long BeginPortal(
|
|
WorldRevealCoordinator coordinator,
|
|
RuntimeWorldTransitState transit,
|
|
uint destinationCell,
|
|
ushort sequence)
|
|
{
|
|
Assert.True(transit.TryQueueTeleportStart(sequence));
|
|
Assert.True(transit.ActivateQueuedTeleport());
|
|
Assert.True(transit.OfferTeleportDestination(
|
|
new RuntimeTeleportDestination(
|
|
EntityGuid: 0x50000001u,
|
|
InstanceSequence: 1,
|
|
PositionSequence: 1,
|
|
TeleportSequence: sequence,
|
|
ForcePositionSequence: 1,
|
|
Position: new Position(
|
|
destinationCell,
|
|
Vector3.Zero,
|
|
Quaternion.Identity)),
|
|
teleportTimestampAdvanced: true));
|
|
Assert.True(coordinator.TryBeginPortal(
|
|
sequence,
|
|
destinationCell,
|
|
out long generation));
|
|
return generation;
|
|
}
|
|
|
|
private sealed class RecordingAudioQuiescence : AcDream.App.Audio.IWorldAudioQuiescence
|
|
{
|
|
public int SuspendCalls { get; private set; }
|
|
public int ResumeCalls { get; private set; }
|
|
public int SuspendAttempts { get; private set; }
|
|
public int ResumeAttempts { get; private set; }
|
|
public int ThrowSuspendCount { get; set; }
|
|
public int ThrowResumeCount { get; set; }
|
|
|
|
public void SuspendWorldAudio()
|
|
{
|
|
SuspendAttempts++;
|
|
if (ThrowSuspendCount > 0)
|
|
{
|
|
ThrowSuspendCount--;
|
|
throw new InvalidOperationException("suspend failed");
|
|
}
|
|
SuspendCalls++;
|
|
}
|
|
|
|
public void ResumeWorldAudio()
|
|
{
|
|
ResumeAttempts++;
|
|
if (ThrowResumeCount > 0)
|
|
{
|
|
ThrowResumeCount--;
|
|
throw new InvalidOperationException("resume failed");
|
|
}
|
|
ResumeCalls++;
|
|
}
|
|
}
|
|
|
|
private sealed class RecordingDestinationScheduler
|
|
: IWorldRevealStreamingScheduler
|
|
{
|
|
public List<(long Generation, uint Cell, int Radius)> Begins { get; } = [];
|
|
public List<long> Ends { get; } = [];
|
|
public int BeginAttempts { get; private set; }
|
|
public int EndAttempts { get; private set; }
|
|
public int ThrowBeginCount { get; set; }
|
|
public int ThrowEndCount { get; set; }
|
|
public Action? OnBegin { get; set; }
|
|
public Action? OnEnd { get; set; }
|
|
|
|
public void BeginDestinationReservation(
|
|
long revealGeneration,
|
|
uint destinationCell,
|
|
int requiredRenderRadius)
|
|
{
|
|
BeginAttempts++;
|
|
if (ThrowBeginCount > 0)
|
|
{
|
|
ThrowBeginCount--;
|
|
throw new InvalidOperationException("stream begin failed");
|
|
}
|
|
Begins.Add(
|
|
(revealGeneration, destinationCell, requiredRenderRadius));
|
|
OnBegin?.Invoke();
|
|
}
|
|
|
|
public void EndDestinationReservation(long revealGeneration)
|
|
{
|
|
EndAttempts++;
|
|
if (ThrowEndCount > 0)
|
|
{
|
|
ThrowEndCount--;
|
|
throw new InvalidOperationException("stream end failed");
|
|
}
|
|
Ends.Add(revealGeneration);
|
|
OnEnd?.Invoke();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// #280 (D4/§9-10): retail has ONE square — the prefetched set and the
|
|
/// blocked-on set are literally the same array. So the destination
|
|
/// reservation must open at exactly the radius the gate measures, asserted
|
|
/// against the fake window's own input rather than a literal.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData(3, 8)]
|
|
[InlineData(4, 12)]
|
|
[InlineData(5, 15)]
|
|
public void DestinationReservation_OpensAtTheDerivedGateRadius(
|
|
int nearRadius,
|
|
int farRadius)
|
|
{
|
|
const uint outdoorCell = 0x11340021u;
|
|
var streaming = new RecordingDestinationScheduler();
|
|
var state = new State
|
|
{
|
|
Window = new StreamingRevealWindow(nearRadius, farRadius),
|
|
};
|
|
WorldRevealCoordinator coordinator = state.Build(streaming: streaming);
|
|
|
|
long generation = coordinator.BeginLogin(outdoorCell);
|
|
|
|
Assert.Equal(
|
|
(generation, outdoorCell, state.Window.FarRadius),
|
|
Assert.Single(streaming.Begins));
|
|
}
|
|
|
|
[Fact]
|
|
public void IndoorDestinationReservation_OpensAtTheEnvCellArmsZeroRadius()
|
|
{
|
|
const uint indoorCell = 0x11340100u;
|
|
var streaming = new RecordingDestinationScheduler();
|
|
var state = new State { Window = new StreamingRevealWindow(4, 12) };
|
|
WorldRevealCoordinator coordinator = state.Build(streaming: streaming);
|
|
|
|
long generation = coordinator.BeginLogin(indoorCell);
|
|
|
|
Assert.Equal(
|
|
(generation, indoorCell, 0),
|
|
Assert.Single(streaming.Begins));
|
|
}
|
|
|
|
/// <summary>
|
|
/// P6, reservation half: the radii are runtime mutable through Settings.
|
|
/// Retail's <c>SmartBox::set_mid_radius</c> @0x00453180 resets, re-radiuses
|
|
/// and re-arms the blocking prefetch at the NEW value rather than
|
|
/// finishing the old hold at the old one; acdream's equivalent closes and
|
|
/// reopens the reservation on the SAME reveal generation.
|
|
/// </summary>
|
|
[Fact]
|
|
public void MidHoldRadiusChange_ReopensTheReservationOnTheSameGeneration()
|
|
{
|
|
const uint outdoorCell = 0x11340021u;
|
|
var streaming = new RecordingDestinationScheduler();
|
|
var state = new State { Window = new StreamingRevealWindow(3, 8) };
|
|
WorldRevealCoordinator coordinator = state.Build(streaming: streaming);
|
|
|
|
long generation = coordinator.BeginLogin(outdoorCell);
|
|
coordinator.Evaluate(outdoorCell);
|
|
Assert.Equal(
|
|
(generation, outdoorCell, 8),
|
|
Assert.Single(streaming.Begins));
|
|
Assert.Empty(streaming.Ends);
|
|
|
|
state.Window = new StreamingRevealWindow(5, 15);
|
|
coordinator.Evaluate(outdoorCell);
|
|
|
|
Assert.Equal(
|
|
[(generation, outdoorCell, 8), (generation, outdoorCell, 15)],
|
|
streaming.Begins);
|
|
Assert.Equal([generation], streaming.Ends);
|
|
|
|
// Idempotent: an unchanged window does not churn the reservation.
|
|
coordinator.Evaluate(outdoorCell);
|
|
Assert.Equal(2, streaming.Begins.Count);
|
|
Assert.Single(streaming.Ends);
|
|
}
|
|
|
|
private sealed class RecordingRenderResourceScheduler
|
|
: IWorldRevealRenderResourceScheduler
|
|
{
|
|
public List<long> Begins { get; } = [];
|
|
public List<long> Ends { get; } = [];
|
|
public int BeginAttempts { get; private set; }
|
|
public int EndAttempts { get; private set; }
|
|
public int ThrowBeginCount { get; set; }
|
|
public int ThrowEndCount { get; set; }
|
|
|
|
public void BeginDestinationReveal(long revealGeneration)
|
|
{
|
|
BeginAttempts++;
|
|
if (ThrowBeginCount > 0)
|
|
{
|
|
ThrowBeginCount--;
|
|
throw new InvalidOperationException("render begin failed");
|
|
}
|
|
Begins.Add(revealGeneration);
|
|
}
|
|
|
|
public void EndDestinationReveal(long revealGeneration)
|
|
{
|
|
EndAttempts++;
|
|
if (ThrowEndCount > 0)
|
|
{
|
|
ThrowEndCount--;
|
|
throw new InvalidOperationException("render end failed");
|
|
}
|
|
Ends.Add(revealGeneration);
|
|
}
|
|
}
|
|
}
|