using System.Numerics;
using System.Runtime.InteropServices;
using System.Text;
using AcDream.App.Rendering.Scene;
using AcDream.Core.Physics;
namespace AcDream.App.Rendering.Walk;
///
/// Campaign FW3.2b-2: the production over
/// the retained scene () and the FW3.1
/// .
///
///
/// Campaign OVERHAUL S2 chunk 5: cell membership is a BORROWED VIEW over
/// 's retail per-cell part-entry product
/// ( — retail's
/// CPartArray::AddPartsShadow 0x00517e40 output, keyed by the SAME
/// CELLARRAY
/// answers). This class no longer sweeps the scene's static/dynamic indices
/// to REBUILD membership: for a queried cell id,
/// walks the registry's per-cell entries (already in retail
/// CELLARRAY-then-part-array insertion order), collapses them to their
/// distinct owning entity ids in that same order, and resolves each id back
/// to its current through
/// — a presentation-side
/// lookup by the SAME the
/// registry is keyed by (chunk 5's App-side addition to
/// ArchRenderScene). Statics vs. dynamics are the same
/// ProjectionClass split ArchRenderScene itself uses (a live
/// dynamic root or an equipped child is "dynamic"; every other class is
/// "static"), and building shells are excluded — they draw at their own
/// building's shell turn (retail CPhysicsPart::Draw(parts, 0)
/// @0x0059f331), never at the cell's ordinary DrawObjCell turn.
///
///
///
/// An entity the registry HAS flooded into this cell but whose projected
/// record the presentation journal has not applied yet this frame (the
/// transient race between LiveEntityRuntime's projection journal and
/// ShadowObjectRegistry's physics-side registration, both driven off
/// the same Create/appearance edge but landing through independent
/// incremental pipelines) contributes to NO cell — retail draws nothing for
/// an object not yet in a cell; there is no second, conservative fallback.
/// Every DISTINCT entity id this happens for in one frame is counted once in
/// , regardless of how many
/// cells its CELLARRAY touches or how many Get* calls observe it.
///
///
/// Building shells are the ONE exception left to a per-frame sweep:
/// retail's shell/portal machinery is out of S2's scope (the landcell
/// building channel), so still reads a
/// small per-frame bucket (_shellsByAnchor) filled by a single narrow
/// sweep in
/// that keeps only IsBuildingShell records —
/// it never resolves ordinary static/dynamic membership.
///
/// Campaign FW3.4a: , ,
/// , , and
/// materialize their result into
/// — a grow-only buffer reset to length 0 once per frame
/// in — rather than a fresh RenderProjectionRecord[]
/// allocation per distinct cell/anchor per frame (the single largest
/// contributor to the FW3.4 perf checkpoint's 14× frame-allocation
/// regression, 1.9 MB/frame p50, before the arena existed).
/// is the matching grow-on-demand scratch
/// buffer collects one cell's filtered records
/// into before a single call — the same
/// steady-state-zero-allocation shape already
/// has. Every segment is
/// STRICTLY per-frame scratch — nothing holds one across a frame boundary
/// (the driver/populator consume it immediately) — so reusing the same
/// backing array's memory next frame is safe. Per-cell RESULTS
/// ( etc.) are cleared and re-materialized once per
/// frame on first ask, same as before chunk 5.
///
internal sealed class WalkProductionWorldData : IWalkFrameWorldData
{
private readonly WalkBuildingRegistry _buildings;
private readonly ShadowObjectRegistry _shadows;
private RenderSceneQuery _scene;
private uint _tupleLandblockId;
private readonly Dictionary _cellCache = new();
private readonly Dictionary _cellDynamicCache = new();
private readonly Dictionary _cellObjectsCache = new();
private readonly Dictionary _facilityShadowProbeSignatures = new();
private readonly Dictionary> _shellsByAnchor = new();
private readonly Dictionary _outdoorMaterialized = new();
private readonly Dictionary _outdoorDynamicsMaterialized = new();
private readonly Dictionary _outdoorObjectsMaterialized = new();
private readonly Dictionary _shellMaterialized = new();
// Campaign OVERHAUL S2 chunk 5: every distinct entity id counted into
// UnregisteredRenderMembershipCount this frame, so the SAME entity
// touching several cells (or being asked for through both the static and
// dynamic Get* pair) is counted once — not once per cell visit. Cleared
// in BeginFrame alongside the counter itself.
private readonly HashSet _unregisteredEntitiesThisFrame = new();
// The one surviving per-frame sweep: building shells only (see this
// type's own doc comment). Statics/dynamics no longer sweep the scene at
// all — membership is a borrowed, on-demand view over the registry.
private RenderProjectionRecord[] _sweepScratch = new RenderProjectionRecord[1024];
// Campaign OVERHAUL S2 chunk 5: ResolveCellView's grow-on-demand scratch
// buffer — collects one cell's filtered records before a single
// AppendToArena call. Starts small: a cell's real membership is usually
// a handful of parts, not the hundreds an old full-scene sweep held.
private RenderProjectionRecord[] _cellViewScratch = new RenderProjectionRecord[64];
// Campaign FW3.4a: the per-frame, grow-only materialization arena — see
// this type's own doc comment.
private RenderProjectionRecord[] _arena = new RenderProjectionRecord[4096];
private int _arenaLength;
internal WalkProductionWorldData(
WalkBuildingRegistry buildings,
ShadowObjectRegistry shadows)
{
_buildings = buildings ?? throw new ArgumentNullException(nameof(buildings));
_shadows = shadows ?? throw new ArgumentNullException(nameof(shadows));
}
///
/// Campaign OVERHAUL S2 chunk 5: how many DISTINCT entities this frame
/// were present in the registry's retail CELLARRAY (so
/// named
/// them) but had no resolvable yet
/// through — the
/// transient streaming window where the physics publisher's registration
/// outran the presentation journal's applied projection for the same
/// Create/appearance edge. Retail has no such gap
/// (CEnvCell::init_static_objects installs the CELLARRAY before a
/// static is ever drawable) so a nonzero count here reflects two
/// independent incremental state machines racing during streaming, not
/// steady-state behavior; filed as AD-116, citing the same residency-race
/// reasoning AD-49 already accepts for
/// 's own outdoor
/// seed. Such an entity
/// contributes to NO cell this frame — there is no second, conservative
/// fallback (chunk 5 removed the authored-parent-cell and
/// root-position-cell fallbacks chunk 2/4 carried). Reset to zero at the
/// start of every , after that PRIOR frame's
/// value has already been reported by the diagnostic line below.
///
public int UnregisteredRenderMembershipCount { get; private set; }
/// Rebuilds the frame's building-shell bucket and clears the
/// per-cell caches. Call once per frame before the driver runs.
/// /
/// were the streaming recenter origin the deleted position-based
/// fallbacks used to convert a render-origin-relative position back to
/// its true landblock byte (LandscapeCellId). Campaign OVERHAUL
/// S2 chunk 5 deleted every position-based fallback — the registry's
/// CELLARRAY is already expressed in exact cell ids — so these two
/// parameters are accepted (unchanged call-site signature) but no
/// longer stored or read.
internal void BeginFrame(
RenderSceneQuery scene,
uint tupleLandblockId,
int renderCenterLbX,
int renderCenterLbY)
{
// Campaign OVERHAUL S2 chunk 5: report the PRIOR frame's count
// before resetting it — the count is only final once that frame's
// walk (a sequence of on-demand Get* calls this class has no other
// "frame is done" hook for) has finished, so this is necessarily a
// one-frame-delayed report, matching every other per-second/per-
// change probe in this file family (print-only; never influences
// admission).
if (UnregisteredRenderMembershipCount > 0
&& AcDream.Core.Rendering.RenderingDiagnostics.ProbeFacilityStairsEnabled)
{
Console.WriteLine(
$"[walk-membership] unregistered={UnregisteredRenderMembershipCount} "
+ $"tupleLandblock=0x{_tupleLandblockId:X8}");
}
_scene = scene;
_tupleLandblockId = tupleLandblockId;
_cellCache.Clear();
_cellDynamicCache.Clear();
_cellObjectsCache.Clear();
_outdoorMaterialized.Clear();
_outdoorDynamicsMaterialized.Clear();
_outdoorObjectsMaterialized.Clear();
_shellMaterialized.Clear();
_arenaLength = 0;
UnregisteredRenderMembershipCount = 0;
_unregisteredEntitiesThisFrame.Clear();
foreach (List bucket in _shellsByAnchor.Values)
bucket.Clear();
// Building shells are the landcell BUILDING channel (out of S2):
// retail draws a shell at its own DrawBuilding turn, never through
// the cell's ordinary shadow_part_list walk (Contract C), so this
// is the one sweep that survives chunk 5 — it exists ONLY to bucket
// IsBuildingShell records by their authored anchor, never to
// resolve ordinary static/dynamic cell membership.
int required = _scene.IndexCounts.For(RenderSceneIndex.OutdoorStatic);
if (required > _sweepScratch.Length)
{
_sweepScratch = new RenderProjectionRecord[
Math.Max(required, _sweepScratch.Length * 2)];
}
int count = _scene.CopyIndexTo(RenderSceneIndex.OutdoorStatic, _sweepScratch);
for (int i = 0; i < count; i++)
{
ref readonly RenderProjectionRecord record = ref _sweepScratch[i];
if (!record.EntityPayload.IsBuildingShell)
continue;
uint anchor = BuildingShellBucketCellId(in record);
if (!_shellsByAnchor.TryGetValue(anchor, out List? shells))
_shellsByAnchor[anchor] = shells = new List();
shells.Add(record);
}
}
/// Facility Hub / cathedral discriminator: does the given cell's
/// borrowed retail render membership contain a record with this exact
/// authored SourceId? Reimplemented directly over the registry's
/// per-cell entries (chunk 5) rather than reading a swept bucket
/// dictionary — 's
/// ProbeFacilityStairsEnabled block is the only caller.
internal bool StaticBucketContains(uint cellId, uint sourceId)
{
IReadOnlyList entries =
_shadows.GetRetailPartEntriesInCell(cellId);
uint previousEntityId = 0;
bool havePrevious = false;
for (int i = 0; i < entries.Count; i++)
{
uint entityId = entries[i].EntityId;
if (havePrevious && entityId == previousEntityId)
continue;
previousEntityId = entityId;
havePrevious = true;
if (_scene.TryGetByLocalEntityId(entityId, out RenderProjectionRecord record)
&& record.Source.SourceId == sourceId)
{
return true;
}
}
return false;
}
public WalkFrameStaticRecords GetCellStatics(uint cellId)
{
if (_cellCache.TryGetValue(cellId, out WalkFrameStaticRecords cached))
return cached;
WalkFrameStaticRecords records = ResolveCellView(cellId, dynamic: false);
EmitFacilityShadowProbe(records.Records, cellId, "static");
_cellCache[cellId] = records;
return records;
}
public WalkFrameStaticRecords GetCellObjects(uint cellId)
{
if (_cellObjectsCache.TryGetValue(cellId, out WalkFrameStaticRecords cached))
return cached;
WalkFrameStaticRecords records = ResolveCellView(cellId, dynamic: null);
EmitFacilityShadowProbe(records.Records, cellId, "combined");
_cellObjectsCache[cellId] = records;
return records;
}
public WalkFrameStaticRecords GetCellDynamics(uint cellId)
{
if (_cellDynamicCache.TryGetValue(cellId, out WalkFrameStaticRecords cached))
return cached;
WalkFrameStaticRecords records = ResolveCellView(cellId, dynamic: true);
EmitFacilityShadowProbe(records.Records, cellId, "dynamic");
_cellDynamicCache[cellId] = records;
return records;
}
public WalkFrameStaticRecords GetOutdoorStatics(uint cellId)
{
if (_outdoorMaterialized.TryGetValue(cellId, out WalkFrameStaticRecords cached))
return cached;
WalkFrameStaticRecords records = ResolveCellView(cellId, dynamic: false);
_outdoorMaterialized[cellId] = records;
return records;
}
public WalkFrameStaticRecords GetOutdoorDynamics(uint cellId)
{
if (_outdoorDynamicsMaterialized.TryGetValue(
cellId,
out WalkFrameStaticRecords cached))
{
return cached;
}
WalkFrameStaticRecords records = ResolveCellView(cellId, dynamic: true);
_outdoorDynamicsMaterialized[cellId] = records;
return records;
}
public WalkFrameStaticRecords GetOutdoorObjects(uint cellId)
{
if (_outdoorObjectsMaterialized.TryGetValue(
cellId,
out WalkFrameStaticRecords cached))
{
return cached;
}
WalkFrameStaticRecords records = ResolveCellView(cellId, dynamic: null);
_outdoorObjectsMaterialized[cellId] = records;
return records;
}
///
/// Campaign OVERHAUL S2 chunk 5: one cell id's borrowed retail render
/// membership. Reads
/// — already in retail CELLARRAY-then-part-array insertion order —
/// collapses adjacent entries down to their distinct owning entity ids
/// (an entity's own entries for one cell are always written as one
/// contiguous run: PublishRetailPartEntries removes then re-adds a
/// whole entity's rows atomically, never interleaving two entities'
/// rows), resolves each id to its current projected record through
/// , and keeps only
/// the records matching 's static/dynamic
/// class and excluding building shells. Indoor vs. outdoor is entirely a
/// property of WHICH cellId the caller passes (an indoor cell id's low
/// word is >= 0x100, an outdoor one is in [1, 64] —
/// retail's own two id spaces): the registry already published this
/// entity into every crossed cell under its own true id, so no
/// additional indoor/outdoor dispatch is needed here.
///
private WalkFrameStaticRecords ResolveCellView(uint cellId, bool? dynamic)
{
IReadOnlyList entries =
_shadows.GetRetailPartEntriesInCell(cellId);
if (entries.Count == 0)
return WalkFrameStaticRecords.Empty with { TupleLandblockId = _tupleLandblockId };
int written = 0;
uint previousEntityId = 0;
bool havePrevious = false;
for (int i = 0; i < entries.Count; i++)
{
uint entityId = entries[i].EntityId;
if (havePrevious && entityId == previousEntityId)
continue;
previousEntityId = entityId;
havePrevious = true;
if (!_scene.TryGetByLocalEntityId(entityId, out RenderProjectionRecord record))
{
// The transient streaming window (this type's own doc
// comment): the registry already flooded this entity into
// its retail CELLARRAY, but the presentation journal has
// not applied its projected record yet this frame. Retail
// draws nothing for an object not yet in a cell — no second
// fallback.
if (_unregisteredEntitiesThisFrame.Add(entityId))
UnregisteredRenderMembershipCount++;
continue;
}
if (record.EntityPayload.IsBuildingShell)
continue; // buildings draw at their own shell turn.
if (dynamic.HasValue
&& IsDynamicProjectionClass(record.ProjectionClass) != dynamic.Value)
continue;
if (written == _cellViewScratch.Length)
{
var grown = new RenderProjectionRecord[_cellViewScratch.Length * 2];
Array.Copy(_cellViewScratch, grown, written);
_cellViewScratch = grown;
}
_cellViewScratch[written++] = record;
}
return written == 0
? WalkFrameStaticRecords.Empty with { TupleLandblockId = _tupleLandblockId }
: new WalkFrameStaticRecords(
AppendToArena(_cellViewScratch.AsSpan(0, written)), _tupleLandblockId);
}
/// The same static/dynamic split ArchRenderScene's own
/// internal indexing uses: a live dynamic root or an equipped child is
/// "dynamic"; every other (including
/// ) is
/// "static".
private static bool IsDynamicProjectionClass(RenderProjectionClass projectionClass) =>
projectionClass is RenderProjectionClass.LiveDynamicRoot
or RenderProjectionClass.EquippedChild;
///
/// Facility Hub discriminator for retail's cross-cell render-shadow path.
/// The scene query is currently keyed by authored parent cell, while retail
/// also appends each object's parts to every cell in its physics CELLARRAY
/// through CPhysicsObj::add_shadows_to_cells / CPartArray::AddPartsShadow.
/// This probe prints the authoritative physics owner set without changing
/// admission, so a correction is made only if the live staircase/player
/// registration proves that path is populated.
///
private void EmitFacilityShadowProbe(
ReadOnlySpan records,
uint queriedCellId,
string route)
{
if (!AcDream.Core.Rendering.RenderingDiagnostics.ProbeFacilityStairsEnabled)
return;
for (int recordIndex = 0; recordIndex < records.Length; recordIndex++)
{
ref readonly RenderProjectionRecord record = ref records[recordIndex];
bool targetPlayer = record.EntityPayload.CasterIdentity
== RenderCasterIdentityKind.LocalPlayer;
bool targetStair = record.Source.SourceId == 0x02000623u;
bool targetCathedralStair =
record.Source.SourceId == 0x020009A2u;
if (!targetStair && !targetCathedralStair && !targetPlayer)
continue;
IReadOnlyList ownerCells =
_shadows.GetOwnerCells(record.Source.LocalEntityId);
var cells = new StringBuilder(ownerCells.Count * 11 + 2);
cells.Append('[');
for (int cellIndex = 0; cellIndex < ownerCells.Count; cellIndex++)
{
if (cellIndex != 0)
cells.Append(',');
cells.Append("0x").Append(ownerCells[cellIndex].ToString("X8"));
}
cells.Append(']');
string key = $"{route}:{queriedCellId:X8}:"
+ $"{record.Source.LocalEntityId:X8}";
string signature = $"{record.Source.ParentCellId:X8}:{cells}";
if (_facilityShadowProbeSignatures.TryGetValue(key, out string? prior)
&& string.Equals(prior, signature, StringComparison.Ordinal))
{
continue;
}
_facilityShadowProbeSignatures[key] = signature;
Console.WriteLine(
$"[facility-shadow] route={route} "
+ $"kind={(targetPlayer ? "player" : targetCathedralStair ? "cathedral-stair" : "stair")} "
+ $"guid=0x{record.Source.ServerGuid:X8} "
+ $"local=0x{record.Source.LocalEntityId:X8} "
+ $"query=0x{queriedCellId:X8} "
+ $"parent=0x{record.Source.ParentCellId:X8} "
+ $"owners={cells}");
}
}
public WalkFrameStaticRecords GetBuildingShellStatics(WalkBuilding building)
{
uint anchor = BuildingShellBucketCellId(building);
if (anchor == 0)
return WalkFrameStaticRecords.Empty with { TupleLandblockId = _tupleLandblockId };
if (_shellMaterialized.TryGetValue(anchor, out WalkFrameStaticRecords cached))
return cached;
WalkFrameStaticRecords records =
_shellsByAnchor.TryGetValue(anchor, out List? shells)
&& shells.Count > 0
? new WalkFrameStaticRecords(
AppendToArena(CollectionsMarshal.AsSpan(shells)), _tupleLandblockId)
: WalkFrameStaticRecords.Empty with { TupleLandblockId = _tupleLandblockId };
_shellMaterialized[anchor] = records;
return records;
}
/// Copies into at
/// its current length, growing the arena first if needed (doubling, or
/// exactly enough for an unusually large sweep — the same growth shape
/// / already use),
/// and returns the segment the copy landed in. A prior frame's growth can
/// leave an earlier-returned segment pointing at a retired backing array
/// — harmless, since that array's content stays valid and nothing reads
/// a segment across a frame boundary (see this type's own doc
/// comment).
private ArraySegment AppendToArena(
ReadOnlySpan source)
{
if (source.Length == 0)
return ArraySegment.Empty;
int required = _arenaLength + source.Length;
if (required > _arena.Length)
{
var grown = new RenderProjectionRecord[Math.Max(required, _arena.Length * 2)];
Array.Copy(_arena, grown, _arenaLength);
_arena = grown;
}
source.CopyTo(_arena.AsSpan(_arenaLength, source.Length));
var segment = new ArraySegment(_arena, _arenaLength, source.Length);
_arenaLength += source.Length;
return segment;
}
/// The building's authored shell anchor: its first non-exit
/// portal's destination cell — the SAME rule LandblockLoader used
/// when it stamped BuildingShellAnchorCellId on the shell entity.
internal static uint AnchorCellId(WalkBuilding building)
{
foreach (ref readonly WalkBldPortal portal in building.Portals.AsSpan())
{
if (portal.OtherCellId != 0xFFFFFFFFu)
return portal.OtherCellId;
}
return 0;
}
///
/// Selects the retained-scene bucket for a shell record. Portal-bearing
/// buildings use their authored EnvCell anchor. A portal-less building has
/// no such anchor, but it is still a real CBuildingObj; retail's
/// DrawSortCell reaches it through the outdoor cell containing its
/// placement, represented by EffectCellId on the retained record.
///
internal static uint BuildingShellBucketCellId(in RenderProjectionRecord record)
=> record.Source.BuildingShellAnchorCellId != 0
? record.Source.BuildingShellAnchorCellId
: record.Source.EffectCellId;
///
/// Resolves the same bucket from the walk-side building. The authored
/// interior anchor wins when present; otherwise the landscape assembler's
/// exact position cell is the shell turn that retail uses.
///
internal static uint BuildingShellBucketCellId(WalkBuilding building)
{
ArgumentNullException.ThrowIfNull(building);
uint anchor = AnchorCellId(building);
return anchor != 0 ? anchor : building.PositionCellId;
}
public Matrix4x4 GetBuildingWorldTransform(WalkBuilding building)
{
if (!_buildings.TryGetEntry(building, out WalkBuildingFactory.Entry? entry))
{
throw new InvalidOperationException(
$"walk building 0x{building.PositionCellId:X8} has no committed registry entry");
}
return entry.PartZeroWorldTransform;
}
}