feat(render) Campaign FW3.1: production walk world data behind the seam

The retail frame walk's world model now materializes from production
landblock-build owners through the legal IDatReaderWriter seam, with
zero frame wiring (FW3.2 roots the frame):

- WalkCellFactory: WalkCell built in the SAME pass as LoadedCell
  (EnvCellLandblockBuild.BuildVisibilityCell) from the raw portal
  Flags/polygons/planes/stab lists already parsed there; stored as
  LoadedCell.Walk, committed atomically with the cell. The
  fixture-pinned decodes (inverse-0x2 portal side, 0xFFFF->0xFFFFFFFF
  exit widening) live here.
- WalkBuildingFactory + WalkBuildingRegistry: the production
  WalkBuilding build (drawing BSP with PORT nodes, degrade ladder,
  portal sides/stab lists, sort center, model frame) from the SAME
  LandBlockInfo the streaming build already fetches, under the
  factory's existing DAT lock - closing the gap where BuildingLoader
  drops every walk field at load.
- WalkLandscapeAssembler: the retail 51x51 viewer-centred grid
  (mid_radius 25) fed incrementally from landblock publish/retire;
  per-block z-slab (heightTable[max]+200 / [min]-1) computed
  worker-side in LandblockBuildFactory from the heights already in
  hand. O(1) SetViewer on same-block frames.
- WalkProductionFrameContext: the walk's frame contexts over
  CellVisibility + WalkBuildingRegistry with a generic
  inverse-view-projection ray caster (rays feed cross products only -
  scale-free) and the znear=0.1 CY plane.
- Publication: LandblockRenderPublisher owns both walk registries,
  publishing in the same AdvanceCompleteOne step as BuildingRegistry
  and retiring in RemoveBuildingRegistry - same commit, same
  retirement, no new ticket stage.

Conformance: ALL TEN oracle fixtures replay identically through the
PRODUCTION builders (WalkProductionWorldConformanceTests) - same
signatures as the test adapter, first run. Known gap documented for
FW3.2: far-tier landblocks carry no EnvCell transaction, so their
z-slab never reaches the assembler.

Suites: full Release build 0 warnings; Walk lane 186/1 skip;
hermetic 6,738/0 (+24); RuntimeDatAccessArchitectureTests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-30 13:19:22 +02:00
parent b95850defe
commit b10ad662b0
14 changed files with 1711 additions and 2 deletions

View file

@ -0,0 +1,174 @@
using System.Numerics;
namespace AcDream.App.Rendering.Walk;
/// <summary>
/// Campaign FW3.1 — the production <see cref="IWalkFrameContext"/> /
/// <see cref="IRetailFrameWalkContext"/> / <see cref="IWalkBuildingFrameContext"/>
/// implementation: cells resolve through the committed
/// <see cref="CellVisibility"/> registry (<c>LoadedCell.Walk</c>, per the FW
/// binding rule that the walk consumes ONLY the committed registry —
/// <c>CellVisibility.TryGetCell</c>, never a synthetic/dead-code path);
/// buildings resolve through <see cref="WalkBuildingRegistry"/>; the camera
/// pose, projection, and viewport are supplied by the caller.
///
/// Deliberately NOT coupled to any concrete camera type — FW3.2 will supply
/// live values from <c>WorldCameraFrame</c>; wiring that in is additive.
/// The ray caster is a GENERIC inverse-view-projection unprojection, not the
/// capture client's exact <c>Render::xinvscale</c>/<c>tx</c>/<c>vdst</c>
/// constants the FW1 conformance harness's <c>WalkTraceReplayContext</c>
/// uses — those are FIXTURE PINS specific to the 1024×720 capture client,
/// not production values. This is safe because
/// <see cref="WalkCopyView.Append"/> only ever CROSS-PRODUCTS these rays to
/// build view-edge planes: a uniform scale or additive offset along a ray
/// cancels out of every cross product it feeds, so any two points along the
/// true eye ray (near/far unprojection) are observably equivalent to
/// retail's exact construction for this contract.
///
/// One instance is a per-frame value (like <c>WalkTraceReplayContext</c>):
/// construct fresh each frame with that frame's camera pose.
/// </summary>
public sealed class WalkProductionFrameContext : IWalkFrameContext, IRetailFrameWalkContext
{
/// <summary><c>Render::znear</c> @0x0081ec84 / <c>set_vdst</c> @0x0054b240.</summary>
public const float ZNear = 0.1f;
private sealed class InverseViewProjectionRayCaster : IWalkRayCaster
{
private readonly Matrix4x4 _inverseViewProjection;
private readonly float _viewportWidth;
private readonly float _viewportHeight;
public InverseViewProjectionRayCaster(
Matrix4x4 viewProjection, float viewportWidth, float viewportHeight)
{
if (!Matrix4x4.Invert(viewProjection, out _inverseViewProjection))
{
throw new ArgumentException(
"The walk's view-projection matrix must be invertible.",
nameof(viewProjection));
}
_viewportWidth = viewportWidth;
_viewportHeight = viewportHeight;
}
/// <summary>Screen space: origin top-left, +Y down — matching
/// <see cref="WalkScreenClip.TransformToScreen"/> and
/// <see cref="WalkCopyView.AppendFullViewportQuad"/>'s quad
/// winding.</summary>
public Vector3 RayThrough(float screenX, float screenY)
{
float ndcX = screenX / _viewportWidth * 2f - 1f;
float ndcY = 1f - screenY / _viewportHeight * 2f;
Vector4 near = Vector4.Transform(
new Vector4(ndcX, ndcY, 0f, 1f), _inverseViewProjection);
Vector4 far = Vector4.Transform(
new Vector4(ndcX, ndcY, 1f, 1f), _inverseViewProjection);
Vector3 nearWorld = new Vector3(near.X, near.Y, near.Z) / near.W;
Vector3 farWorld = new Vector3(far.X, far.Y, far.Z) / far.W;
return farWorld - nearWorld;
}
}
private readonly CellVisibility _cells;
private readonly WalkBuildingRegistry _buildings;
private readonly Matrix4x4 _viewProjection;
private readonly IWalkRayCaster _rays;
private Vector2[] _activeViewVerts = new Vector2[32];
private int _activeViewVertCount;
public WalkProductionFrameContext(
CellVisibility cells,
WalkBuildingRegistry buildings,
Vector3 worldViewpoint,
Vector3 forward,
Matrix4x4 viewProjection,
float viewportWidth,
float viewportHeight)
{
_cells = cells ?? throw new ArgumentNullException(nameof(cells));
_buildings = buildings ?? throw new ArgumentNullException(nameof(buildings));
WorldViewpoint = worldViewpoint;
_viewProjection = viewProjection;
ViewportWidth = viewportWidth;
ViewportHeight = viewportHeight;
_rays = new InverseViewProjectionRayCaster(viewProjection, viewportWidth, viewportHeight);
// The retail CY near plane: N = forward, d = -dot(eye, forward) - znear.
CyPlane = new WalkPlane(forward, -Vector3.Dot(worldViewpoint, forward) - ZNear);
}
public Vector3 WorldViewpoint { get; }
public float ViewportWidth { get; }
public float ViewportHeight { get; }
public WalkPlane CyPlane { get; }
public IWalkRayCaster Rays => _rays;
public IWalkFrameContext CellContext => this;
public Vector3 ViewpointIn(WalkCell cell)
=> Vector3.Transform(WorldViewpoint, cell.InverseWorldTransform);
public Matrix4x4 ObjectToClip(WalkCell cell) => cell.WorldTransform * _viewProjection;
/// <summary>Resolves through the committed <see cref="CellVisibility"/>
/// registry only — a missing/uncommitted cell returns null (retail's
/// portal-skip behavior; the FW binding rule requires any such miss to
/// be diagnostically counted under a flag rather than silently swallowed
/// once a caller wires one up — this seam does not itself log, matching
/// <see cref="IWalkFrameContext.GetVisible"/>'s documented contract).</summary>
public WalkCell? GetVisible(uint cellId)
=> _cells.TryGetCell(cellId, out LoadedCell? cell) ? cell?.Walk : null;
public void SetActiveView(WalkPortalView views, int index)
{
WalkViewPoly poly = views.View.Polys[index];
if (_activeViewVerts.Length < poly.VertexCount)
_activeViewVerts = new Vector2[poly.VertexCount];
for (int k = 0; k < poly.VertexCount; k++)
_activeViewVerts[k] = views.View.Vertices[poly.VertexIndex + k].Point;
_activeViewVertCount = poly.VertexCount;
}
public Vector3 ViewpointInBuilding(WalkBuilding building)
=> Vector3.Transform(WorldViewpoint, GetEntry(building).InverseWorldTransform);
/// <summary><c>CPhysicsPart::UpdateViewerDistance</c> @0x0050e030: the
/// distance to the part's SCALED sort center, not the position origin.</summary>
public float ViewerDistanceTo(WalkBuilding building)
{
WalkBuildingFactory.Entry entry = GetEntry(building);
return Vector3.Distance(
WorldViewpoint, Vector3.Transform(building.SortCenter, entry.WorldTransform));
}
public int ClipBuildingPolygon(
WalkBuilding building, WalkPolygon polygon, int side, Span<WalkScreenPoint> output)
{
Matrix4x4 objectToClip = GetEntry(building).WorldTransform * _viewProjection;
Span<WalkScreenPoint> projected = stackalloc WalkScreenPoint[polygon.Vertices.Length];
for (int i = 0; i < polygon.Vertices.Length; i++)
{
projected[i] = WalkScreenClip.TransformToScreen(
polygon.Vertices[i], objectToClip, ViewportWidth, ViewportHeight);
}
if (side != 0)
projected.Reverse();
return WalkScreenClip.ClipAgainstView(
projected, _activeViewVerts.AsSpan(0, _activeViewVertCount), output);
}
private WalkBuildingFactory.Entry GetEntry(WalkBuilding building)
{
if (!_buildings.TryGetEntry(building, out var entry))
{
// Fail loud (the PV3 post-mortem rule): a building the walk is
// actively placing MUST be committed in the same registry the
// walk was handed — a miss here is a walk/registry desync, never
// a silently-skipped building.
throw new InvalidOperationException(
"WalkProductionFrameContext was asked to place a WalkBuilding " +
"that is not committed in its WalkBuildingRegistry.");
}
return entry;
}
}