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>
This commit is contained in:
parent
f30039d3d9
commit
c94a1a407e
20 changed files with 2141 additions and 1026 deletions
|
|
@ -520,13 +520,6 @@ public sealed class ArchRenderSceneTests
|
|||
LightCandidate: 1,
|
||||
Dirty: 6),
|
||||
query.IndexCounts);
|
||||
Assert.Equal(1, query.GetCellStaticCount(indoorCell));
|
||||
Assert.Equal(1, query.GetCellDynamicCount(indoorCell));
|
||||
var cellRecords = new RenderProjectionRecord[1];
|
||||
Assert.Equal(1, query.CopyCellStaticsTo(indoorCell, cellRecords));
|
||||
Assert.Equal(indoorStatic.Id, cellRecords[0].Id);
|
||||
Assert.Equal(1, query.CopyCellDynamicsTo(indoorCell, cellRecords));
|
||||
Assert.Equal(cellDynamic.Id, cellRecords[0].Id);
|
||||
Assert.True(scene.Memory.EstimatedIndexBytes > 0);
|
||||
}
|
||||
|
||||
|
|
@ -580,7 +573,6 @@ public sealed class ArchRenderSceneTests
|
|||
Assert.Equal(1, updated.IndexCounts.Translucent);
|
||||
Assert.Equal(1, updated.IndexCounts.LightCandidate);
|
||||
Assert.Equal(1, updated.IndexCounts.Dirty);
|
||||
Assert.Equal(1, updated.GetCellDynamicCount(indoorCell));
|
||||
|
||||
RenderProjectionRecord replacement = rebucketed with
|
||||
{
|
||||
|
|
@ -592,7 +584,6 @@ public sealed class ArchRenderSceneTests
|
|||
RenderSceneQuery replaced = scene.OpenQuery();
|
||||
Assert.Equal(0, replaced.IndexCounts.Dynamic);
|
||||
Assert.Equal(1, replaced.IndexCounts.IndoorCellStatic);
|
||||
Assert.Equal(1, replaced.GetCellStaticCount(indoorCell));
|
||||
|
||||
scene.Apply(
|
||||
[
|
||||
|
|
@ -1085,6 +1076,92 @@ public sealed class ArchRenderSceneTests
|
|||
Assert.Equal(changed.LatestRevision, query.DirectionalShadowTransformRevision);
|
||||
}
|
||||
|
||||
// Campaign OVERHAUL S2 review fix round (F5/F6): the LocalEntityId index.
|
||||
|
||||
private static RenderProjectionRecord WithLocalEntityId(
|
||||
RenderProjectionRecord record,
|
||||
uint localEntityId) =>
|
||||
record with
|
||||
{
|
||||
Source = new RenderSourceMetadata(
|
||||
LocalEntityId: localEntityId,
|
||||
ServerGuid: 0,
|
||||
SourceId: 1,
|
||||
ParentCellId: record.Source.ParentCellId,
|
||||
EffectCellId: 0,
|
||||
BuildingShellAnchorCellId: 0,
|
||||
TransformFingerprint: new RenderSceneHash128(1, 1),
|
||||
GeometryFingerprint: new RenderSceneHash128(2, 2),
|
||||
AppearanceFingerprint: new RenderSceneHash128(3, 3)),
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void TryGetByLocalEntityId_IgnoresEnvCellShellRecords()
|
||||
{
|
||||
// An EnvCell shell carries its CELL id in the entity-id slot; a
|
||||
// dungeon cell id such as 0x8A020100 is numerically a procedural
|
||||
// scenery entity id. The shell must never shadow the real entity.
|
||||
const uint aliasedId = 0x8A020100u;
|
||||
RenderSceneGeneration generation = Generation(31);
|
||||
using var scene = new ArchRenderScene(generation);
|
||||
RenderProjectionRecord shell = WithLocalEntityId(
|
||||
Record(310, 1, RenderProjectionClass.IndoorCellStatic), aliasedId);
|
||||
RenderProjectionRecord scenery = WithLocalEntityId(
|
||||
Record(311, 1, RenderProjectionClass.OutdoorStatic), aliasedId);
|
||||
|
||||
scene.Apply(
|
||||
[
|
||||
RenderProjectionDelta.Register(generation, 1, shell),
|
||||
RenderProjectionDelta.Register(generation, 2, scenery),
|
||||
]);
|
||||
RenderSceneQuery query = scene.OpenQuery();
|
||||
|
||||
Assert.True(query.TryGetByLocalEntityId(aliasedId, out RenderProjectionRecord found));
|
||||
Assert.Equal(scenery.Id, found.Id);
|
||||
|
||||
// Unregistering the shell never disturbs the entity's mapping.
|
||||
scene.Apply(
|
||||
[
|
||||
RenderProjectionDelta.Unregister(
|
||||
generation,
|
||||
3,
|
||||
shell.Id,
|
||||
shell.OwnerIncarnation),
|
||||
]);
|
||||
query = scene.OpenQuery();
|
||||
Assert.True(query.TryGetByLocalEntityId(aliasedId, out found));
|
||||
Assert.Equal(scenery.Id, found.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_ThatRebindsTheLocalEntityId_MovesTheIndex()
|
||||
{
|
||||
RenderSceneGeneration generation = Generation(32);
|
||||
using var scene = new ArchRenderScene(generation);
|
||||
RenderProjectionRecord original = WithLocalEntityId(
|
||||
Record(320, 1, RenderProjectionClass.LiveDynamicRoot), 19);
|
||||
scene.Apply([RenderProjectionDelta.Register(generation, 1, original)]);
|
||||
Assert.True(scene.OpenQuery().TryGetByLocalEntityId(19, out _));
|
||||
|
||||
// Same class, cell, flags, mesh count and sort key — only the id
|
||||
// changes; the index must follow it (IndexMembershipEquals compares
|
||||
// the id since the S2 review fix).
|
||||
RenderProjectionRecord rebound = WithLocalEntityId(original, 20);
|
||||
scene.Apply(
|
||||
[
|
||||
RenderProjectionDelta.Update(
|
||||
RenderProjectionDeltaKind.UpdateFlags,
|
||||
generation,
|
||||
2,
|
||||
rebound),
|
||||
]);
|
||||
RenderSceneQuery query = scene.OpenQuery();
|
||||
|
||||
Assert.False(query.TryGetByLocalEntityId(19, out _));
|
||||
Assert.True(query.TryGetByLocalEntityId(20, out RenderProjectionRecord found));
|
||||
Assert.Equal(original.Id, found.Id);
|
||||
}
|
||||
|
||||
private static RenderProjectionRecord Record(
|
||||
ulong id,
|
||||
ulong incarnation,
|
||||
|
|
|
|||
|
|
@ -103,7 +103,6 @@ public sealed class DirectionalShadowCasterFrameTests
|
|||
Assert.Equal(
|
||||
[RenderSceneIndex.OutdoorStatic, RenderSceneIndex.OutdoorDynamic],
|
||||
source.CopiedIndices);
|
||||
Assert.Equal(0, source.CellQueries);
|
||||
Assert.Equal(2, frame.Stats.IndexCopies);
|
||||
}
|
||||
|
||||
|
|
@ -635,7 +634,6 @@ public sealed class DirectionalShadowCasterFrameTests
|
|||
|
||||
public int IndexCountReads { get; private set; }
|
||||
public int IndexCopies { get; private set; }
|
||||
public int CellQueries { get; private set; }
|
||||
public int ProjectionReads { get; private set; }
|
||||
public int BatchedProjectionCopies { get; private set; }
|
||||
public ulong TopologyRevision { get; private set; } = 1;
|
||||
|
|
@ -731,6 +729,32 @@ public sealed class DirectionalShadowCasterFrameTests
|
|||
return false;
|
||||
}
|
||||
|
||||
public bool TryGetByLocalEntityId(
|
||||
RenderSceneGeneration generation,
|
||||
uint localEntityId,
|
||||
out RenderProjectionRecord record)
|
||||
{
|
||||
for (int index = 0; index < _statics.Length; index++)
|
||||
{
|
||||
if (_statics[index].Source.LocalEntityId == localEntityId)
|
||||
{
|
||||
record = _statics[index];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (int index = 0; index < _dynamics.Length; index++)
|
||||
{
|
||||
if (_dynamics[index].Source.LocalEntityId == localEntityId)
|
||||
{
|
||||
record = _dynamics[index];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
record = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
public int CopyById(
|
||||
RenderSceneGeneration generation,
|
||||
ReadOnlySpan<RenderProjectionId> ids,
|
||||
|
|
@ -791,25 +815,6 @@ public sealed class DirectionalShadowCasterFrameTests
|
|||
return values.Length;
|
||||
}
|
||||
|
||||
public int GetCellCount(
|
||||
RenderSceneGeneration generation,
|
||||
uint fullCellId,
|
||||
bool dynamic)
|
||||
{
|
||||
CellQueries++;
|
||||
throw new InvalidOperationException("Directional shadows do not query PView cells.");
|
||||
}
|
||||
|
||||
public int CopyCellTo(
|
||||
RenderSceneGeneration generation,
|
||||
uint fullCellId,
|
||||
bool dynamic,
|
||||
Span<RenderProjectionRecord> destination)
|
||||
{
|
||||
CellQueries++;
|
||||
throw new InvalidOperationException("Directional shadows do not query PView cells.");
|
||||
}
|
||||
|
||||
|
||||
public void ReplaceStatics(
|
||||
RenderProjectionRecord[] values,
|
||||
|
|
|
|||
|
|
@ -316,7 +316,7 @@ public class Issue177StairDescentCameraFloodTests
|
|||
Assert.Contains(
|
||||
worldData.GetCellStatics(FacilityHub | 0x015Eu).Records,
|
||||
record => record.Id == projectionId);
|
||||
Assert.Equal(0, worldData.UnregisteredStaticRenderFallbackCount);
|
||||
Assert.Equal(0, worldData.UnregisteredRenderMembershipCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -3,9 +3,23 @@ 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]
|
||||
|
|
@ -79,288 +93,6 @@ public sealed class WalkProductionWorldDataTests
|
|||
WalkProductionWorldData.BuildingShellBucketCellId(building));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BucketOutdoorRecord_UsesEveryOutdoorPhysicsShadowCellAcrossLandblockEdge()
|
||||
{
|
||||
RenderProjectionRecord record = Record(
|
||||
id: 0x1234u,
|
||||
position: new Vector3(191f, 191f, 0f));
|
||||
var buckets = new Dictionary<uint, List<RenderProjectionRecord>>();
|
||||
|
||||
WalkProductionWorldData.BucketOutdoorRecord(
|
||||
in record,
|
||||
[0xF07F0040u, 0xF0800001u, 0xF4180101u],
|
||||
buckets,
|
||||
renderCenterLbX: 0xF0,
|
||||
renderCenterLbY: 0x7F);
|
||||
|
||||
Assert.Equal([0xF07F0040u, 0xF0800001u], buckets.Keys.Order());
|
||||
Assert.All(buckets.Values, bucket => Assert.Equal(record, Assert.Single(bucket)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BucketOutdoorRecord_UnregisteredEffectFallsBackToRootPositionCell()
|
||||
{
|
||||
RenderProjectionRecord record = Record(
|
||||
id: 0x5678u,
|
||||
position: new Vector3(193f, 25f, 0f));
|
||||
var buckets = new Dictionary<uint, List<RenderProjectionRecord>>();
|
||||
|
||||
WalkProductionWorldData.BucketOutdoorRecord(
|
||||
in record,
|
||||
Array.Empty<uint>(),
|
||||
buckets,
|
||||
renderCenterLbX: 0xEF,
|
||||
renderCenterLbY: 0x7F);
|
||||
|
||||
Assert.Equal([0xF07F0002u], buckets.Keys);
|
||||
Assert.Equal(record, Assert.Single(buckets[0xF07F0002u]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BucketIndoorRecord_InstallsCrossCellPartInBothInteriorCells()
|
||||
{
|
||||
RenderProjectionRecord record = Record(
|
||||
id: 0x48A02035u,
|
||||
position: new Vector3(55.25f, -46.47f, -3f)) with
|
||||
{
|
||||
Source = new RenderSourceMetadata() with
|
||||
{
|
||||
LocalEntityId = 0x48A02035u,
|
||||
ParentCellId = 0x8A02015Fu,
|
||||
},
|
||||
};
|
||||
var indoor = new Dictionary<uint, List<RenderProjectionRecord>>();
|
||||
var outdoor = new Dictionary<uint, List<RenderProjectionRecord>>();
|
||||
|
||||
WalkProductionWorldData.BucketIndoorRecord(
|
||||
in record,
|
||||
[0x8A02015Fu, 0x8A02015Eu],
|
||||
indoor,
|
||||
outdoor);
|
||||
|
||||
Assert.Equal([0x8A02015Eu, 0x8A02015Fu], indoor.Keys.Order());
|
||||
Assert.All(indoor.Values, bucket => Assert.Equal(record, Assert.Single(bucket)));
|
||||
Assert.Empty(outdoor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BucketIndoorRecord_CanCrossAnExitIntoLandscapeCell()
|
||||
{
|
||||
RenderProjectionRecord record = Record(
|
||||
id: 0x48A02035u,
|
||||
position: Vector3.Zero) with
|
||||
{
|
||||
Source = new RenderSourceMetadata() with
|
||||
{
|
||||
LocalEntityId = 0x48A02035u,
|
||||
ParentCellId = 0x8A02015Fu,
|
||||
},
|
||||
};
|
||||
var indoor = new Dictionary<uint, List<RenderProjectionRecord>>();
|
||||
var outdoor = new Dictionary<uint, List<RenderProjectionRecord>>();
|
||||
|
||||
WalkProductionWorldData.BucketIndoorRecord(
|
||||
in record,
|
||||
[0x8A02015Fu, 0x8A020021u],
|
||||
indoor,
|
||||
outdoor);
|
||||
|
||||
Assert.Equal(record, Assert.Single(indoor[0x8A02015Fu]));
|
||||
Assert.Equal(record, Assert.Single(outdoor[0x8A020021u]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BucketDynamicRecord_InstallsMultipartPlayerInEveryCrossedInteriorCell()
|
||||
{
|
||||
RenderProjectionRecord record = Record(
|
||||
id: 0x000F4243u,
|
||||
position: new Vector3(58.81f, -49.42f, -0.85f)) with
|
||||
{
|
||||
Source = new RenderSourceMetadata() with
|
||||
{
|
||||
LocalEntityId = 0x000F4243u,
|
||||
ParentCellId = 0x8A02015Eu,
|
||||
},
|
||||
};
|
||||
var indoor = new Dictionary<uint, List<RenderProjectionRecord>>();
|
||||
var outdoor = new Dictionary<uint, List<RenderProjectionRecord>>();
|
||||
|
||||
WalkProductionWorldData.BucketDynamicRecord(
|
||||
in record,
|
||||
[0x8A02015Eu, 0x8A02015Fu, 0x8A0201C1u],
|
||||
indoor,
|
||||
outdoor,
|
||||
renderCenterLbX: 0x8A,
|
||||
renderCenterLbY: 0x02);
|
||||
|
||||
Assert.Equal(
|
||||
[0x8A02015Eu, 0x8A02015Fu, 0x8A0201C1u],
|
||||
indoor.Keys.Order());
|
||||
Assert.All(
|
||||
indoor.Values,
|
||||
bucket => Assert.Equal(record, Assert.Single(bucket)));
|
||||
Assert.Empty(outdoor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BucketDynamicRecord_UnregisteredInteriorEffectFallsBackToParentCell()
|
||||
{
|
||||
RenderProjectionRecord record = Record(
|
||||
id: 0x00001234u,
|
||||
position: Vector3.Zero) with
|
||||
{
|
||||
Source = new RenderSourceMetadata() with
|
||||
{
|
||||
LocalEntityId = 0x00001234u,
|
||||
ParentCellId = 0x8A02015Fu,
|
||||
},
|
||||
};
|
||||
var indoor = new Dictionary<uint, List<RenderProjectionRecord>>();
|
||||
var outdoor = new Dictionary<uint, List<RenderProjectionRecord>>();
|
||||
|
||||
WalkProductionWorldData.BucketDynamicRecord(
|
||||
in record,
|
||||
Array.Empty<uint>(),
|
||||
indoor,
|
||||
outdoor,
|
||||
renderCenterLbX: 0x8A,
|
||||
renderCenterLbY: 0x02);
|
||||
|
||||
Assert.Equal(record, Assert.Single(indoor[0x8A02015Fu]));
|
||||
Assert.Empty(outdoor);
|
||||
}
|
||||
|
||||
// Campaign OVERHAUL S2 chunk 4: ResolveDynamicRenderCells is now a direct
|
||||
// TryGetRetailCellArray read for EVERY dynamic record, children included
|
||||
// — ShadowObjectRegistry.AttachChild owns Contract B's child-inheritance
|
||||
// recursion at the registry, so the render-side parent-chain walk this
|
||||
// test used to exercise no longer exists.
|
||||
|
||||
private static ShadowShape ChildBsp(uint gfxObjId) =>
|
||||
ShadowShape.Bsp(
|
||||
gfxObjId,
|
||||
Vector3.Zero,
|
||||
Quaternion.Identity,
|
||||
scale: 1f,
|
||||
localGeometry: ShadowPartGeometry.Create(
|
||||
new FlatCollisionSphere(Vector3.Zero, 1f), null));
|
||||
|
||||
[Fact]
|
||||
public void ResolveDynamicRenderCells_AttachedChildReadsTheRegistrysRetailCellArrayDirectly()
|
||||
{
|
||||
var shadows = new ShadowObjectRegistry();
|
||||
const uint rootId = 0x5000000Au;
|
||||
const uint childId = 0x800045EEu;
|
||||
IReadOnlyList<ShadowShape> rootParts = new[] { ChildBsp(0x02000001u) };
|
||||
|
||||
shadows.RegisterMultiPart(
|
||||
rootId,
|
||||
new Vector3(12f, 12f, 50f),
|
||||
Quaternion.Identity,
|
||||
rootParts,
|
||||
state: 0u,
|
||||
flags: EntityCollisionFlags.None,
|
||||
worldOffsetX: 0f,
|
||||
worldOffsetY: 0f,
|
||||
landblockId: 0xF4180000u,
|
||||
seedCellId: 0xF4180104u,
|
||||
isStatic: false,
|
||||
partArray: rootParts);
|
||||
Assert.True(shadows.TryGetRetailCellArray(rootId, out IReadOnlyList<uint> rootCells));
|
||||
Assert.NotEmpty(rootCells); // the fixture must actually exercise a flood
|
||||
|
||||
// No render-side parent walk any more: Contract B's inheritance is
|
||||
// owned by the registry at attach time.
|
||||
Assert.True(shadows.AttachChild(childId, rootId, new[] { ChildBsp(0x02000002u) }));
|
||||
|
||||
RenderProjectionRecord wand = Record(
|
||||
id: childId,
|
||||
position: Vector3.Zero) with
|
||||
{
|
||||
Source = new RenderSourceMetadata() with
|
||||
{
|
||||
LocalEntityId = childId,
|
||||
ParentCellId = 0xF4180104u,
|
||||
},
|
||||
EntityPayload = new RenderEntityPayload() with
|
||||
{
|
||||
CasterIdentity = RenderCasterIdentityKind.EquippedChild,
|
||||
},
|
||||
};
|
||||
|
||||
IReadOnlyList<uint> cells =
|
||||
WalkProductionWorldData.ResolveDynamicRenderCells(
|
||||
in wand,
|
||||
id => shadows.TryGetRetailCellArray(id, out IReadOnlyList<uint> c)
|
||||
? (true, c)
|
||||
: (false, Array.Empty<uint>()),
|
||||
shadows.GetOwnerCells,
|
||||
out bool usedFallback);
|
||||
|
||||
Assert.False(usedFallback);
|
||||
Assert.Equal(rootCells, cells);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveDynamicRenderCells_UnregisteredEntityFallsBackToOwnerCells()
|
||||
{
|
||||
var shadows = new ShadowObjectRegistry();
|
||||
const uint entityId = 0x00099Fu;
|
||||
IReadOnlyList<ShadowShape> shapes = new[] { ChildBsp(0x02000003u) };
|
||||
shadows.RegisterMultiPart(
|
||||
entityId,
|
||||
new Vector3(12f, 12f, 50f),
|
||||
Quaternion.Identity,
|
||||
shapes,
|
||||
state: 0u,
|
||||
flags: EntityCollisionFlags.None,
|
||||
worldOffsetX: 0f,
|
||||
worldOffsetY: 0f,
|
||||
landblockId: 0xF4180000u,
|
||||
seedCellId: 0xF4180104u,
|
||||
isStatic: false);
|
||||
// No partArray supplied above -> no retail cell array registered for
|
||||
// this entity (item E of the S2 chunk-1 contract), so the ordinary
|
||||
// collision-flood answer is the conservative fallback.
|
||||
Assert.False(shadows.TryGetRetailCellArray(entityId, out _));
|
||||
Assert.NotEmpty(shadows.GetOwnerCells(entityId));
|
||||
|
||||
RenderProjectionRecord record = Record(
|
||||
id: entityId,
|
||||
position: Vector3.Zero) with
|
||||
{
|
||||
Source = new RenderSourceMetadata() with
|
||||
{
|
||||
LocalEntityId = entityId,
|
||||
ParentCellId = 0xF4180104u,
|
||||
},
|
||||
};
|
||||
|
||||
IReadOnlyList<uint> cells =
|
||||
WalkProductionWorldData.ResolveDynamicRenderCells(
|
||||
in record,
|
||||
id => shadows.TryGetRetailCellArray(id, out IReadOnlyList<uint> c)
|
||||
? (true, c)
|
||||
: (false, Array.Empty<uint>()),
|
||||
shadows.GetOwnerCells,
|
||||
out bool usedFallback);
|
||||
|
||||
Assert.True(usedFallback);
|
||||
Assert.Equal(shadows.GetOwnerCells(entityId), cells);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Campaign OVERHAUL S2 chunk 2: BeginFrame's indoor static sweep now
|
||||
// borrows membership from ShadowObjectRegistry.TryGetRetailCellArray
|
||||
// instead of rebuilding it (the deleted ResolveStaticRenderCells /
|
||||
// ShadowObjectRegistry.ComputeStaticRenderCells pair). These two tests
|
||||
// drive the REAL production path end to end — a real (bare, no-DAT)
|
||||
// ShadowObjectRegistry, a real ArchRenderScene, and BeginFrame itself —
|
||||
// rather than the hardcoded-cell-array style of the Bucket* tests above,
|
||||
// which exercise only the (unchanged) bucketing primitive.
|
||||
// -----------------------------------------------------------------
|
||||
|
||||
private static ShadowShape Bsp(uint gfxObjId, float radius = 1f) =>
|
||||
ShadowShape.Bsp(
|
||||
gfxObjId,
|
||||
|
|
@ -388,20 +120,54 @@ public sealed class WalkProductionWorldDataTests
|
|||
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)],
|
||||
},
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void BeginFrame_IndoorStatic_RegisteredEntityUsesRegistryRetailCellArray()
|
||||
{
|
||||
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
|
||||
// indoor sweep actually consulted the registry rather than falling
|
||||
// back to the authored parent.
|
||||
const uint decoyParentCellId = 0x8A0201C1u;
|
||||
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,
|
||||
};
|
||||
|
||||
var shadows = new ShadowObjectRegistry();
|
||||
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,
|
||||
|
|
@ -412,10 +178,26 @@ public sealed class WalkProductionWorldDataTests
|
|||
flags: EntityCollisionFlags.None,
|
||||
worldOffsetX: 0f,
|
||||
worldOffsetY: 0f,
|
||||
landblockId: 0x8A020000u,
|
||||
seedCellId: retailCellId,
|
||||
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);
|
||||
|
||||
|
|
@ -435,34 +217,265 @@ public sealed class WalkProductionWorldDataTests
|
|||
Assert.DoesNotContain(
|
||||
worldData.GetCellStatics(decoyParentCellId).Records,
|
||||
record => record.Id == projection.Id);
|
||||
Assert.Equal(0, worldData.UnregisteredStaticRenderFallbackCount);
|
||||
Assert.Equal(0, worldData.UnregisteredRenderMembershipCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BeginFrame_IndoorStatic_UnregisteredEntityFallsBackToParentCellAndCountsFallback()
|
||||
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;
|
||||
|
||||
// Bare, empty registry: entityId is never registered, so
|
||||
// TryGetRetailCellArray must answer false for it.
|
||||
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: 0x02000002u, parentCellId);
|
||||
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(parentCellId).Records,
|
||||
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);
|
||||
Assert.Equal(1, worldData.UnregisteredStaticRenderFallbackCount);
|
||||
}
|
||||
|
||||
[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) =>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue