acdream/src/AcDream.App/Rendering/Walk/WalkProductionWorldData.cs
Erik 4918677b45 feat(render) Campaign FW3.2b-2: THE STATIC CUTOVER - the walk drives production statics
The retail frame walk now drives every production static draw. In
RetailPViewRenderer.DrawInside, when the concrete executor + the
packed product + the FW3.1 walk registries are all wired (all
production compositions - anything less throws):

- A pre-walk events-only collection pass (the shadow sink generalized
  to WalkVisitedSetCollector) gathers the frame's visited cells,
  buildings, and landscape-cell turns; the visited cells union into
  prepareCells so EnvCellRenderer prepares every shell the driver
  draws.
- DrawWalkDrivenStatics runs the WalkFrameDriver over the production
  world data (WalkProductionWorldData over RenderSceneQuery + the
  building registry): sky, terrain slices, outdoor statics at their
  landscape-cell turns, buildings (alpha barrier -> punch/look-in
  passes -> shell) in retail order, interior clear+seals as leaf
  closures (the old tail block's drain reasoning moves with them),
  flood cells shell-then-contents. Landscape/cell-stage particle
  owners re-source from the walk's visited sets - retail gates
  particles per cell turn (ShouldDrawParticles @0x0050FE60), which
  this is; the old sphere filter was the approximation.
- DrawLandscapeDynamicsPhase + DrawBuildingLookInDynamics carry the
  dynamics-only remainder (LookInObject now dynamic-classified,
  late outside-dynamics + weather, particle unions); DrawDynamicsLast
  and the outdoor flush are unchanged.
- The product builder stops emitting LandscapeOutdoorStatic /
  LandscapeBuildingShell / CellStatic (methods deleted, dead index
  tracking removed); LookInObject loads cells with
  includeStatics: false.

The old static path survives ONLY behind !walkActive for the
standalone/diagnostic executor-fake path that keeps 15 retail-ordering
regression tests exercising the barrier/punch/seal machinery; no
production composition can reach it. Its deletion is FW4 scope (the
plan's "deleting the patch apparatus") - recorded in the plan.

Transitional risks recorded in code/report: the two-pass walk cost
(FW3.4 measures), the interior slice-count reconciliation between the
old clip assembly and the walk's own exit-view survival, and the
outdoor merged-flood punch coverage now riding the walk's own
building-BSP punches (retail-faithful per FW1; the owner visual gate
verifies).

Suites (lead-verified): full Release build 0 warnings; hermetic
6,750/0 (baseline minus the three deleted route tests); Walk lane
201/1; InstalledDat Walk conformance 40/1 untouched. The two
IL-branch tests the implementation round reported failing pass in
every lead run - the recurring parallel-load flake pair.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 15:43:22 +02:00

180 lines
8.1 KiB
C#

using System.Numerics;
using AcDream.App.Rendering.Scene;
namespace AcDream.App.Rendering.Walk;
/// <summary>
/// Campaign FW3.2b-2: the production <see cref="IWalkFrameWorldData"/> over
/// the retained scene (<see cref="RenderSceneQuery"/>) and the FW3.1
/// <see cref="WalkBuildingRegistry"/>. Rebuilt facts per frame via
/// <see cref="BeginFrame"/>:
///
/// <list type="bullet">
/// <item>Cell statics — <see cref="RenderSceneQuery.CopyCellStaticsTo"/> on
/// demand, one pooled array 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).</item>
/// <item>Outdoor statics — ONE <see cref="RenderSceneQuery.CopyIndexTo"/>
/// sweep bucketed by landscape cell id
/// (<c>(lb &amp; 0xFFFF0000) | (cellX*8 + cellY + 1)</c> from the record's
/// world position — the same encoding <see cref="IWalkEventSink.OnLandscapeCellTurn"/>
/// computes), EXCLUDING building shells (they draw at their building's own
/// shell turn, retail <c>CPhysicsPart::Draw(parts, 0)</c> @0x0059f331, not
/// at the cell's <c>DrawObjCell</c> turn).</item>
/// <item>Building shells — the same sweep's <c>IsBuildingShell</c> records
/// bucketed by <c>Source.BuildingShellAnchorCellId</c>; a
/// <see cref="WalkBuilding"/> maps to its anchor via its first
/// non-exit portal's destination (the SAME rule
/// <c>LandblockLoader</c> used to author the anchor).</item>
/// </list>
///
/// The tuple landblock id handed to the classifier is the frame's player
/// landblock — the packed path's own convention for every
/// <c>RenderFrameEntityDrawRequest</c>.
/// </summary>
internal sealed class WalkProductionWorldData : IWalkFrameWorldData
{
private readonly WalkBuildingRegistry _buildings;
private RenderSceneQuery _scene;
private uint _tupleLandblockId;
private readonly Dictionary<uint, WalkFrameStaticRecords> _cellCache = new();
private readonly Dictionary<uint, List<RenderProjectionRecord>> _outdoorByCell = new();
private readonly Dictionary<uint, List<RenderProjectionRecord>> _shellsByAnchor = new();
private readonly Dictionary<uint, WalkFrameStaticRecords> _outdoorMaterialized = new();
private readonly Dictionary<uint, WalkFrameStaticRecords> _shellMaterialized = new();
private RenderProjectionRecord[] _sweepScratch = new RenderProjectionRecord[1024];
private RenderProjectionRecord[] _cellScratch = new RenderProjectionRecord[256];
internal WalkProductionWorldData(WalkBuildingRegistry buildings)
{
_buildings = buildings ?? throw new ArgumentNullException(nameof(buildings));
}
/// <summary>Rebuilds the frame's outdoor/shell buckets and clears the
/// per-cell cache. Call once per frame before the driver runs.</summary>
internal void BeginFrame(RenderSceneQuery scene, uint tupleLandblockId)
{
_scene = scene;
_tupleLandblockId = tupleLandblockId;
_cellCache.Clear();
_outdoorMaterialized.Clear();
_shellMaterialized.Clear();
foreach (List<RenderProjectionRecord> bucket in _outdoorByCell.Values)
bucket.Clear();
foreach (List<RenderProjectionRecord> bucket in _shellsByAnchor.Values)
bucket.Clear();
int count;
while (true)
{
count = _scene.CopyIndexTo(RenderSceneIndex.OutdoorStatic, _sweepScratch);
if (count < _sweepScratch.Length)
break;
_sweepScratch = new RenderProjectionRecord[_sweepScratch.Length * 2];
}
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<RenderProjectionRecord>? shells))
_shellsByAnchor[anchor] = shells = new List<RenderProjectionRecord>();
shells.Add(record);
continue;
}
uint cellId = LandscapeCellId(record.Transform.Position);
if (!_outdoorByCell.TryGetValue(cellId, out List<RenderProjectionRecord>? bucket))
_outdoorByCell[cellId] = bucket = new List<RenderProjectionRecord>();
bucket.Add(record);
}
}
/// <summary>The landscape cell owning a world position — retail's
/// 24 m cell grid inside the 192 m landblock, the same
/// <c>(lb &amp; 0xFFFF0000) | (cellX*8 + cellY + 1)</c> encoding the walk's
/// landscape turn emits.</summary>
internal static uint LandscapeCellId(Vector3 worldPosition)
{
int lbX = (int)MathF.Floor(worldPosition.X / 192f);
int lbY = (int)MathF.Floor(worldPosition.Y / 192f);
float localX = worldPosition.X - lbX * 192f;
float localY = worldPosition.Y - lbY * 192f;
int cellX = Math.Clamp((int)(localX / 24f), 0, 7);
int cellY = Math.Clamp((int)(localY / 24f), 0, 7);
uint landblock = ((uint)(byte)lbX << 24) | ((uint)(byte)lbY << 16);
return landblock | (uint)(cellX * 8 + cellY + 1);
}
public WalkFrameStaticRecords GetCellStatics(uint cellId)
{
if (_cellCache.TryGetValue(cellId, out WalkFrameStaticRecords cached))
return cached;
int count;
while (true)
{
count = _scene.CopyCellStaticsTo(cellId, _cellScratch);
if (count < _cellScratch.Length)
break;
_cellScratch = new RenderProjectionRecord[_cellScratch.Length * 2];
}
WalkFrameStaticRecords records = count == 0
? WalkFrameStaticRecords.Empty with { TupleLandblockId = _tupleLandblockId }
: new WalkFrameStaticRecords(_cellScratch[..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<RenderProjectionRecord>? bucket)
&& bucket.Count > 0
? new WalkFrameStaticRecords([.. 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<RenderProjectionRecord>? shells)
&& shells.Count > 0
? new WalkFrameStaticRecords([.. shells], _tupleLandblockId)
: WalkFrameStaticRecords.Empty with { TupleLandblockId = _tupleLandblockId };
_shellMaterialized[anchor] = records;
return records;
}
/// <summary>The building's authored shell anchor: its first non-exit
/// portal's destination cell — the SAME rule <c>LandblockLoader</c> used
/// when it stamped <c>BuildingShellAnchorCellId</c> on the shell entity.</summary>
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;
}
}