acdream/tests/AcDream.App.Tests/Rendering/Walk/WalkProductionWorldDataTests.cs
Erik c94a1a407e feat(render): Campaign OVERHAUL S2 chunk 5 + closeout — registry is the only render membership owner
Chunk 5 (consumer cutover): WalkProductionWorldData's per-cell views are
borrowed from ShadowObjectRegistry.GetRetailPartEntriesInCell and resolved
through RenderSceneQuery.TryGetByLocalEntityId; every render-side sweep,
bucket, parent-cell and root-position fallback is deleted (AD-116 for the
one-frame registry→scene window, counted in UnregisteredRenderMembershipCount).
A live entity with visual parts but no collision geometry registers
render-only (LiveEntityCollisionBuilder computes the part array before the
empty-shapes gate).

Closeout fixes found while landing it:
- RefloodOwnerForLandblock forwards the retained part array — a reflood is
  retail's recalc_cross_cells over the SAME CPartArray; without it every owner
  touched by a landblock replacement commit lost its render membership.
- Non-colliding DAT statics register render-only from BOTH publishers
  (LandblockPhysicsPublisher.PublishStaticEntity,
  LandblockPhysicsContentBuilder.RegisterRenderOnlyStatic). The G2 self-gate
  pixel diff caught them vanishing (Facility Hub wall panels): retail floods
  every object regardless of collision (CEnvCell::init_static_objects
  0x0052c350, add_shadows_to_cells 0x00514ae0).
- S2 dual review fix batch (arch + retail lens, lead-verified):
  Suspend clears the retail product (remove_shadows_from_cells 0x00511230 is
  one transaction); AttachChild/DetachChild advance the mutation revision so
  a prepared SetPosition cannot clobber a child's rows; an attached child
  never floods on its own re-registration; RemoveLandblock and the non-rooted
  RetireOwnerFromLandblock prune retail rows (render-only statics end with
  their landblock); a render-only owner's no-cell-array commit republishes at
  its destination cell (AD-117); an empty non-null part array is treated as
  null; per-move closures/LINQ replaced by index loops; EnvCell shells stay
  out of the scene's LocalEntityId index (payload-less records); the index
  predicate compares the id; the dead per-cell scene indices are deleted.

Register: AD-116 (chunk 5), AD-117 (four residual Contract A/B readings).
Evidence: s2-membership-ownership-map.md §8 (chunk 5) and §9 (closeout).

Gates (Release): Core 4,984/4,984; Content 214/214; Runtime 1,884/1,884;
App hermetic lane 6,760/6,760; App InstalledDat lane 217 pass / 1 skip /
2 pre-existing #383 layout-fixture failures; App Windows lane 1/1.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 01:01:47 +02:00

488 lines
21 KiB
C#

using System.Numerics;
using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Scene.Arch;
using AcDream.App.Rendering.Walk;
using AcDream.Core.Physics;
using AcDream.Core.World;
namespace AcDream.App.Tests.Rendering.Walk;
/// <summary>
/// Campaign OVERHAUL S2 chunk 5: <see cref="WalkProductionWorldData"/> no
/// longer sweeps the scene to rebuild membership into bucket dictionaries —
/// <c>GetCellStatics</c>/<c>GetCellDynamics</c>/<c>GetOutdoorStatics</c>/
/// <c>GetOutdoorDynamics</c> are borrowed, on-demand views over
/// <see cref="ShadowObjectRegistry"/>'s retail per-cell part-entry product,
/// resolved back to a <see cref="RenderProjectionRecord"/> through
/// <see cref="RenderSceneQuery.TryGetByLocalEntityId"/>. Every test below
/// drives the REAL production path end to end — a real (bare, no-DAT)
/// <see cref="ShadowObjectRegistry"/> and a real <see cref="ArchRenderScene"/>
/// — rather than a hand-fed bucket dictionary, since the resolution itself IS
/// the thing under test now.
/// </summary>
public sealed class WalkProductionWorldDataTests
{
[Fact]
public void BuildingShellBucketCellId_PortalLessBuildingUsesLandscapePositionCell()
{
const uint positionCellId = 0xF4180011u;
RenderProjectionRecord record = Record(
id: 0xCF418060u,
position: new Vector3(53.57f, 13.874f, 160f)) with
{
Source = new RenderSourceMetadata() with
{
LocalEntityId = 0xCF418060u,
SourceId = 0x01001FD3u,
EffectCellId = positionCellId,
BuildingShellAnchorCellId = 0,
},
EntityPayload = new RenderEntityPayload() with
{
IsBuildingShell = true,
},
};
var building = new WalkBuilding
{
PositionCellId = positionCellId,
Portals = [],
};
Assert.Equal(
positionCellId,
WalkProductionWorldData.BuildingShellBucketCellId(in record));
Assert.Equal(
positionCellId,
WalkProductionWorldData.BuildingShellBucketCellId(building));
}
[Fact]
public void BuildingShellBucketCellId_PortalBearingBuildingKeepsInteriorAnchor()
{
const uint anchorCellId = 0xF4180112u;
RenderProjectionRecord record = Record(
id: 0xCF41805Fu,
position: new Vector3(36f, 13.8349f, 160f)) with
{
Source = new RenderSourceMetadata() with
{
LocalEntityId = 0xCF41805Fu,
SourceId = 0x01001FB7u,
EffectCellId = 0xF4180009u,
BuildingShellAnchorCellId = anchorCellId,
},
EntityPayload = new RenderEntityPayload() with
{
IsBuildingShell = true,
},
};
var building = new WalkBuilding
{
PositionCellId = 0xF4180009u,
Portals =
[
new WalkBldPortal { OtherCellId = anchorCellId },
],
};
Assert.Equal(
anchorCellId,
WalkProductionWorldData.BuildingShellBucketCellId(in record));
Assert.Equal(
anchorCellId,
WalkProductionWorldData.BuildingShellBucketCellId(building));
}
private static ShadowShape Bsp(uint gfxObjId, float radius = 1f) =>
ShadowShape.Bsp(
gfxObjId,
Vector3.Zero,
Quaternion.Identity,
scale: 1f,
localGeometry: ShadowPartGeometry.Create(
new FlatCollisionSphere(Vector3.Zero, radius), null));
private static RenderProjectionRecord IndoorStaticRecord(
uint entityId, uint sourceId, uint parentCellId) =>
new RenderProjectionRecord() with
{
Id = RenderProjectionId.FromRaw(entityId),
ProjectionClass = RenderProjectionClass.IndoorCellStatic,
OwnerIncarnation = RenderOwnerIncarnation.FromRaw(1),
Transform = new RenderTransform(Matrix4x4.Identity),
PreviousTransform = new PreviousRenderTransform(Matrix4x4.Identity),
Residency = new RenderSpatialResidency(
RenderSpatialBucket.FromRaw(parentCellId), 0x8A020000u, parentCellId),
Flags = RenderProjectionFlags.Draw,
Source = new RenderSourceMetadata() with
{
LocalEntityId = entityId,
SourceId = sourceId,
ParentCellId = parentCellId,
},
// Every production entity record carries its MeshRefs
// (RenderProjectionRecordFactory); only an EnvCell SHELL has no
// payload, and ArchRenderScene keeps shells out of the
// LocalEntityId index on exactly that distinction.
EntityPayload = new RenderEntityPayload() with
{
MeshRefs = [new MeshRef(sourceId, Matrix4x4.Identity)],
},
};
private static RenderProjectionRecord DynamicRecord(
uint entityId, uint sourceId, uint parentCellId) =>
IndoorStaticRecord(entityId, sourceId, parentCellId) with
{
Id = RenderProjectionId.FromRaw(0x0100_0000_0000_0000u | entityId),
ProjectionClass = RenderProjectionClass.LiveDynamicRoot,
};
private static RenderProjectionRecord OutdoorStaticRecord(
uint entityId, uint sourceId, uint cellId, bool isBuildingShell = false) =>
new RenderProjectionRecord() with
{
Id = RenderProjectionId.FromRaw(0x0200_0000_0000_0000u | entityId),
ProjectionClass = RenderProjectionClass.OutdoorStatic,
OwnerIncarnation = RenderOwnerIncarnation.FromRaw(1),
Transform = new RenderTransform(Matrix4x4.Identity),
PreviousTransform = new PreviousRenderTransform(Matrix4x4.Identity),
Residency = new RenderSpatialResidency(
RenderSpatialBucket.FromRaw(cellId), cellId & 0xFFFF0000u, cellId),
Flags = RenderProjectionFlags.Draw,
Source = new RenderSourceMetadata() with
{
LocalEntityId = entityId,
SourceId = sourceId,
ParentCellId = cellId,
},
EntityPayload = new RenderEntityPayload() with
{
IsBuildingShell = isBuildingShell,
},
};
private static void Register(
ShadowObjectRegistry shadows,
uint entityId,
uint seedCellId,
uint landblockId)
{
IReadOnlyList<ShadowShape> parts = new[] { Bsp(0x01001234u) };
shadows.RegisterMultiPart(
entityId,
Vector3.Zero,
Quaternion.Identity,
parts,
state: 0u,
flags: EntityCollisionFlags.None,
worldOffsetX: 0f,
worldOffsetY: 0f,
landblockId: landblockId,
seedCellId: seedCellId,
isStatic: true,
partArray: parts);
}
[Fact]
public void GetCellStatics_RegisteredEntityIsResolvedByLocalEntityIdAtTheRetailCell()
{
const uint entityId = 0x48A02001u;
const uint retailCellId = 0x8A02015Fu;
// A DECOY parent cell, deliberately different from the registered
// retail array's cell, so the assertions below can only pass if the
// per-cell view actually consulted the registry rather than the
// authored parent (deleted alongside the sweep this test used to
// exercise).
const uint decoyParentCellId = 0x8A0201C1u;
var shadows = new ShadowObjectRegistry();
Register(shadows, entityId, retailCellId, landblockId: 0x8A020000u);
Assert.True(shadows.TryGetRetailCellArray(entityId, out IReadOnlyList<uint> retailCells));
Assert.Equal(new[] { retailCellId }, retailCells);
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(1);
using var scene = new ArchRenderScene(generation);
RenderProjectionRecord projection =
IndoorStaticRecord(entityId, sourceId: 0x02000001u, decoyParentCellId);
scene.Apply([RenderProjectionDelta.Register(generation, 1, projection)]);
var worldData = new WalkProductionWorldData(new WalkBuildingRegistry(), shadows);
worldData.BeginFrame(
scene.OpenQuery(), 0x8A020000u, renderCenterLbX: 0x8A, renderCenterLbY: 0x02);
Assert.Contains(
worldData.GetCellStatics(retailCellId).Records,
record => record.Id == projection.Id);
Assert.DoesNotContain(
worldData.GetCellStatics(decoyParentCellId).Records,
record => record.Id == projection.Id);
Assert.Equal(0, worldData.UnregisteredRenderMembershipCount);
}
[Fact]
public void GetCellStatics_RegistryAheadOfSceneContributesToNoCellAndCountsFallback()
{
const uint entityId = 0x48A02002u;
const uint retailCellId = 0x8A02015Fu;
// The registry HAS flooded this entity into its retail CELLARRAY —
// exactly the streaming-window race chunk 5 keeps as the ONE
// remaining fallback: the physics publisher (registry) ran before
// the presentation journal applied this frame's projected record.
var shadows = new ShadowObjectRegistry();
Register(shadows, entityId, retailCellId, landblockId: 0x8A020000u);
Assert.True(shadows.TryGetRetailCellArray(entityId, out _));
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(1);
using var scene = new ArchRenderScene(generation);
// Deliberately no scene.Apply — the projected record does not exist
// yet this frame.
var worldData = new WalkProductionWorldData(new WalkBuildingRegistry(), shadows);
worldData.BeginFrame(
scene.OpenQuery(), 0x8A020000u, renderCenterLbX: 0x8A, renderCenterLbY: 0x02);
Assert.Equal(0, worldData.GetCellStatics(retailCellId).Records.Count);
Assert.Equal(1, worldData.UnregisteredRenderMembershipCount);
}
[Fact]
public void GetCellStatics_UnregisteredEntityContributesToNoCellWithoutCounting()
{
// The OPPOSITE race: the scene has a projected record, but the
// registry never registered a retail CELLARRAY for this entity at
// all. Under the borrowed-view model this entity is never visited
// (nothing in the registry names it), so it correctly appears in NO
// cell and does not inflate the one remaining fallback counter —
// that counter only tracks entities the REGISTRY has flooded.
const uint entityId = 0x48A02003u;
const uint parentCellId = 0x8A02015Fu;
var shadows = new ShadowObjectRegistry();
Assert.False(shadows.TryGetRetailCellArray(entityId, out _));
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(1);
using var scene = new ArchRenderScene(generation);
RenderProjectionRecord projection =
IndoorStaticRecord(entityId, sourceId: 0x02000003u, parentCellId);
scene.Apply([RenderProjectionDelta.Register(generation, 1, projection)]);
var worldData = new WalkProductionWorldData(new WalkBuildingRegistry(), shadows);
worldData.BeginFrame(
scene.OpenQuery(), 0x8A020000u, renderCenterLbX: 0x8A, renderCenterLbY: 0x02);
Assert.Equal(0, worldData.GetCellStatics(parentCellId).Records.Count);
Assert.Equal(0, worldData.UnregisteredRenderMembershipCount);
}
[Fact]
public void UnregisteredRenderMembershipCount_CountsDistinctEntitiesNotCellVisits()
{
// One entity crossing SEVERAL cells — outdoor, since the bbox flood's
// fixed outdoor expansion reliably crosses cells even against a bare
// registry with no portal DAT data to cross INDOOR cells with (see
// GetOutdoorStatics_UsesTheSameRegistryDrivenViewAsIndoor) — all
// unresolved in the scene, must count once total, not once per cell
// the registry flooded it into.
const uint entityId = 0x87640002u;
const uint seedCellId = 0x87640030u;
IReadOnlyList<ShadowShape> parts = new[] { Bsp(0x01001234u) };
var shadows = new ShadowObjectRegistry();
shadows.RegisterMultiPart(
entityId,
Vector3.Zero,
Quaternion.Identity,
parts,
state: 0u,
flags: EntityCollisionFlags.None,
worldOffsetX: 0f,
worldOffsetY: 0f,
landblockId: 0x87640000u,
seedCellId: seedCellId,
isStatic: true,
partArray: parts);
Assert.True(shadows.TryGetRetailCellArray(entityId, out IReadOnlyList<uint> cells));
Assert.True(cells.Count >= 2, "fixture must actually cross more than one cell");
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(1);
using var scene = new ArchRenderScene(generation);
var worldData = new WalkProductionWorldData(new WalkBuildingRegistry(), shadows);
worldData.BeginFrame(
scene.OpenQuery(), 0x87640000u, renderCenterLbX: 0x87, renderCenterLbY: 0x64);
foreach (uint cellId in cells)
Assert.Equal(0, worldData.GetOutdoorStatics(cellId).Records.Count);
Assert.Equal(1, worldData.UnregisteredRenderMembershipCount);
}
[Fact]
public void GetCellDynamics_OnlyReturnsDynamicClassRecordsFromTheSameCell()
{
const uint staticEntityId = 0x48A02010u;
const uint dynamicEntityId = 0x48A02011u;
const uint sharedCellId = 0x8A02015Fu;
var shadows = new ShadowObjectRegistry();
Register(shadows, staticEntityId, sharedCellId, landblockId: 0x8A020000u);
Register(shadows, dynamicEntityId, sharedCellId, landblockId: 0x8A020000u);
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(1);
using var scene = new ArchRenderScene(generation);
RenderProjectionRecord staticProjection =
IndoorStaticRecord(staticEntityId, sourceId: 0x02000010u, sharedCellId);
RenderProjectionRecord dynamicProjection =
DynamicRecord(dynamicEntityId, sourceId: 0x02000011u, sharedCellId);
scene.Apply([
RenderProjectionDelta.Register(generation, 1, staticProjection),
RenderProjectionDelta.Register(generation, 2, dynamicProjection),
]);
var worldData = new WalkProductionWorldData(new WalkBuildingRegistry(), shadows);
worldData.BeginFrame(
scene.OpenQuery(), 0x8A020000u, renderCenterLbX: 0x8A, renderCenterLbY: 0x02);
Assert.Contains(
worldData.GetCellStatics(sharedCellId).Records,
record => record.Id == staticProjection.Id);
Assert.DoesNotContain(
worldData.GetCellStatics(sharedCellId).Records,
record => record.Id == dynamicProjection.Id);
Assert.Contains(
worldData.GetCellDynamics(sharedCellId).Records,
record => record.Id == dynamicProjection.Id);
Assert.DoesNotContain(
worldData.GetCellDynamics(sharedCellId).Records,
record => record.Id == staticProjection.Id);
}
[Fact]
public void GetCellStatics_ExcludesBuildingShellRecordsFromTheSameCell()
{
const uint shellEntityId = 0x48A02020u;
const uint ordinaryEntityId = 0x48A02021u;
const uint sharedCellId = 0x8A02015Fu;
var shadows = new ShadowObjectRegistry();
Register(shadows, shellEntityId, sharedCellId, landblockId: 0x8A020000u);
Register(shadows, ordinaryEntityId, sharedCellId, landblockId: 0x8A020000u);
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(1);
using var scene = new ArchRenderScene(generation);
RenderProjectionRecord shellProjection =
IndoorStaticRecord(shellEntityId, sourceId: 0x02000020u, sharedCellId) with
{
EntityPayload = new RenderEntityPayload() with { IsBuildingShell = true },
};
RenderProjectionRecord ordinaryProjection =
IndoorStaticRecord(ordinaryEntityId, sourceId: 0x02000021u, sharedCellId);
scene.Apply([
RenderProjectionDelta.Register(generation, 1, shellProjection),
RenderProjectionDelta.Register(generation, 2, ordinaryProjection),
]);
var worldData = new WalkProductionWorldData(new WalkBuildingRegistry(), shadows);
worldData.BeginFrame(
scene.OpenQuery(), 0x8A020000u, renderCenterLbX: 0x8A, renderCenterLbY: 0x02);
Assert.DoesNotContain(
worldData.GetCellStatics(sharedCellId).Records,
record => record.Id == shellProjection.Id);
Assert.Contains(
worldData.GetCellStatics(sharedCellId).Records,
record => record.Id == ordinaryProjection.Id);
}
[Fact]
public void GetOutdoorStatics_UsesTheSameRegistryDrivenViewAsIndoor()
{
const uint entityId = 0x87640001u;
const uint outdoorCellId = 0x87640030u;
var shadows = new ShadowObjectRegistry();
Register(shadows, entityId, outdoorCellId, landblockId: 0x87640000u);
Assert.True(shadows.TryGetRetailCellArray(entityId, out IReadOnlyList<uint> retailCells));
// The outdoor bbox flood's fixed expansion crosses more than the
// seed cell even for a small radius; only the seed cell's own
// membership matters for this test.
Assert.Contains(outdoorCellId, retailCells);
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(1);
using var scene = new ArchRenderScene(generation);
RenderProjectionRecord projection =
OutdoorStaticRecord(entityId, sourceId: 0x02000030u, outdoorCellId);
scene.Apply([RenderProjectionDelta.Register(generation, 1, projection)]);
var worldData = new WalkProductionWorldData(new WalkBuildingRegistry(), shadows);
worldData.BeginFrame(
scene.OpenQuery(), 0x87640000u, renderCenterLbX: 0x87, renderCenterLbY: 0x64);
Assert.Contains(
worldData.GetOutdoorStatics(outdoorCellId).Records,
record => record.Id == projection.Id);
}
[Fact]
public void GetCellStatics_CachesTheResultForTheRestOfTheFrame()
{
const uint entityId = 0x48A02030u;
const uint retailCellId = 0x8A02015Fu;
var shadows = new ShadowObjectRegistry();
Register(shadows, entityId, retailCellId, landblockId: 0x8A020000u);
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(1);
using var scene = new ArchRenderScene(generation);
RenderProjectionRecord projection =
IndoorStaticRecord(entityId, sourceId: 0x02000030u, retailCellId);
scene.Apply([RenderProjectionDelta.Register(generation, 1, projection)]);
var worldData = new WalkProductionWorldData(new WalkBuildingRegistry(), shadows);
worldData.BeginFrame(
scene.OpenQuery(), 0x8A020000u, renderCenterLbX: 0x8A, renderCenterLbY: 0x02);
WalkFrameStaticRecords first = worldData.GetCellStatics(retailCellId);
WalkFrameStaticRecords second = worldData.GetCellStatics(retailCellId);
Assert.Equal(first.Records.Array, second.Records.Array);
Assert.Equal(first.Records.Offset, second.Records.Offset);
Assert.Equal(first.Records.Count, second.Records.Count);
}
[Fact]
public void StaticBucketContains_FindsARegisteredSourceIdInItsRetailCell()
{
const uint entityId = 0x48A02040u;
const uint sourceId = 0x020009A2u;
const uint retailCellId = 0xF4180112u;
var shadows = new ShadowObjectRegistry();
Register(shadows, entityId, retailCellId, landblockId: 0xF4180000u);
RenderSceneGeneration generation = RenderSceneGeneration.FromRaw(1);
using var scene = new ArchRenderScene(generation);
RenderProjectionRecord projection =
IndoorStaticRecord(entityId, sourceId, retailCellId);
scene.Apply([RenderProjectionDelta.Register(generation, 1, projection)]);
var worldData = new WalkProductionWorldData(new WalkBuildingRegistry(), shadows);
worldData.BeginFrame(
scene.OpenQuery(), 0xF4180000u, renderCenterLbX: 0xF4, renderCenterLbY: 0x18);
Assert.True(worldData.StaticBucketContains(retailCellId, sourceId));
Assert.False(worldData.StaticBucketContains(retailCellId, sourceId + 1));
Assert.False(worldData.StaticBucketContains(0xF4180107u, sourceId));
}
private static RenderProjectionRecord Record(uint id, Vector3 position) =>
new RenderProjectionRecord() with
{
Id = RenderProjectionId.FromRaw(id),
Transform = new RenderTransform(Matrix4x4.CreateTranslation(position)),
Source = new RenderSourceMetadata() with { LocalEntityId = id },
};
}