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>
180 lines
8.1 KiB
C#
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 & 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 & 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;
|
|
}
|
|
}
|