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; } }