Both items come from the D-1 fix review (both lenses PASS, D-1 genuinely closed). No production behaviour changes. L1 — THE SEAM HAD NO COVERAGE. The D-1 fix's "empty by construction" claim rests on LandblockSpawnAdapter's atlas-tier filter (`if (entity.ServerGuid != 0) continue;`) skipping the live server projections DetachNearLayer deliberately RETAINS across a demote. The reviewer removed that filter and all 4,170 App tests passed — only two Core unit tests caught it, none through a demote. So the invariant the re-assert depends on could have been deleted silently, re-opening D-1 by another route: a non-empty re-assert whose mesh reference is never satisfied leaves IsRenderReady false, which is the portal hang again. NearToFarDemote_WithALiveServerEntity_StaysRenderReady now demotes a landblock that CARRIES a live server-spawned entity through the real GpuWorldState + LandblockSpawnAdapter + LandblockPresentationPipeline, and asserts the retained entity never enters the desired set. Sabotage-verified: with the filter removed, exactly one test fails — this one — and the other 25 pass, including all four D-1 regression tests. That is the finding restated as a measurement: the D-1 tests genuinely do not cover this seam, and now something does. AP-150 citation corrected: the row cited 0x004D7064 as the ECM_UI::SendNotice_DisplayStringInfo call site. That address is the PStringBase construction of the "In Portal Space - Please Wait..." literal (:219516); the actual call is 0x004D70A1 (-> 0x006925B0). Same class of slip the #280 commit had just corrected for #326 — worth noting that a row filed WITH a byte-level disassembly still mis-cited a neighbouring address. Also refactored the existing pipeline demote test to keep its doc comment attached to its own method (an earlier insertion had orphaned its [Fact]). App.Tests 4,170 -> 4,171 passed / 3 skipped, net +1 for the new test. No new skips; none of #302/#308/#321 surfaced. src/ is byte-unchanged (the sabotage was reverted and verified). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
898 lines
36 KiB
C#
898 lines
36 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>>= 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→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()
|
|
=> NearToFarDemoteLeavesTheLandblockRenderReadyCore();
|
|
|
|
/// <summary>
|
|
/// L1, filed at the D-1 fix review: the fix's "empty by construction"
|
|
/// claim rests on <c>LandblockSpawnAdapter</c>'s atlas-tier filter
|
|
/// (<c>if (entity.ServerGuid != 0) continue;</c>) skipping the live
|
|
/// server projections that <c>DetachNearLayer</c> deliberately RETAINS.
|
|
/// That seam had no composition-level coverage — the review removed the
|
|
/// filter and all 4,170 App tests still passed, with only two Core unit
|
|
/// tests catching it and none through a demote. So the invariant the
|
|
/// re-assert depends on could have been deleted silently.
|
|
///
|
|
/// <para>
|
|
/// Here the demoted landblock CARRIES a live server-spawned entity. If
|
|
/// the filter stops skipping it, the re-asserted registration is no
|
|
/// longer empty, its mesh reference is never satisfied, and
|
|
/// <c>IsRenderReady</c> goes false — which is D-1 again by another
|
|
/// route. Sabotage that reddens this: delete the `ServerGuid != 0`
|
|
/// continue in <c>LandblockSpawnAdapter.OnLandblockLoaded</c>.
|
|
/// </para>
|
|
/// </summary>
|
|
[Fact]
|
|
public void NearToFarDemote_WithALiveServerEntity_StaysRenderReady()
|
|
{
|
|
const uint landblockId = 0x1237FFFFu;
|
|
const ulong staticGfxObjId = 0x01000011ul;
|
|
const ulong serverGfxObjId = 0x01000012ul;
|
|
var meshes = new ReadinessMeshAdapter();
|
|
var state = new GpuWorldState(new LandblockSpawnAdapter(meshes));
|
|
var pipeline = new LandblockPresentationPipeline(
|
|
publishBeforeSpatialCommit: (_, _) => { },
|
|
state);
|
|
meshes.ReadyIds.Add(staticGfxObjId);
|
|
meshes.ReadyIds.Add(serverGfxObjId);
|
|
|
|
// An ordinary atlas-tier prop, and a server-spawned entity of the
|
|
// kind DetachNearLayer retains across the demote.
|
|
var staticEntity = new WorldEntity
|
|
{
|
|
Id = 1,
|
|
ServerGuid = 0,
|
|
SourceGfxObjOrSetupId = (uint)staticGfxObjId,
|
|
Position = System.Numerics.Vector3.Zero,
|
|
Rotation = System.Numerics.Quaternion.Identity,
|
|
MeshRefs = [new MeshRef((uint)staticGfxObjId, System.Numerics.Matrix4x4.Identity)],
|
|
};
|
|
var serverEntity = new WorldEntity
|
|
{
|
|
Id = 2,
|
|
ServerGuid = 0x50000123u,
|
|
SourceGfxObjOrSetupId = (uint)serverGfxObjId,
|
|
Position = System.Numerics.Vector3.Zero,
|
|
Rotation = System.Numerics.Quaternion.Identity,
|
|
MeshRefs = [new MeshRef((uint)serverGfxObjId, System.Numerics.Matrix4x4.Identity)],
|
|
};
|
|
state.AddLandblock(new LoadedLandblock(
|
|
landblockId,
|
|
new LandBlock(),
|
|
new[] { staticEntity, serverEntity }));
|
|
|
|
// Only the atlas-tier prop ever drove a reference count.
|
|
Assert.True(state.IsRenderReady(landblockId));
|
|
Assert.Equal(1, meshes.ReferenceCounts[staticGfxObjId]);
|
|
Assert.DoesNotContain(serverGfxObjId, meshes.ReferenceCounts.Keys);
|
|
|
|
pipeline.BeginNearLayerRetirement(landblockId);
|
|
|
|
Assert.False(state.IsNearTier(landblockId));
|
|
Assert.True(state.IsLoaded(landblockId));
|
|
// The retained server entity must not have re-entered the desired
|
|
// set: the re-assert stays empty and the block stays render-ready.
|
|
Assert.DoesNotContain(serverGfxObjId, meshes.ReferenceCounts.Keys);
|
|
Assert.DoesNotContain(staticGfxObjId, meshes.ReferenceCounts.Keys);
|
|
Assert.True(state.IsRenderReady(landblockId));
|
|
}
|
|
|
|
private static void NearToFarDemoteLeavesTheLandblockRenderReadyCore()
|
|
{
|
|
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);
|
|
}
|
|
}
|