acdream/tests/AcDream.App.Tests/Streaming/StreamingControllerReadinessTests.cs
Erik 73cdb95c7b fix(streaming): make a demoted landblock render-ready like a published one (#280 D-1)
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>
2026-08-06 07:27:28 +02:00

821 lines
32 KiB
C#

using AcDream.App.Streaming;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Wb;
using AcDream.Core.World;
using DatReaderWriter.DBObjs;
using System.Collections.Immutable;
namespace AcDream.App.Tests.Streaming;
public sealed class StreamingControllerReadinessTests
{
[Fact]
public void RenderNeighborhoodResident_RequiresEveryPublishedLandblockInRing()
{
var state = new GpuWorldState();
StreamingController controller = CreateController(state);
for (int dx = -1; dx <= 1; dx++)
for (int dy = -1; dy <= 1; dy++)
{
if (dx == 1 && dy == 1)
continue;
AddPublished(state, 0x12 + dx, 0x36 + dy);
}
Assert.False(controller.IsRenderNeighborhoodResident(0x12360022u, 1, 1));
AddPublished(state, 0x13, 0x37);
Assert.True(controller.IsRenderNeighborhoodResident(0x12360022u, 1, 1));
}
[Fact]
public void RenderNeighborhoodResident_RadiusZeroAcceptsEnvCellId()
{
var state = new GpuWorldState();
StreamingController controller = CreateController(state);
AddPublished(state, 0x8C, 0x04);
Assert.True(controller.IsRenderNeighborhoodResident(0x8C0401ADu, 0, 0));
Assert.False(controller.IsRenderNeighborhoodResident(0x8D0401ADu, 0, 0));
}
[Fact]
public void RenderNeighborhoodResident_SkipsOffMapNeighborsLikePhysicsGate()
{
var state = new GpuWorldState();
StreamingController controller = CreateController(state);
AddPublished(state, 0, 0);
AddPublished(state, 0, 1);
AddPublished(state, 1, 0);
AddPublished(state, 1, 1);
Assert.True(controller.IsRenderNeighborhoodResident(0x00000001u, 1, 1));
}
[Fact]
public void RenderNeighborhoodResident_RejectsNegativeRadius()
{
StreamingController controller = CreateController(new GpuWorldState());
Assert.Throws<ArgumentOutOfRangeException>(
() => controller.IsRenderNeighborhoodResident(0x1236FFFFu, -1, -1));
}
[Theory]
[InlineData(0xFF0401ADu)]
[InlineData(0x04FF01ADu)]
[InlineData(0x12360000u)]
[InlineData(0x12360041u)]
[InlineData(0x1236FFFEu)]
public void RenderNeighborhoodResident_RejectsInvalidMapEdgeDestination(uint cellId)
{
StreamingController controller = CreateController(new GpuWorldState());
Assert.False(controller.IsRenderNeighborhoodResident(cellId, 0, 0));
}
[Fact]
public void RenderNeighborhoodResident_WaitsForActualMeshUpload()
{
var meshes = new ReadinessMeshAdapter();
var state = new GpuWorldState(new LandblockSpawnAdapter(meshes));
StreamingController controller = CreateController(state);
uint id = 0x1236FFFFu;
const ulong envCellGeometryId = 0x2_0000_1234ul;
var entity = new WorldEntity
{
Id = 1,
ServerGuid = 0,
SourceGfxObjOrSetupId = 0x01000010u,
Position = System.Numerics.Vector3.Zero,
Rotation = System.Numerics.Quaternion.Identity,
MeshRefs = new[] { new MeshRef(0x01000010u, System.Numerics.Matrix4x4.Identity) },
};
state.AddLandblock(
new LoadedLandblock(id, new LandBlock(), new[] { entity }),
new[] { envCellGeometryId });
Assert.False(controller.IsRenderNeighborhoodResident(id, 0, 0));
meshes.ReadyIds.Add(0x01000010ul);
Assert.False(controller.IsRenderNeighborhoodResident(id, 0, 0));
meshes.ReadyIds.Add(envCellGeometryId);
Assert.True(controller.IsRenderNeighborhoodResident(id, 0, 0));
}
[Fact]
public void RenderNeighborhoodResident_PreservesShellGateWhenPromotionPrecedesBaseLoad()
{
var meshes = new ReadinessMeshAdapter();
var state = new GpuWorldState(new LandblockSpawnAdapter(meshes));
StreamingController controller = CreateController(state);
uint id = 0x1236FFFFu;
const ulong envCellGeometryId = 0x2_0000_1234ul;
state.AddEntitiesToExistingLandblock(
id,
Array.Empty<WorldEntity>(),
new[] { envCellGeometryId });
state.AddLandblock(new LoadedLandblock(id, new LandBlock(), Array.Empty<WorldEntity>()));
Assert.True(state.IsNearTier(id));
Assert.False(controller.IsRenderNeighborhoodResident(id, 0, 0));
meshes.ReadyIds.Add(envCellGeometryId);
Assert.True(controller.IsRenderNeighborhoodResident(id, 0, 0));
}
[Fact]
public void RenderNeighborhoodResident_FarTerrainDoesNotSatisfyPortalGate()
{
var meshes = new ReadinessMeshAdapter();
var state = new GpuWorldState(new LandblockSpawnAdapter(meshes));
StreamingController controller = CreateController(state);
uint id = 0x1236FFFFu;
const ulong envCellGeometryId = 0x2_0000_1234ul;
state.AddLandblock(
new LoadedLandblock(id, new LandBlock(), Array.Empty<WorldEntity>()),
tier: LandblockStreamTier.Far);
Assert.False(controller.IsRenderNeighborhoodResident(id, 0, 0));
state.AddEntitiesToExistingLandblock(
id,
Array.Empty<WorldEntity>(),
new[] { envCellGeometryId });
Assert.False(controller.IsRenderNeighborhoodResident(id, 0, 0));
meshes.ReadyIds.Add(envCellGeometryId);
Assert.True(controller.IsRenderNeighborhoodResident(id, 0, 0));
}
[Fact]
public void StaleFarPublication_DoesNotReplaceNearEntitiesOrReadiness()
{
var meshes = new ReadinessMeshAdapter();
var state = new GpuWorldState(new LandblockSpawnAdapter(meshes));
StreamingController controller = CreateController(state);
uint id = 0x1236FFFFu;
const ulong envCellGeometryId = 0x2_0000_1234ul;
var entity = new WorldEntity
{
Id = 1,
ServerGuid = 0,
SourceGfxObjOrSetupId = 0x01000010u,
Position = System.Numerics.Vector3.Zero,
Rotation = System.Numerics.Quaternion.Identity,
MeshRefs = [new MeshRef(0x01000010u, System.Numerics.Matrix4x4.Identity)],
};
meshes.ReadyIds.UnionWith([0x01000010ul, envCellGeometryId]);
state.AddLandblock(
new LoadedLandblock(id, new LandBlock(), new[] { entity }),
new[] { envCellGeometryId },
LandblockStreamTier.Near);
Assert.True(controller.IsRenderNeighborhoodResident(id, 0, 0));
state.AddLandblock(
new LoadedLandblock(id, new LandBlock(), Array.Empty<WorldEntity>()),
tier: LandblockStreamTier.Far);
Assert.True(state.IsNearTier(id));
Assert.Contains(entity, state.Entities);
Assert.True(controller.IsRenderNeighborhoodResident(id, 0, 0));
}
[Fact]
public void EnvCellPublication_RearmsSpecializedPreparationAfterPin()
{
uint id = 0x1236FFFFu;
const ulong geometryId = 0x2_0000_1234ul;
var meshes = new ReadinessMeshAdapter();
var state = new GpuWorldState(new LandblockSpawnAdapter(meshes));
var outbox = new Queue<LandblockStreamResult>();
int ensureCalls = 0;
var shell = new EnvCellShellPlacement(
0x12360001u,
geometryId,
0x0D000001u,
2,
ImmutableArray.Create<ushort>(3, 4),
System.Numerics.Vector3.Zero,
System.Numerics.Quaternion.Identity,
System.Numerics.Matrix4x4.Identity,
new WbBoundingBox(System.Numerics.Vector3.Zero, System.Numerics.Vector3.One),
new WbBoundingBox(System.Numerics.Vector3.Zero, System.Numerics.Vector3.One));
var envCells = new EnvCellLandblockBuild(
id,
Array.Empty<LoadedCell>(),
new[] { shell });
var build = new LandblockBuild(
new LoadedLandblock(id, new LandBlock(), Array.Empty<WorldEntity>()),
envCells);
outbox.Enqueue(new LandblockStreamResult.Loaded(
id,
LandblockStreamTier.Near,
build,
new AcDream.Core.Terrain.LandblockMeshData(
Array.Empty<AcDream.Core.Terrain.TerrainVertex>(),
Array.Empty<uint>())));
var controller = new StreamingController(
enqueueLoad: (_, _) => { },
enqueueUnload: _ => { },
drainCompletions: max =>
{
var batch = new List<LandblockStreamResult>();
while (batch.Count < max && outbox.Count > 0) batch.Add(outbox.Dequeue());
return batch;
},
applyTerrain: (_, _) => { },
state: state,
nearRadius: 0,
farRadius: 0,
ensureEnvCellMeshes: completed =>
{
// The specialized request is replayed only after the synthetic
// id is pinned, closing existing-at-schedule -> evicted-before-
// publication without falling through generic GfxObj decode.
Assert.Equal(1, meshes.ReferenceCounts[geometryId]);
Assert.Equal(shell, Assert.Single(completed.Shells));
ensureCalls++;
});
for (int frame = 0; frame < 32 && ensureCalls == 0; frame++)
controller.Tick(0x12, 0x36);
Assert.Equal(1, ensureCalls);
}
[Fact]
public void PromotionWithoutBase_PublishesSelfContainedNearAndPinsBeforeReplay()
{
uint id = 0x1236FFFFu;
const ulong geometryId = 0x2_0000_5678ul;
var meshes = new ReadinessMeshAdapter();
var state = new GpuWorldState(new LandblockSpawnAdapter(meshes));
var outbox = new Queue<LandblockStreamResult>();
int ensureCalls = 0;
var shell = new EnvCellShellPlacement(
0x12360001u,
geometryId,
0x0D000001u,
2,
ImmutableArray.Create<ushort>(3, 4),
System.Numerics.Vector3.Zero,
System.Numerics.Quaternion.Identity,
System.Numerics.Matrix4x4.Identity,
new WbBoundingBox(System.Numerics.Vector3.Zero, System.Numerics.Vector3.One),
new WbBoundingBox(System.Numerics.Vector3.Zero, System.Numerics.Vector3.One));
var envCells = new EnvCellLandblockBuild(
id,
Array.Empty<LoadedCell>(),
new[] { shell });
var nearBuild = new LandblockBuild(
new LoadedLandblock(id, new LandBlock(), Array.Empty<WorldEntity>()),
envCells);
var mesh = new AcDream.Core.Terrain.LandblockMeshData(
Array.Empty<AcDream.Core.Terrain.TerrainVertex>(),
Array.Empty<uint>());
outbox.Enqueue(new LandblockStreamResult.Promoted(id, nearBuild, mesh));
var controller = new StreamingController(
enqueueLoad: (_, _) => { },
enqueueUnload: _ => { },
drainCompletions: max =>
{
var batch = new List<LandblockStreamResult>();
while (batch.Count < max && outbox.Count > 0) batch.Add(outbox.Dequeue());
return batch;
},
applyTerrain: (_, _) => { },
state: state,
nearRadius: 0,
farRadius: 0,
ensureEnvCellMeshes: completed =>
{
Assert.Equal(1, meshes.ReferenceCounts[geometryId]);
Assert.Equal(shell, Assert.Single(completed.Shells));
ensureCalls++;
});
for (int frame = 0; frame < 32 && ensureCalls == 0; frame++)
controller.Tick(0x12, 0x36);
Assert.Equal(1, ensureCalls);
Assert.Equal(1, meshes.ReferenceCounts[geometryId]);
Assert.True(state.IsNearTier(id));
// A Far job that had already started may still finish after the
// self-contained promotion. It must not replace or replay the Near tier.
var farBase = new LandblockBuild(
new LoadedLandblock(id, new LandBlock(), Array.Empty<WorldEntity>()));
outbox.Enqueue(new LandblockStreamResult.Loaded(
id,
LandblockStreamTier.Far,
farBase,
mesh));
controller.Tick(0x12, 0x36);
Assert.Equal(1, ensureCalls);
Assert.Equal(1, meshes.ReferenceCounts[geometryId]);
Assert.True(state.IsNearTier(id));
}
[Fact]
public void PromotionBeforeBase_DemotedBeforeBase_DropsPendingNearPresentation()
{
uint id = 0x1236FFFFu;
const ulong geometryId = 0x2_0000_9ABCul;
var meshes = new ReadinessMeshAdapter();
var state = new GpuWorldState(new LandblockSpawnAdapter(meshes));
var outbox = new Queue<LandblockStreamResult>();
int ensureCalls = 0;
var appliedBuilds = new List<LandblockBuild>();
var demotedAfterStaticDetach = new List<uint>();
var shell = new EnvCellShellPlacement(
0x12360001u,
geometryId,
0x0D000001u,
2,
ImmutableArray.Create<ushort>(3, 4),
System.Numerics.Vector3.Zero,
System.Numerics.Quaternion.Identity,
System.Numerics.Matrix4x4.Identity,
new WbBoundingBox(System.Numerics.Vector3.Zero, System.Numerics.Vector3.One),
new WbBoundingBox(System.Numerics.Vector3.Zero, System.Numerics.Vector3.One));
var envCells = new EnvCellLandblockBuild(
id,
Array.Empty<LoadedCell>(),
new[] { shell });
var staticEntity = new WorldEntity
{
Id = 1,
ServerGuid = 0,
SourceGfxObjOrSetupId = 0x01000010u,
Position = System.Numerics.Vector3.Zero,
Rotation = System.Numerics.Quaternion.Identity,
MeshRefs = [new MeshRef(0x01000010u, System.Numerics.Matrix4x4.Identity)],
};
var nearBuild = new LandblockBuild(
new LoadedLandblock(id, new LandBlock(), new[] { staticEntity }),
envCells);
var mesh = new AcDream.Core.Terrain.LandblockMeshData(
Array.Empty<AcDream.Core.Terrain.TerrainVertex>(),
Array.Empty<uint>());
outbox.Enqueue(new LandblockStreamResult.Promoted(id, nearBuild, mesh));
var controller = new StreamingController(
enqueueLoad: (_, _) => { },
enqueueUnload: _ => { },
drainCompletions: max =>
{
var batch = new List<LandblockStreamResult>();
while (batch.Count < max && outbox.Count > 0) batch.Add(outbox.Dequeue());
return batch;
},
applyTerrain: (build, _) => appliedBuilds.Add(build),
state: state,
nearRadius: 0,
farRadius: 3,
demoteNearLayer: demotedId =>
{
Assert.DoesNotContain(staticEntity, state.Entities);
demotedAfterStaticDetach.Add(demotedId);
},
ensureEnvCellMeshes: _ => ensureCalls++);
controller.Tick(0x12, 0x36); // self-contained promotion publishes Near.
Assert.Single(appliedBuilds);
Assert.NotNull(appliedBuilds[0].EnvCells);
controller.Tick(0x12, 0x39); // normal Near->Far demotion retires presentation.
var farBase = new LandblockBuild(
new LoadedLandblock(id, new LandBlock(), Array.Empty<WorldEntity>()));
outbox.Enqueue(new LandblockStreamResult.Loaded(
id,
LandblockStreamTier.Far,
farBase,
mesh));
controller.Tick(0x12, 0x39);
// Slice E retains retirement as a cursor-budgeted transaction. A
// generous frame can retire Near and resume Far immediately; a frame
// that exhausts its elapsed budget resumes on the next Tick. Pin the
// converged state rather than wall-clock-dependent frame count.
for (int i = 0; i < 4 && appliedBuilds.Count < 2; i++)
controller.Tick(0x12, 0x39);
Assert.Equal(1, ensureCalls);
Assert.DoesNotContain(geometryId, meshes.ReferenceCounts);
Assert.DoesNotContain(staticEntity, state.Entities);
Assert.Equal(new[] { id }, demotedAfterStaticDetach);
Assert.True(state.IsLoaded(id));
Assert.False(state.IsNearTier(id));
Assert.Equal(2, appliedBuilds.Count);
Assert.Null(appliedBuilds[1].EnvCells);
Assert.Empty(appliedBuilds[1].Landblock.Entities);
}
// ── #280: the tiered reveal window ───────────────────────────────────
//
// Retail loads, draws, and blocks on ONE square (LScape::mid_radius,
// LScape::PreFetchCells @0x00505660). acdream's loaded landscape is
// tiered, so the equivalent predicate is tiered: full publication inside
// the Near ring, terrain publication out to the Far radius. Requiring Near
// tier across the whole window is unsatisfiable — nothing outside the Near
// ring is ever promoted — and holds the reveal forever.
/// <summary>
/// Case (a), the discriminating one: an INNER-ring member that only has
/// its Far-tier terrain must still fail. If this passes while the near arm
/// has been loosened into the far arm, the gate silently stops requiring
/// scenery/statics at the destination itself.
/// </summary>
[Fact]
public void TieredWindow_InnerRingFarTierMemberIsNotResident()
{
var state = new GpuWorldState();
StreamingController controller = CreateController(state);
for (int dx = -2; dx <= 2; dx++)
for (int dy = -2; dy <= 2; dy++)
{
AddPublished(
state,
0x12 + dx,
0x36 + dy,
dx == 1 && dy == 0
? LandblockStreamTier.Far
: LandblockStreamTier.Near);
}
Assert.False(
controller.IsRenderNeighborhoodResident(0x12360022u, 1, 2));
}
/// <summary>
/// Case (b): an OUTER-ring member at Far tier satisfies the gate. This is
/// the half that makes a derived Far-radius reveal window reachable at all.
/// </summary>
[Fact]
public void TieredWindow_OuterRingFarTierMemberIsResident()
{
var state = new GpuWorldState();
StreamingController controller = CreateController(state);
for (int dx = -2; dx <= 2; dx++)
for (int dy = -2; dy <= 2; dy++)
{
bool inner = Math.Abs(dx) <= 1 && Math.Abs(dy) <= 1;
AddPublished(
state,
0x12 + dx,
0x36 + dy,
inner ? LandblockStreamTier.Near : LandblockStreamTier.Far);
}
Assert.True(
controller.IsRenderNeighborhoodResident(0x12360022u, 1, 2));
}
/// <summary>
/// Case (c): an absent outer-ring member still fails. The outer arm is a
/// real publication test, not a rubber stamp.
/// </summary>
[Fact]
public void TieredWindow_AbsentOuterRingMemberIsNotResident()
{
var state = new GpuWorldState();
StreamingController controller = CreateController(state);
for (int dx = -2; dx <= 2; dx++)
for (int dy = -2; dy <= 2; dy++)
{
if (dx == 2 && dy == -2)
continue;
bool inner = Math.Abs(dx) <= 1 && Math.Abs(dy) <= 1;
AddPublished(
state,
0x12 + dx,
0x36 + dy,
inner ? LandblockStreamTier.Near : LandblockStreamTier.Far);
}
Assert.False(
controller.IsRenderNeighborhoodResident(0x12360022u, 1, 2));
AddPublished(state, 0x14, 0x34, LandblockStreamTier.Far);
Assert.True(
controller.IsRenderNeighborhoodResident(0x12360022u, 1, 2));
}
/// <summary>
/// A far radius below the near radius is a caller bug, not something to
/// clamp silently inside the predicate — the barrier clamps before it gets
/// here.
/// </summary>
[Fact]
public void TieredWindow_RejectsAFarRadiusBelowTheNearRadius()
{
StreamingController controller = CreateController(new GpuWorldState());
Assert.Throws<ArgumentOutOfRangeException>(
() => controller.IsRenderNeighborhoodResident(0x1236FFFFu, 3, 2));
}
/// <summary>
/// Retail parity (LScape::PreFetchCells' <c>&gt;= 0x7F8</c> bounds skip): a
/// map-corner destination converges because the out-of-bounds members are
/// skipped, not required. Exercised at a WIDE window, which is where the
/// derived radius now puts the gate.
/// </summary>
[Fact]
public void TieredWindow_MapCornerDestinationConvergesAtAWideRadius()
{
var state = new GpuWorldState();
StreamingController controller = CreateController(state);
for (int x = 0; x <= 4; x++)
for (int y = 0; y <= 4; y++)
{
bool inner = x <= 2 && y <= 2;
AddPublished(
state,
x,
y,
inner ? LandblockStreamTier.Near : LandblockStreamTier.Far);
}
Assert.True(
controller.IsRenderNeighborhoodResident(0x00000001u, 2, 4));
}
/// <summary>
/// #280 proof obligation P1 — the whole fix rests on this. A Far-tier
/// publication must satisfy <see cref="GpuWorldState.IsRenderReady"/>; if
/// it did not, the tiered gate's outer arm could never be satisfied and
/// the reveal would hang. Driven through the real presentation pipeline's
/// <c>PublicationKind.Far</c> path against a real
/// <see cref="LandblockSpawnAdapter"/>.
/// </summary>
[Fact]
public void FarPublication_IsRenderReadyThroughTheRealPipeline()
{
const uint landblockId = 0x1236FFFFu;
var meshes = new ReadinessMeshAdapter();
var state = new GpuWorldState(new LandblockSpawnAdapter(meshes));
var pipeline = new LandblockPresentationPipeline(
publishBeforeSpatialCommit: (_, _) => { },
state);
// A Near-shaped completion the streaming window has DOWNGRADED before
// publication: entities and physics payload are stripped by
// PublishAsFar, which is exactly the shape an outer-ring landblock
// ARRIVES in. This is not the demote transition — a landblock that was
// already published Near and is later retired to Far takes a different
// route (LandblockPresentationPipeline.BeginNearLayerRetirement) and is
// covered separately below.
var entity = new WorldEntity
{
Id = 1,
ServerGuid = 0,
SourceGfxObjOrSetupId = 0x01000010u,
Position = System.Numerics.Vector3.Zero,
Rotation = System.Numerics.Quaternion.Identity,
MeshRefs = [new MeshRef(0x01000010u, System.Numerics.Matrix4x4.Identity)],
};
var source = new LandblockBuild(
new LoadedLandblock(landblockId, new LandBlock(), new[] { entity }));
var mesh = new AcDream.Core.Terrain.LandblockMeshData(
Array.Empty<AcDream.Core.Terrain.TerrainVertex>(),
Array.Empty<uint>());
var accepted = new LandblockStreamResult.Loaded(
landblockId,
LandblockStreamTier.Near,
source,
mesh);
pipeline.PublishAsFar(accepted, source, mesh);
Assert.True(state.IsLoaded(landblockId));
Assert.False(state.IsNearTier(landblockId));
// The Far registration carries an EMPTY desired mesh set, so it is
// render-ready without any IWbMeshAdapter upload ever completing —
// note ReadinessMeshAdapter.ReadyIds is untouched below.
Assert.Empty(meshes.ReadyIds);
Assert.True(state.IsRenderReady(landblockId));
// ...and the tier check is therefore the SOLE thing keeping it out of
// the inner arm, which is what makes D2's split meaningful rather than
// decorative.
StreamingController controller = CreateController(state);
Assert.False(
controller.IsRenderNeighborhoodResident(landblockId, 0, 0));
}
/// <summary>
/// #280 D-1 regression, the OTHER way a landblock reaches Far tier. A
/// Near&#8594;Far demote retires the Near mesh layer while
/// <c>GpuWorldState.DetachNearLayer</c> keeps the landblock loaded,
/// terrain-resident and drawn. Before the fix the retirement's
/// MeshReferences stage left <c>WantsLoaded == false</c> forever — nothing
/// re-publishes an already-loaded landblock — so the demoted block
/// satisfied NEITHER arm of the widened reveal gate and the client hung in
/// portal space until relog.
///
/// <para>
/// Driven through the real <see cref="GpuWorldState"/>, the real
/// <see cref="LandblockSpawnAdapter"/> and the real
/// <see cref="LandblockPresentationPipeline"/>, which is the production
/// route <c>StreamingController.DemoteLandblock</c> takes.
/// </para>
/// </summary>
[Fact]
public void NearToFarDemote_LeavesTheLandblockRenderReadyThroughTheRealPipeline()
{
const uint landblockId = 0x1236FFFFu;
const ulong gfxObjId = 0x01000010ul;
var meshes = new ReadinessMeshAdapter();
var state = new GpuWorldState(new LandblockSpawnAdapter(meshes));
var pipeline = new LandblockPresentationPipeline(
publishBeforeSpatialCommit: (_, _) => { },
state);
meshes.ReadyIds.Add(gfxObjId);
var entity = new WorldEntity
{
Id = 1,
ServerGuid = 0,
SourceGfxObjOrSetupId = (uint)gfxObjId,
Position = System.Numerics.Vector3.Zero,
Rotation = System.Numerics.Quaternion.Identity,
MeshRefs = [new MeshRef((uint)gfxObjId, System.Numerics.Matrix4x4.Identity)],
};
state.AddLandblock(
new LoadedLandblock(landblockId, new LandBlock(), new[] { entity }));
Assert.True(state.IsNearTier(landblockId));
Assert.True(state.IsRenderReady(landblockId));
Assert.Equal(1, meshes.ReferenceCounts[gfxObjId]);
pipeline.BeginNearLayerRetirement(landblockId);
// The demote really happened: Near layer gone, mesh reference released.
Assert.False(state.IsNearTier(landblockId));
Assert.DoesNotContain(gfxObjId, meshes.ReferenceCounts.Keys);
// ...and the landblock is still loaded and still drawn.
Assert.True(state.IsLoaded(landblockId));
// The demoted block must now be indistinguishable from one that
// arrived as Far: registered, empty desired set, render-ready.
Assert.True(state.IsRenderReady(landblockId));
}
/// <summary>
/// #280 D-1, at the gate rather than at the predicate. A full Near window
/// satisfies the tiered gate; demoting one OUTER-ring member — which is
/// what an ordinary region recenter or a mid-hold quality drop does — must
/// not make the gate permanently unsatisfiable.
/// </summary>
[Fact]
public void TieredWindow_StaysResidentAfterAnOuterRingDemote()
{
var meshes = new ReadinessMeshAdapter();
var state = new GpuWorldState(new LandblockSpawnAdapter(meshes));
var pipeline = new LandblockPresentationPipeline(
publishBeforeSpatialCommit: (_, _) => { },
state);
StreamingController controller = CreateController(state);
for (int dx = -2; dx <= 2; dx++)
for (int dy = -2; dy <= 2; dy++)
AddPublished(state, 0x12 + dx, 0x36 + dy);
Assert.True(controller.IsRenderNeighborhoodResident(0x12360022u, 1, 2));
// Chebyshev distance 2 from the destination: inside the gate's far
// window, outside its near ring — the exact band a demote can land in.
pipeline.BeginNearLayerRetirement(0x1434FFFFu);
Assert.True(state.IsLoaded(0x1434FFFFu));
Assert.False(state.IsNearTier(0x1434FFFFu));
Assert.True(controller.IsRenderNeighborhoodResident(0x12360022u, 1, 2));
}
/// <summary>
/// The same transition through the METERED retirement coordinator
/// production actually composes (<c>CreateBudgeted</c>), whose
/// MeshReferences stage is a separate call site from the legacy pipeline's.
/// </summary>
[Fact]
public void NearToFarDemote_LeavesTheLandblockRenderReadyUnderBudgetedRetirement()
{
const uint landblockId = 0x1236FFFFu;
const ulong gfxObjId = 0x01000010ul;
var meshes = new ReadinessMeshAdapter();
var state = new GpuWorldState(new LandblockSpawnAdapter(meshes));
meshes.ReadyIds.Add(gfxObjId);
var entity = new WorldEntity
{
Id = 1,
ServerGuid = 0,
SourceGfxObjOrSetupId = (uint)gfxObjId,
Position = System.Numerics.Vector3.Zero,
Rotation = System.Numerics.Quaternion.Identity,
MeshRefs = [new MeshRef((uint)gfxObjId, System.Numerics.Matrix4x4.Identity)],
};
state.AddLandblock(
new LoadedLandblock(landblockId, new LandBlock(), new[] { entity }));
LandblockRetirementCoordinator coordinator =
LandblockRetirementCoordinator.CreateBudgeted(
state,
AdvanceNoopPresentationStep,
static ticket =>
{
while (AdvanceNoopPresentationStep(ticket)
!= LandblockRetirementOperationResult.NoWork)
{
}
});
coordinator.BeginNearLayer(landblockId);
int frames = 0;
while (coordinator.PendingCount != 0 && frames++ < 64)
{
var meter = new StreamingWorkMeter(new StreamingWorkBudget(
maxUpdateTime: TimeSpan.FromSeconds(1),
maxCompletionAdmissions: 64,
maxAdoptedCpuBytes: 64 * 1024 * 1024,
maxEntityOperations: 64,
maxGpuUploadBytes: 64 * 1024 * 1024,
maxGlRetireOperations: 64,
destinationReserveFraction: 0.75f));
coordinator.Advance(meter);
meter.FinishFrame();
}
Assert.Equal(0, coordinator.PendingCount);
Assert.True(state.IsLoaded(landblockId));
Assert.False(state.IsNearTier(landblockId));
Assert.DoesNotContain(gfxObjId, meshes.ReferenceCounts.Keys);
Assert.True(state.IsRenderReady(landblockId));
}
private static LandblockRetirementOperationResult AdvanceNoopPresentationStep(
LandblockRetirementTicket ticket) =>
ticket.NextIncompleteStage switch
{
LandblockRetirementStage.EntityLighting
or LandblockRetirementStage.EntityTranslucency =>
ticket.RunEntityStep(
ticket.NextIncompleteStage,
static _ => true,
static _ => { }),
LandblockRetirementStage.PluginProjection =>
ticket.RunEntityStep(
LandblockRetirementStage.PluginProjection,
static entity => entity.ServerGuid == 0,
static _ => { }),
LandblockRetirementStage.Terrain
or LandblockRetirementStage.Physics
or LandblockRetirementStage.CellVisibility
or LandblockRetirementStage.BuildingRegistry
or LandblockRetirementStage.EnvironmentCells =>
ticket.RunOnceStep(ticket.NextIncompleteStage, static () => { }),
_ => LandblockRetirementOperationResult.NoWork,
};
private static StreamingController CreateController(GpuWorldState state)
=> new(
(_, _) => { },
_ => { },
_ => Array.Empty<LandblockStreamResult>(),
(_, _) => { },
state,
nearRadius: 1,
farRadius: 2);
private static void AddPublished(
GpuWorldState state,
int x,
int y,
LandblockStreamTier tier = LandblockStreamTier.Near)
{
uint id = ((uint)x << 24) | ((uint)y << 16) | 0xFFFFu;
state.AddLandblock(
new LoadedLandblock(id, new LandBlock(), Array.Empty<WorldEntity>()),
tier: tier);
}
private sealed class ReadinessMeshAdapter : IWbMeshAdapter
{
public HashSet<ulong> ReadyIds { get; } = new();
public Dictionary<ulong, int> ReferenceCounts { get; } = new();
public void IncrementRefCount(ulong id) =>
ReferenceCounts[id] = ReferenceCounts.GetValueOrDefault(id) + 1;
public void DecrementRefCount(ulong id)
{
int next = ReferenceCounts.GetValueOrDefault(id) - 1;
if (next <= 0) ReferenceCounts.Remove(id);
else ReferenceCounts[id] = next;
}
public bool IsRenderDataReady(ulong id) => ReadyIds.Contains(id);
}
}