using System.Numerics;
using System.Runtime.InteropServices;
using AcDream.App.Rendering.Scene;
namespace AcDream.App.Rendering.Walk;
///
/// Campaign FW3.2b-2: the production over
/// the retained scene () and the FW3.1
/// . Rebuilt facts per frame via
/// :
///
///
/// - Cell statics — on
/// demand, one arena segment per distinct cell per frame (a cell can be
/// visited once by the root flood OR once per admitting look-in portal; the
/// per-frame cache keeps the copy single).
/// - Outdoor statics — ONE
/// sweep bucketed by landscape cell id
/// ((lb & 0xFFFF0000) | (cellX*8 + cellY + 1) from the record's
/// world position — the same encoding
/// computes), EXCLUDING building shells (they draw at their building's own
/// shell turn, retail CPhysicsPart::Draw(parts, 0) @0x0059f331, not
/// at the cell's DrawObjCell turn).
/// - Building shells — the same sweep's IsBuildingShell records
/// bucketed by Source.BuildingShellAnchorCellId; a
/// maps to its anchor via its first
/// non-exit portal's destination (the SAME rule
/// LandblockLoader used to author the anchor).
///
///
/// The tuple landblock id handed to the classifier is the frame's player
/// landblock — the packed path's own convention for every
/// RenderFrameEntityDrawRequest.
///
/// Campaign FW3.4a: , ,
/// and used to materialize their result
/// with _cellScratch[..count] / [.. bucket] — a FRESH
/// RenderProjectionRecord[] allocation per distinct cell/anchor per
/// frame. At a town-density frame (dozens of cells) that was the single
/// largest contributor to the FW3.4 perf checkpoint's 14× frame-allocation
/// regression (1.9 MB/frame p50). replaces it: a
/// grow-only buffer, reset to length 0 once per frame in
/// , that every materialization call
/// s its records into instead of snapshotting a
/// new array — after the arena reaches its steady-state size (a few frames
/// of warmup, same shape as /'s
/// existing grow-on-demand pattern), zero further heap allocation occurs
/// here. Every segment is
/// STRICTLY per-frame scratch — nothing holds one across a frame boundary
/// (the driver/populator consume it immediately, matching
/// 's existing lifetime contract) — so reusing the
/// same backing array's memory next frame is safe.
///
internal sealed class WalkProductionWorldData : IWalkFrameWorldData
{
private readonly WalkBuildingRegistry _buildings;
private RenderSceneQuery _scene;
private uint _tupleLandblockId;
private int _renderCenterLbX;
private int _renderCenterLbY;
private readonly Dictionary _cellCache = new();
private readonly Dictionary> _outdoorByCell = new();
private readonly Dictionary> _shellsByAnchor = new();
private readonly Dictionary _outdoorMaterialized = new();
private readonly Dictionary _shellMaterialized = new();
private RenderProjectionRecord[] _sweepScratch = new RenderProjectionRecord[1024];
private RenderProjectionRecord[] _cellScratch = new RenderProjectionRecord[256];
// 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)
{
_buildings = buildings ?? throw new ArgumentNullException(nameof(buildings));
}
/// Rebuilds the frame's outdoor/shell buckets and clears the
/// per-cell cache. Call once per frame before the driver runs.
/// /
/// are the streaming recenter origin: record positions are
/// RENDER-ORIGIN-RELATIVE (each landblock's entities carry
/// (lbX − CenterX)·192 offsets), so mapping a position back to
/// its TRUE landblock byte needs the center added back — the first
/// connected gate of the FW3.2b-2 cutover shipped without this and most
/// outdoor scenery landed in garbage buckets no walk turn ever reads.
internal void BeginFrame(
RenderSceneQuery scene,
uint tupleLandblockId,
int renderCenterLbX,
int renderCenterLbY)
{
_scene = scene;
_tupleLandblockId = tupleLandblockId;
_renderCenterLbX = renderCenterLbX;
_renderCenterLbY = renderCenterLbY;
_cellCache.Clear();
_outdoorMaterialized.Clear();
_shellMaterialized.Clear();
_arenaLength = 0;
foreach (List bucket in _outdoorByCell.Values)
bucket.Clear();
foreach (List bucket in _shellsByAnchor.Values)
bucket.Clear();
// CopyIndexTo THROWS on an undersized destination (ArchRenderScene
// validates up front — the first connected gate run of the FW3.2b-2
// cutover crashed on exactly this at Aerlinthe's 5,040 outdoor
// statics), so presize from the query's own index counts.
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)
{
uint anchor = record.Source.BuildingShellAnchorCellId;
if (!_shellsByAnchor.TryGetValue(anchor, out List? shells))
_shellsByAnchor[anchor] = shells = new List();
shells.Add(record);
continue;
}
uint cellId = LandscapeCellId(
record.Transform.Position, _renderCenterLbX, _renderCenterLbY);
if (!_outdoorByCell.TryGetValue(cellId, out List? bucket))
_outdoorByCell[cellId] = bucket = new List();
bucket.Add(record);
}
}
/// The landscape cell owning a RENDER-ORIGIN-RELATIVE position
/// — retail's 24 m cell grid inside the 192 m landblock, producing the
/// same TRUE (lb & 0xFFFF0000) | (cellX*8 + cellY + 1) encoding
/// the walk's landscape turn emits: the relative block index
/// (floor(p/192)) plus the streaming center recovers the true
/// landblock byte, because entity positions carry
/// (lbX − CenterX)·192 world offsets
/// (LandblockBuildFactory's worldOffset).
internal static uint LandscapeCellId(
Vector3 relativePosition, int renderCenterLbX, int renderCenterLbY)
{
int relBlockX = (int)MathF.Floor(relativePosition.X / 192f);
int relBlockY = (int)MathF.Floor(relativePosition.Y / 192f);
float localX = relativePosition.X - relBlockX * 192f;
float localY = relativePosition.Y - relBlockY * 192f;
int cellX = Math.Clamp((int)(localX / 24f), 0, 7);
int cellY = Math.Clamp((int)(localY / 24f), 0, 7);
uint landblock =
((uint)(byte)(renderCenterLbX + relBlockX) << 24)
| ((uint)(byte)(renderCenterLbY + relBlockY) << 16);
return landblock | (uint)(cellX * 8 + cellY + 1);
}
public WalkFrameStaticRecords GetCellStatics(uint cellId)
{
if (_cellCache.TryGetValue(cellId, out WalkFrameStaticRecords cached))
return cached;
// Same up-front-validation contract as CopyIndexTo: presize from the
// query's own count rather than probing with an undersized span.
int required = _scene.GetCellStaticCount(cellId);
if (required > _cellScratch.Length)
{
_cellScratch = new RenderProjectionRecord[
Math.Max(required, _cellScratch.Length * 2)];
}
int count = _scene.CopyCellStaticsTo(cellId, _cellScratch);
WalkFrameStaticRecords records = count == 0
? WalkFrameStaticRecords.Empty with { TupleLandblockId = _tupleLandblockId }
: new WalkFrameStaticRecords(
AppendToArena(_cellScratch.AsSpan(0, count)), _tupleLandblockId);
_cellCache[cellId] = records;
return records;
}
public WalkFrameStaticRecords GetOutdoorStatics(uint cellId)
{
if (_outdoorMaterialized.TryGetValue(cellId, out WalkFrameStaticRecords cached))
return cached;
WalkFrameStaticRecords records =
_outdoorByCell.TryGetValue(cellId, out List? bucket)
&& bucket.Count > 0
? new WalkFrameStaticRecords(
AppendToArena(CollectionsMarshal.AsSpan(bucket)), _tupleLandblockId)
: WalkFrameStaticRecords.Empty with { TupleLandblockId = _tupleLandblockId };
_outdoorMaterialized[cellId] = records;
return records;
}
public WalkFrameStaticRecords GetBuildingShellStatics(WalkBuilding building)
{
uint anchor = AnchorCellId(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;
}
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.WorldTransform;
}
}