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:
parent
b95850defe
commit
b10ad662b0
14 changed files with 1711 additions and 2 deletions
174
src/AcDream.App/Rendering/Walk/WalkProductionFrameContext.cs
Normal file
174
src/AcDream.App/Rendering/Walk/WalkProductionFrameContext.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue