acdream/tests/AcDream.App.Tests/Streaming/StreamingControllerReadinessTests.cs
Erik 3aab05b0cc fix(streaming): derive the portal reveal window from the live streaming radii (#280)
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>
2026-08-05 23:54:59 +02:00

645 lines
25 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 that the streaming window has since demoted
// to Far: entities and physics payload are stripped by PublishAsFar,
// which is exactly the shape an outer-ring landblock is published in.
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));
}
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);
}
}