using System.Numerics;
namespace AcDream.App.Rendering.Walk;
///
/// Campaign FW3.1 — the production /
/// /
/// implementation: cells resolve through the committed
/// registry (LoadedCell.Walk, per the FW
/// binding rule that the walk consumes ONLY the committed registry —
/// CellVisibility.TryGetCell, never a synthetic/dead-code path);
/// buildings resolve through ; 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 WorldCameraFrame; wiring that in is additive.
/// The ray caster is a GENERIC inverse-view-projection unprojection, not the
/// capture client's exact Render::xinvscale/tx/vdst
/// constants the FW1 conformance harness's WalkTraceReplayContext
/// uses — those are FIXTURE PINS specific to the 1024×720 capture client,
/// not production values. This is safe because
/// 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.
///
/// Production retains one instance per renderer and rebinds its frame-local
/// camera values through . The registries and grow-only
/// active-view scratch remain renderer-lifetime owners.
///
public sealed class WalkProductionFrameContext : IWalkFrameContext, IRetailFrameWalkContext
{
/// Render::znear @0x0081ec84 / set_vdst @0x0054b240.
public const float ZNear = 0.1f;
private sealed class InverseViewProjectionRayCaster : IWalkRayCaster
{
private Matrix4x4 _inverseViewProjection;
private float _viewportWidth;
private float _viewportHeight;
public InverseViewProjectionRayCaster(
Matrix4x4 viewProjection, float viewportWidth, float viewportHeight)
=> Reset(viewProjection, viewportWidth, viewportHeight);
internal void Reset(
Matrix4x4 viewProjection, float viewportWidth, float viewportHeight)
{
if (!Matrix4x4.Invert(viewProjection, out Matrix4x4 inverseViewProjection))
{
throw new ArgumentException(
"The walk's view-projection matrix must be invertible.",
nameof(viewProjection));
}
// Publish only after validation succeeds. Matrix4x4.Invert writes
// its out value even on failure; assigning the field directly
// would silently corrupt the retained ray caster for the next
// report-and-continue frame.
_inverseViewProjection = inverseViewProjection;
_viewportWidth = viewportWidth;
_viewportHeight = viewportHeight;
}
/// Screen space: origin top-left, +Y down — matching
/// and
/// 's quad
/// winding.
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 Matrix4x4 _viewProjection;
private readonly InverseViewProjectionRayCaster _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));
_rays = new InverseViewProjectionRayCaster(
viewProjection, viewportWidth, viewportHeight);
Reset(worldViewpoint, forward, viewProjection, viewportWidth, viewportHeight);
}
///
/// FW6 allocation closeout: rebind this retained context to one frame's
/// camera values while preserving its active-view scratch. The cell and
/// building registries are lifetime owners and therefore never change.
///
internal void Reset(
Vector3 worldViewpoint,
Vector3 forward,
Matrix4x4 viewProjection,
float viewportWidth,
float viewportHeight)
{
// Validate/invert before publishing any new frame value so a bad
// camera matrix leaves the previous usable binding intact.
_rays.Reset(viewProjection, viewportWidth, viewportHeight);
WorldViewpoint = worldViewpoint;
_viewProjection = viewProjection;
ViewportWidth = viewportWidth;
ViewportHeight = viewportHeight;
_activeViewVertCount = 0;
// 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; private set; }
public float ViewportWidth { get; private set; }
public float ViewportHeight { get; private set; }
public WalkPlane CyPlane { get; private set; }
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;
/// Resolves through the committed
/// 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
/// 's documented contract).
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);
/// CPhysicsPart::UpdateViewerDistance @0x0050e030: the
/// distance to the part's SCALED sort center, not the position origin.
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 output)
{
Matrix4x4 objectToClip = GetEntry(building).WorldTransform * _viewProjection;
Span 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;
}
}