Both #280 review lenses returned FAIL on the same defect, and both were
right. IsRenderNeighborhoodResident's widened outer arm requires
IsRenderReady out to FarRadius, justified by "a Far-tier landblock
registers with an empty mesh set and is therefore render-ready." That held
only for a landblock that ARRIVED as Far. The second, equally first-class
way to be Far tier is a Near->Far DEMOTE:
DemoteLandblock -> EnqueueNearLayerRetirement
-> LandblockRetirementStage.MeshReferences
-> GpuWorldState.ReleaseLandblockMeshReferences
-> LandblockSpawnAdapter.OnLandblockUnloaded => WantsLoaded = false
while DetachNearLayer deliberately keeps the landblock loaded, terrain-mesh
resident, terrain-collision resident and DRAWN. Nothing re-publishes an
already-loaded landblock, so the demoted member satisfied NEITHER arm of
the gate, permanently: wormhole tunnel plus centered "In Portal Space -
Please Wait..." forever, no recovery short of relog.
Reachable by ordinary play. Two consecutive recalls to the same landblock
with walking in between makes ChangesStreamingCenter false, so there is no
origin recenter and the region recentres through the ordinary demote diff.
Also reachable via a mid-hold quality-preset drop -- ironically the exact
scenario ReconcileDestinationReservationRadius was added to support. The
pre-#280 radius-1 gate never touched that band, because nothing inside the
Near ring can demote.
FIX SHAPE. Make the two routes genuinely equivalent rather than teaching
the predicate to tolerate the difference. ReleaseLandblockMeshReferences
becomes "reconcile the registration to the post-retirement tier": after the
release converges, if the landblock is still loaded AND still Far tier,
re-assert the empty registration -- the identical OnLandblockLoaded(lb,
empty) a PublicationKind.Far activation makes. It is empty by construction:
DetachNearLayer retains only live server projections, which the adapter's
atlas-tier filter skips. A full retirement is unaffected (DetachLandblock
clears both _loaded and _tierByLandblock), and a throwing release still
retries because the re-assert is only reached after the adapter converged.
The alternative -- "|| (IsFarTier && IsLoaded)" at the gate -- was
rejected: it fixes one caller while leaving IsRenderReady meaning two
different things, which is precisely how this defect arose. After this
change the predicate reads "drawable at its current tier" for every caller,
with no knowledge of how the landblock got there.
WHY THE TESTS MISSED IT, fixed here too:
- Proof obligation P2 was discharged against RESIDENCY (the FarRadius+2
eviction threshold) rather than against IsRenderReady, the gate's actual
atom. The contract now carries the correction and the restated
obligation: no transition may REVOKE IsRenderReady from a landblock that
stays inside FarRadius.
- WorldRevealDerivedWindowIntegrationTests advertised itself as end-to-end
against the real GpuWorldState but constructed it with no spawn adapter,
so its IsRenderReady degenerated to IsLoaded via the "?? true". The
single most load-bearing predicate in the change was stubbed out by a
null in the test named after it -- the same shape as C5b's D3 and #276's
three settler tests. Every fixture in that file now owns a real
LandblockSpawnAdapter.
- The P1 test's comment described its subject as "a Near-shaped completion
the streaming window has since DEMOTED to Far". It is not; it is a fresh
PublishAsFar, the case that does hold. Corrected, since a future reader
would have taken it as demote coverage.
Four new regression tests, all driving the real GpuWorldState +
LandblockSpawnAdapter + LandblockPresentationPipeline through an actual
demote, and all sabotage-verified in both directions (fail with the
production change reverted, pass with it):
NearToFarDemote_LeavesTheLandblockRenderReadyThroughTheRealPipeline
NearToFarDemote_LeavesTheLandblockRenderReadyUnderBudgetedRetirement
TieredWindow_StaysResidentAfterAnOuterRingDemote
OutdoorReveal_SurvivesAnOuterRingDemoteDuringTheHold
The budgeted variant exists because production composes
LandblockRetirementCoordinator.CreateBudgeted, whose MeshReferences stage
is a separate call site from the legacy pipeline's.
SECONDARY, same commit:
- R-1: ACDREAM_PROBE_REVEAL_RADIUS=0 was parser-accepted and
Runtime-rejected -- it yields far = 0 for an outdoor destination, which
fails invalid-readiness-shape on every acknowledgement, hanging the very
A/B route the probe exists to measure. Parser floor raised to 1, with a
7-case table test.
- R-2: the composite-warmup TRIGGER had silently moved onto the far
window's critical path. Pre-#280 the gate and the composite domain were
the same radius-1 square; #280 widened the gate without widening the
domain, so every composite upload serialised behind the last outer-ring
landblock for no readiness benefit. Warmup now starts once the NEAR
sub-window is published -- trigger scope == domain scope, as before. The
reveal gate is untouched: Evaluate still requires the full window AND
composite readiness.
- AP-150 filed: acdream's RetailWaitCueDelay = 5 s arming is NOT retail's
trigger, and #280's commit message got this wrong on both clauses. Retail
emits the notice unconditionally per tunnel rotation segment, in the else
arm of the segment-expiry test at 0x004D6FCD; segment duration is
RandDouble(0.6, 1.8) s, byte-decoded at 0x004D6FE6. The 5.0 constant at
VA 0x007991B0 is CellManager::CheckPrefetchStatus's prefetch RETRY
cadence and has nothing to do with the cue. acdream's own 0.6/1.8 segment
constants already match retail exactly; only the arming is wrong.
Adopting retail's unconditional emit is filed as #329 rather than folded
in here -- it is a user-visible presentation change and wants the user's
eyes.
- AP-151 filed: the gate is materially STRICTER than retail on the
mesh-build/GPU-upload axis. Retail's LScape::PreFetchCells blocks on DAT
RESIDENCY only -- no geometry construction, no upload; that work is lazy
at draw. acdream requires a DAT read, terrain mesh build, render-thread
upload, spatial commit, collision admission and spawn-adapter activation
per member of a 625-member window, metered at MaxCompletionsPerFrame.
Nothing bounds the hold. This is the OPPOSITE asymmetry from AP-149; both
are live at once, on different axes.
- AD-2's amendment stated the false Far-tier readiness assumption verbatim;
corrected, along with the same error in
claude-memory/reference_two_tier_streaming.md, which now carries an
explicit DO-NOT-RETRY on the special-case-the-predicate shape.
- AP-115 scope-noted (it covers the cue's presentation, not its arming).
- #326's SmartBox::set_mid_radius citation corrected: the entry is
0x00453180; 0x004531D0 is the mid-function re-arm branch.
Blast radius: GpuWorldState, LandblockSpawnAdapter,
WorldRevealReadinessBarrier and StreamingDiagnostics are all App-internal;
AcDream.Headless and AcDream.Runtime reference none of them outside
comments. Headless tests run green as part of the gate below, per C5b's
lesson about surveys that skip the no-window host.
Gates: Release build 0 errors, 18 pre-existing xUnit analyzer warnings.
Complete suite "dotnet test AcDream.slnx -c Release -m:1" with
ACDREAM_PAK_PATH set: 11,192 passed / 4 skipped / 0 failed, from a clean
rebuild (a prior session's deleted probe file had been compiled into a
stale test DLL). Baseline at fafc0b65 was 11,179 / 4 / 0; the +13 delta
reconciles exactly to this commit's additions -- 3 readiness tests, 1
integration test, 7 parser table cases, 2 warmup-trigger tests. None of the
known flakes #302/#308/#321 surfaced, and none is conflated with the
finding above.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
370 lines
13 KiB
C#
370 lines
13 KiB
C#
using AcDream.App.Streaming;
|
|
|
|
namespace AcDream.App.Tests.Streaming;
|
|
|
|
public sealed class WorldRevealReadinessBarrierTests
|
|
{
|
|
private sealed class State
|
|
{
|
|
public bool RenderReady;
|
|
|
|
/// <summary>
|
|
/// Radius-aware override, so a test can distinguish "the Near
|
|
/// sub-window is published" from "the whole Far window is published".
|
|
/// </summary>
|
|
public Func<int, int, bool>? RenderReadyByRadius;
|
|
|
|
public bool SpawnCellReady;
|
|
public bool TerrainReady;
|
|
public bool CompositeReady;
|
|
public bool Unhydratable;
|
|
public int Invalidations;
|
|
public int Preparations;
|
|
public uint PreparedCell;
|
|
public int PreparedRadius = -1;
|
|
public int RenderNearRadius = -1;
|
|
public int RenderFarRadius = -1;
|
|
public int TerrainRadius = -1;
|
|
|
|
/// <summary>
|
|
/// The live streaming window the barrier derives from. Mutable so a
|
|
/// test can prove the radii are re-read per evaluation rather than
|
|
/// captured at construction (#280 T6 / P6).
|
|
/// </summary>
|
|
public StreamingRevealWindow Window { get; set; } =
|
|
new(NearRadius: 1, FarRadius: 1);
|
|
|
|
public WorldRevealReadinessBarrier Build() => new(
|
|
revealWindow: () => Window,
|
|
isRenderNeighborhoodReady: (cell, nearRadius, farRadius) =>
|
|
{
|
|
RenderNearRadius = nearRadius;
|
|
RenderFarRadius = farRadius;
|
|
return RenderReadyByRadius?.Invoke(nearRadius, farRadius)
|
|
?? RenderReady;
|
|
},
|
|
isSpawnCellReady: _ => SpawnCellReady,
|
|
isTerrainNeighborhoodReady: (cell, radius) =>
|
|
{
|
|
TerrainRadius = radius;
|
|
return TerrainReady;
|
|
},
|
|
areCompositeTexturesReady: () => CompositeReady,
|
|
prepareCompositeTextures: (cell, radius) =>
|
|
{
|
|
Preparations++;
|
|
PreparedCell = cell;
|
|
PreparedRadius = radius;
|
|
},
|
|
invalidateCompositeTextures: () => Invalidations++,
|
|
isSpawnClaimUnhydratable: _ => Unhydratable);
|
|
}
|
|
|
|
[Fact]
|
|
public void Begin_InvalidatesPriorDestinationTextureReadiness()
|
|
{
|
|
var state = new State();
|
|
var barrier = state.Build();
|
|
|
|
barrier.Begin();
|
|
|
|
Assert.Equal(1, state.Invalidations);
|
|
}
|
|
|
|
/// <summary>
|
|
/// #280: the outdoor gate is DERIVED from the live streaming window, never
|
|
/// a constant. Every assertion below references the fake window's own
|
|
/// input — a test asserting a literal radius would reproduce the exact
|
|
/// defect class this slice removes.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData(3, 8)]
|
|
[InlineData(5, 15)]
|
|
[InlineData(4, 12)]
|
|
public void OutdoorRequiredWindow_IsTheLiveStreamingWindow(
|
|
int nearRadius,
|
|
int farRadius)
|
|
{
|
|
const uint outdoorCell = 0x11340021u;
|
|
var window = new StreamingRevealWindow(nearRadius, farRadius);
|
|
var state = new State { Window = window };
|
|
var barrier = state.Build();
|
|
|
|
Assert.False(barrier.IsReady(outdoorCell));
|
|
|
|
Assert.Equal(window.FarRadius, state.RenderFarRadius);
|
|
Assert.Equal(window.NearRadius, state.RenderNearRadius);
|
|
Assert.Equal(window.FarRadius, barrier.RequiredRenderRadius(outdoorCell));
|
|
Assert.Equal(window, barrier.RequiredWindow(outdoorCell));
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(3, 8)]
|
|
[InlineData(5, 15)]
|
|
public void IndoorRequiredWindow_IsZeroRegardlessOfTheStreamingWindow(
|
|
int nearRadius,
|
|
int farRadius)
|
|
{
|
|
const uint indoorCell = 0x11340100u;
|
|
var state = new State
|
|
{
|
|
Window = new StreamingRevealWindow(nearRadius, farRadius),
|
|
};
|
|
var barrier = state.Build();
|
|
|
|
Assert.Equal(
|
|
new StreamingRevealWindow(0, 0),
|
|
barrier.RequiredWindow(indoorCell));
|
|
Assert.Equal(0, barrier.RequiredRenderRadius(indoorCell));
|
|
}
|
|
|
|
/// <summary>
|
|
/// P6: the radii are runtime mutable through Settings, and retail re-arms
|
|
/// the blocking prefetch at the NEW radius mid-hold
|
|
/// (<c>SmartBox::set_mid_radius</c> @0x00453180). A window captured at
|
|
/// construction would only misbehave when someone opens Settings during a
|
|
/// portal, so it must be proven live.
|
|
/// </summary>
|
|
[Fact]
|
|
public void RequiredWindow_IsRereadOnEveryEvaluationWithoutReconstruction()
|
|
{
|
|
const uint outdoorCell = 0x11340021u;
|
|
var state = new State { Window = new StreamingRevealWindow(3, 8) };
|
|
var barrier = state.Build();
|
|
|
|
barrier.Evaluate(outdoorCell);
|
|
Assert.Equal(8, state.RenderFarRadius);
|
|
|
|
state.Window = new StreamingRevealWindow(5, 15);
|
|
WorldRevealReadinessSnapshot second = barrier.Evaluate(outdoorCell);
|
|
|
|
Assert.Equal(state.Window.FarRadius, state.RenderFarRadius);
|
|
Assert.Equal(state.Window.NearRadius, state.RenderNearRadius);
|
|
Assert.Equal(state.Window.FarRadius, second.RequiredRenderRadius);
|
|
Assert.Equal(state.Window.NearRadius, second.RequiredNearRadius);
|
|
}
|
|
|
|
[Fact]
|
|
public void OutdoorReveal_JoinsRenderTexturesAndTerrainOverTheDerivedWindow()
|
|
{
|
|
const uint outdoorCell = 0x11340021u;
|
|
var state = new State { Window = new StreamingRevealWindow(4, 12) };
|
|
var barrier = state.Build();
|
|
|
|
Assert.False(barrier.IsReady(outdoorCell));
|
|
Assert.Equal(state.Window.FarRadius, state.RenderFarRadius);
|
|
|
|
state.RenderReady = true;
|
|
barrier.Prepare(outdoorCell);
|
|
Assert.Equal(1, state.Preparations);
|
|
Assert.Equal(outdoorCell, state.PreparedCell);
|
|
// D3: the composite domain is entity-scoped and Far-tier builds carry
|
|
// no entities, so composites warm over the NEAR radius only.
|
|
Assert.Equal(state.Window.NearRadius, state.PreparedRadius);
|
|
|
|
state.CompositeReady = true;
|
|
Assert.False(barrier.IsReady(outdoorCell));
|
|
|
|
state.TerrainReady = true;
|
|
Assert.True(barrier.IsReady(outdoorCell));
|
|
Assert.Equal(state.Window.FarRadius, state.TerrainRadius);
|
|
}
|
|
|
|
[Fact]
|
|
public void IndoorReveal_UsesCenterRenderAndExactEnvCellPhysics()
|
|
{
|
|
const uint indoorCell = 0x11340100u;
|
|
var state = new State
|
|
{
|
|
Window = new StreamingRevealWindow(4, 12),
|
|
RenderReady = true,
|
|
CompositeReady = true,
|
|
TerrainReady = true,
|
|
};
|
|
var barrier = state.Build();
|
|
|
|
barrier.Prepare(indoorCell);
|
|
Assert.Equal(0, state.PreparedRadius);
|
|
Assert.False(barrier.IsReady(indoorCell));
|
|
Assert.Equal(-1, state.TerrainRadius);
|
|
|
|
state.SpawnCellReady = true;
|
|
Assert.True(barrier.IsReady(indoorCell));
|
|
Assert.Equal(0, state.RenderNearRadius);
|
|
Assert.Equal(0, state.RenderFarRadius);
|
|
}
|
|
|
|
[Fact]
|
|
public void Prepare_WaitsForStaticMeshPublication()
|
|
{
|
|
var state = new State();
|
|
var barrier = state.Build();
|
|
|
|
barrier.Prepare(0x11340021u);
|
|
|
|
Assert.Equal(0, state.Preparations);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The composite warmup TRIGGER is scoped to the composite DOMAIN. #280
|
|
/// widened the reveal gate to the whole Far window but left the domain at
|
|
/// <c>NearRadius</c>; leaving the trigger on the gate would serialise every
|
|
/// composite upload behind the last outer-ring landblock and lengthen the
|
|
/// hold by the whole warmup duration for no readiness benefit.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Prepare_StartsWarmupOnceTheNearSubWindowIsPublished()
|
|
{
|
|
const uint outdoorCell = 0x11340021u;
|
|
var state = new State
|
|
{
|
|
Window = new StreamingRevealWindow(4, 12),
|
|
// Published out to the Near radius only — the Far ring is still
|
|
// streaming, which is the normal state for most of the hold.
|
|
RenderReadyByRadius = (_, farRadius) => farRadius <= 4,
|
|
};
|
|
var barrier = state.Build();
|
|
|
|
barrier.Prepare(outdoorCell);
|
|
|
|
Assert.Equal(1, state.Preparations);
|
|
Assert.Equal(state.Window.NearRadius, state.PreparedRadius);
|
|
Assert.Equal(state.Window.NearRadius, state.RenderFarRadius);
|
|
// ...and the gate itself has NOT opened: it still measures the whole
|
|
// derived window, and composites are not ready yet either.
|
|
Assert.False(barrier.IsReady(outdoorCell));
|
|
Assert.Equal(state.Window.FarRadius, state.RenderFarRadius);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The Near sub-window is a real precondition, not a formality: an
|
|
/// unpublished destination neighbourhood still blocks warmup.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Prepare_StillWaitsWhenTheNearSubWindowIsIncomplete()
|
|
{
|
|
var state = new State
|
|
{
|
|
Window = new StreamingRevealWindow(4, 12),
|
|
RenderReadyByRadius = (_, _) => false,
|
|
};
|
|
|
|
state.Build().Prepare(0x11340021u);
|
|
|
|
Assert.Equal(0, state.Preparations);
|
|
}
|
|
|
|
[Fact]
|
|
public void ImpossibleClaim_CrossesExistingLoudRecoveryPath()
|
|
{
|
|
var state = new State { Unhydratable = true };
|
|
var barrier = state.Build();
|
|
|
|
barrier.Prepare(0x113401FFu);
|
|
|
|
Assert.True(barrier.IsReady(0x113401FFu));
|
|
Assert.Equal(0, state.Preparations);
|
|
Assert.Equal(-1, state.RenderFarRadius);
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_ExposesTheCanonicalOutdoorDecisionWithoutRepeatingDomains()
|
|
{
|
|
const uint outdoorCell = 0x11340021u;
|
|
var state = new State
|
|
{
|
|
Window = new StreamingRevealWindow(4, 12),
|
|
RenderReady = true,
|
|
CompositeReady = true,
|
|
TerrainReady = true,
|
|
};
|
|
|
|
WorldRevealReadinessSnapshot snapshot = state.Build().Evaluate(outdoorCell);
|
|
|
|
Assert.Equal(outdoorCell, snapshot.DestinationCell);
|
|
Assert.False(snapshot.IsIndoor);
|
|
Assert.Equal(state.Window.FarRadius, snapshot.RequiredRenderRadius);
|
|
Assert.Equal(state.Window.NearRadius, snapshot.RequiredNearRadius);
|
|
Assert.True(snapshot.IsRenderNeighborhoodReady);
|
|
Assert.True(snapshot.AreCompositeTexturesReady);
|
|
Assert.True(snapshot.IsCollisionReady);
|
|
Assert.True(snapshot.IsReady);
|
|
Assert.Equal(-1, state.PreparedRadius);
|
|
}
|
|
|
|
[Fact]
|
|
public void Evaluate_ShortCircuitsDownstreamDomainsUntilRenderPublication()
|
|
{
|
|
var state = new State
|
|
{
|
|
CompositeReady = true,
|
|
TerrainReady = true,
|
|
SpawnCellReady = true,
|
|
};
|
|
|
|
WorldRevealReadinessSnapshot snapshot = state.Build().Evaluate(0x11340100u);
|
|
|
|
Assert.False(snapshot.IsRenderNeighborhoodReady);
|
|
Assert.False(snapshot.AreCompositeTexturesReady);
|
|
Assert.False(snapshot.IsCollisionReady);
|
|
Assert.False(snapshot.IsReady);
|
|
Assert.Equal(-1, state.TerrainRadius);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The near arm demands Near-tier publication and can therefore never
|
|
/// exceed the outer arm. A misconfigured window must clamp rather than
|
|
/// hand <c>IsRenderNeighborhoodResident</c> an argument it rejects.
|
|
/// </summary>
|
|
[Fact]
|
|
public void RequiredWindow_ClampsANearRadiusThatExceedsTheFarRadius()
|
|
{
|
|
var state = new State { Window = new StreamingRevealWindow(9, 4) };
|
|
var barrier = state.Build();
|
|
|
|
Assert.Equal(
|
|
new StreamingRevealWindow(4, 4),
|
|
barrier.RequiredWindow(0x11340021u));
|
|
}
|
|
|
|
/// <summary>
|
|
/// D5: the probe reproduces the pre-fix gate on the same binary so the
|
|
/// connected route's A/B pair is a real comparison. It is a measurement
|
|
/// override, never a user-facing prefetch knob (§3, T11).
|
|
/// </summary>
|
|
[Fact]
|
|
public void RevealRadiusOverride_ReplacesTheDerivedWindowAndClampsTheNearArm()
|
|
{
|
|
var window = new StreamingRevealWindow(4, 12);
|
|
|
|
Assert.Equal(
|
|
window,
|
|
StreamingDiagnostics.ApplyRevealRadiusOverride(window, null));
|
|
Assert.Equal(
|
|
new StreamingRevealWindow(1, 1),
|
|
StreamingDiagnostics.ApplyRevealRadiusOverride(window, 1));
|
|
Assert.Equal(
|
|
new StreamingRevealWindow(4, 25),
|
|
StreamingDiagnostics.ApplyRevealRadiusOverride(window, 25));
|
|
}
|
|
|
|
/// <summary>
|
|
/// The probe's parser floor is 1, not 0. A zero override makes
|
|
/// <c>RequiredWindow</c> return far = 0 for an OUTDOOR destination, which
|
|
/// Runtime's <c>invalid-readiness-shape</c> invariant rejects on every
|
|
/// acknowledgement — i.e. the probe would hang the exact A/B route it
|
|
/// exists to measure. Reject it where it is read, not where it detonates.
|
|
/// </summary>
|
|
[Theory]
|
|
[InlineData(null, null)]
|
|
[InlineData("", null)]
|
|
[InlineData("nonsense", null)]
|
|
[InlineData("0", null)]
|
|
[InlineData("-1", null)]
|
|
[InlineData("1", 1)]
|
|
[InlineData("12", 12)]
|
|
public void RevealRadiusOverride_ParserRefusesRadiiRuntimeWouldReject(
|
|
string? raw,
|
|
int? expected) =>
|
|
Assert.Equal(expected, StreamingDiagnostics.ParseRadius(raw));
|
|
}
|