Campaign OVERHAUL S3 chunk 4 (docs/research/2026-09-01-overhaul/s3-walk-ownership-map.md §10). Retail
polygon-clips exactly three things on the CPU — portal polygons (PView::GetClip), punch fans
(DrawPortalPolyInternal against building_view), and exit seals (setup_view + polyClipFinish);
ordinary meshes, cell shells, terrain, particles and the weather are never view-clipped. Every
acdream-only clip rule beyond those three is deleted; the two that remain get a new CPU/GPU
equivalence pin.
DELETED (no retail counterpart):
- RetailPViewRenderer.DrawLandscapeDynamicsPhase's per-outside-view-slice weather loop
(SetTerrainClip + ClearClipRouting + the old DrawLandscapeSliceLate leaf, one call per active
landscape view) — retail's GameSky::Draw(sky,1) @0x00506ff0 runs ONCE, unclipped, after
LScape::draw's whole block loop. RetailPViewPassExecutor.DrawWeatherOnce stays the one call site;
it now also submits the rain particles (ParticleRenderPass.SkyPostScene) as ONE unclipped
submission, folded in from the deleted loop's per-slice clipSlot draw.
- RetailPViewPassExecutor.DrawLandscapeSliceLate, RetailPViewLandscapeLateSliceContext,
SetTerrainClip (the walk-level wrapper — its only caller was the deleted loop),
ClearClipRouting (same), SetCellShellClipRouting (already had no caller), and
UseIndoorMembershipOnlyRouting (4 call sites in RetailPViewRenderer.cs) — all "first view clips
the shell" residue: SetCellShellClipRouting was the ONLY place EnvCellRenderer.SetClipRouting
ever received a non-null value anywhere in the app, so once it's gone,
UseIndoorMembershipOnlyRouting's own clear calls are provably permanent no-ops. AbortFrame's
matching TryAbort(SetClipRouting(null))/TryAbort(ClearClipRouting) lines are dropped for the
same reason. KEPT: BeginDoorwayScissor, _surface.BindTerrainClip(), EnableClipDistances/
DisableClipDistances, and IWorldPassSurface.SetTerrainClip/ClipFrame.SetTerrainClip/
PrepareClipFrame's terrain-clip publish — all still have real consumers (DrawWalkSky's per-slice
outdoor sky mesh draw; the always-bound TerrainClip UBO section) or are shared classic (non-walk)
infrastructure out of this chunk's ownership (WorldScenePassExecutor, PortalTunnelPresentation).
- WbDrawDispatcher.WalkClassify.ResolvePartClipSlots (renamed ResolvePartVisible, returns bool) and
RetailWholeMeshSlot: the per-view clip-slot LIST always collapsed to either nothing or one
element (RenderDeviceD3D::DrawMesh @0x005A0860 submits the whole mesh once any view admits it) —
never anything but a Boolean wearing a list-shaped costume. IWalkLookInViewSource.
VisibleClipSlotsInLookInTurn is deleted outright (no consumer besides that collapse);
SphereVisibleInLookInTurn gained VisibleClipSlotsInLookInTurn's own testSphere parameter so the
no-authored-sphere case ("any admitted view, geometry ignored") still has a home.
EmitClassifiedBatches now emits exactly one batch per surviving TryClassifyBatch using the
entity's own classic slot (always 0 while _clipRoutingActive is never armed by the walk path).
- The six ProbeCathedralSkip*/ProbeCathedralShellOrderEnabled discriminators (RenderingDiagnostics,
their six docs/launch-options.md rows, every read site in WalkFrameDriver/
RetailPViewPassExecutor.WalkLeaf/RetailPViewRenderer, and the WalkFrameDriver.
TraceCathedralShellOrder method + RetailPViewRenderer._probeCathedralShellOrderFrame it served) —
the investigation is closed: the cathedral leak is retail behavior, not an acdream defect to
discriminate.
O1-O4 (the ledger's carried chunk-1 items, closed here because this chunk owns the weather path):
IWalkEventSink gains OnWeatherTurn(viewerCellId), fired by RetailFrameWalk.DrawLandscape
UNCONDITIONALLY at retail's own call-site shape (GameSky::Draw(sky,1) is itself unconditional from
LScape::draw; the is_player_outside gate lives inside it) but gated by the walk context's own new
WeatherGateOpen/ViewerCellId members (default false/0, so every other IRetailFrameWalkContext
implementer — test fixtures, the FW1 conformance replay context — is unaffected).
WalkProductionFrameContext wires RetailPViewFrameInput.ViewerCellId and
RetailPViewPassExecutor.ShouldDrawWeatherOnce(RenderSky, RenderWeather, PlayerCellId) into those two
members. WalkFrameDriver.OnWeatherTurn prints the "OC" transcript line at COLLECT time; the print is
removed from DrawWeatherOnce (the DRAW stays there, at Replay). Because DrawLandscape's new call sits
at the very end of the method, an outdoor root's transcript naturally ends with the line and an
interior root's lands strictly between "LS" and the flood's first "EC" — exactly retail's position —
with no extra plumbing.
Kept items each get a synthetic-view CPU/GPU equivalence pin (ClipFrameLayoutTests): exit seals via
the real ClipFrame.AppendSlot/GetSlotPlanes round trip, punch fans via the direct
ClipViewSlice.Planes = cps.PlaneArray assignment ClipFrameAssembler.Assemble uses — both assert every
polygon edge midpoint has ~0 signed distance under the GPU-read-back planes and non-negative distance
under every plane.
Every new pin's mutation was hand-verified this session (temporarily reintroduced, confirmed the
exact failing assertion, then reverted): a duplicated OnWeatherTurn call fails Assert.Single with
"2 matching items" on both the outdoor and interior position tests; suppressing the Collect-time
call fails the same assertions with "did not contain any matching items"; perturbing one GPU plane's
offset by 0.05 fails the exit-seal equivalence pin with "the closest plane was only 0.050000012
away". WalkProductionFrameContextTests pins that the ViewerCellId/WeatherGateOpen storage seam
itself never drops or swaps its own two constructor/Reset arguments.
No register row added or removed — every change here deletes an acdream-only rule; none introduces
a new deviation. Full solution build: 0 warnings/0 errors. App hermetic 6,829/6,829 (was 6,828 base
+ 1 new pin). InstalledDat 244 pass/1 skip/4 known (2x #383, TowerAscent, and the pre-existing #458
WalkLandscape.CheckBlocks block-visibility divergence — unrelated to this chunk, untouched by it).
Core.Tests Rendering 216/216.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
224 lines
10 KiB
C#
224 lines
10 KiB
C#
using System.Numerics;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Rendering.Walk;
|
|
|
|
namespace AcDream.App.Tests.Rendering.Walk;
|
|
|
|
/// <summary>Campaign FW3.1 — hermetic (no DAT) coverage of the production
|
|
/// frame context: cell resolution through <see cref="CellVisibility"/>,
|
|
/// building resolution through <see cref="WalkBuildingRegistry"/>, and the
|
|
/// CyPlane/ray-cast wiring. This proves the SEAM (does the context read the
|
|
/// right registries the right way); the ten-fixture conformance gate proves
|
|
/// the WORLD DATA those registries are fed is correct.</summary>
|
|
public sealed class WalkProductionFrameContextTests
|
|
{
|
|
private static Matrix4x4 SimpleViewProjection() =>
|
|
Matrix4x4.CreateLookAt(Vector3.Zero, Vector3.UnitY, Vector3.UnitZ)
|
|
* Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 3f, 4f / 3f, 0.1f, 1000f);
|
|
|
|
[Fact]
|
|
public void GetVisible_ResolvesThroughTheCommittedCellVisibilityRegistry()
|
|
{
|
|
var cellVisibility = new CellVisibility();
|
|
var walkCell = new WalkCell { CellId = 0xA9B40100u };
|
|
var loaded = new LoadedCell { CellId = 0xA9B40100u, Walk = walkCell };
|
|
cellVisibility.CommitLandblock(0xA9B4FFFFu, new[] { loaded });
|
|
var ctx = new WalkProductionFrameContext(
|
|
cellVisibility, new WalkBuildingRegistry(), Vector3.Zero, Vector3.UnitY,
|
|
SimpleViewProjection(), 1024f, 768f);
|
|
|
|
Assert.Same(walkCell, ctx.GetVisible(0xA9B40100u));
|
|
Assert.Null(ctx.GetVisible(0xA9B40101u));
|
|
}
|
|
|
|
[Fact]
|
|
public void GetVisible_ReturnsNullWhenTheCommittedCellHasNoWalkModel()
|
|
{
|
|
// A hand-built LoadedCell that never went through
|
|
// EnvCellLandblockBuildBuilder.BuildVisibilityCell (test-only
|
|
// shortcut some existing fixtures take) — Walk stays null.
|
|
var cellVisibility = new CellVisibility();
|
|
var loaded = new LoadedCell { CellId = 0xA9B40100u };
|
|
cellVisibility.CommitLandblock(0xA9B4FFFFu, new[] { loaded });
|
|
var ctx = new WalkProductionFrameContext(
|
|
cellVisibility, new WalkBuildingRegistry(), Vector3.Zero, Vector3.UnitY,
|
|
SimpleViewProjection(), 1024f, 768f);
|
|
|
|
Assert.Null(ctx.GetVisible(0xA9B40100u));
|
|
}
|
|
|
|
[Fact]
|
|
public void ObjectToClipAndViewpointIn_UseTheCellsOwnTransforms()
|
|
{
|
|
var cell = new WalkCell
|
|
{
|
|
CellId = 1,
|
|
WorldTransform = Matrix4x4.CreateTranslation(10f, 0f, 0f),
|
|
InverseWorldTransform = Matrix4x4.CreateTranslation(-10f, 0f, 0f),
|
|
};
|
|
Matrix4x4 vp = SimpleViewProjection();
|
|
var ctx = new WalkProductionFrameContext(
|
|
new CellVisibility(), new WalkBuildingRegistry(), new Vector3(10f, 0f, 0f), Vector3.UnitY,
|
|
vp, 1024f, 768f);
|
|
|
|
Assert.Equal(cell.WorldTransform * vp, ctx.ObjectToClip(cell));
|
|
Assert.Equal(Vector3.Zero, ctx.ViewpointIn(cell));
|
|
}
|
|
|
|
[Fact]
|
|
public void ViewpointInBuilding_ResolvesThroughWalkBuildingRegistry()
|
|
{
|
|
var registry = new WalkBuildingRegistry();
|
|
var building = new WalkBuilding { PositionCellId = 1 };
|
|
Matrix4x4 world = Matrix4x4.CreateTranslation(5f, 0f, 0f);
|
|
Matrix4x4.Invert(world, out Matrix4x4 inverse);
|
|
registry.Publish(0xA9B4FFFFu, new[] { new WalkBuildingFactory.Entry(building, world, inverse) });
|
|
var ctx = new WalkProductionFrameContext(
|
|
new CellVisibility(), registry, new Vector3(5f, 0f, 0f), Vector3.UnitY,
|
|
SimpleViewProjection(), 1024f, 768f);
|
|
|
|
Assert.Equal(Vector3.Zero, ctx.ViewpointInBuilding(building));
|
|
}
|
|
|
|
[Fact]
|
|
public void ViewpointInBuilding_ThrowsWhenTheBuildingIsNotCommitted()
|
|
{
|
|
// Fail loud (the PV3 post-mortem rule): a walk/registry desync must
|
|
// never resolve to a silently-skipped building.
|
|
var ctx = new WalkProductionFrameContext(
|
|
new CellVisibility(), new WalkBuildingRegistry(), Vector3.Zero, Vector3.UnitY,
|
|
SimpleViewProjection(), 1024f, 768f);
|
|
var unregistered = new WalkBuilding { PositionCellId = 1 };
|
|
|
|
Assert.Throws<InvalidOperationException>(() => ctx.ViewpointInBuilding(unregistered));
|
|
}
|
|
|
|
[Fact]
|
|
public void ViewerDistanceTo_MeasuresToTheBuildingsTransformedSortCenter()
|
|
{
|
|
var registry = new WalkBuildingRegistry();
|
|
var building = new WalkBuilding { PositionCellId = 1, SortCenter = new Vector3(0f, 3f, 0f) };
|
|
Matrix4x4 world = Matrix4x4.CreateTranslation(0f, 10f, 0f);
|
|
Matrix4x4.Invert(world, out Matrix4x4 inverse);
|
|
registry.Publish(0xA9B4FFFFu, new[] { new WalkBuildingFactory.Entry(building, world, inverse) });
|
|
var ctx = new WalkProductionFrameContext(
|
|
new CellVisibility(), registry, Vector3.Zero, Vector3.UnitY,
|
|
SimpleViewProjection(), 1024f, 768f);
|
|
|
|
Assert.Equal(13f, ctx.ViewerDistanceTo(building));
|
|
}
|
|
|
|
[Fact]
|
|
public void CyPlane_MatchesTheRetailNearPlaneFormula()
|
|
{
|
|
Vector3 forward = Vector3.UnitY;
|
|
var eye = new Vector3(0f, 5f, 0f);
|
|
var ctx = new WalkProductionFrameContext(
|
|
new CellVisibility(), new WalkBuildingRegistry(), eye, forward,
|
|
SimpleViewProjection(), 1024f, 768f);
|
|
|
|
Assert.Equal(forward, ctx.CyPlane.Normal);
|
|
Assert.Equal(-Vector3.Dot(eye, forward) - WalkProductionFrameContext.ZNear, ctx.CyPlane.D);
|
|
}
|
|
|
|
[Fact]
|
|
public void Constructor_RejectsANonInvertibleViewProjection()
|
|
{
|
|
Assert.Throws<ArgumentException>(() => new WalkProductionFrameContext(
|
|
new CellVisibility(), new WalkBuildingRegistry(), Vector3.Zero, Vector3.UnitY,
|
|
default, 1024f, 768f));
|
|
}
|
|
|
|
[Fact]
|
|
public void Reset_RebindsCameraValuesAndRetainsTheRayCaster()
|
|
{
|
|
Matrix4x4 firstProjection = SimpleViewProjection();
|
|
var ctx = new WalkProductionFrameContext(
|
|
new CellVisibility(), new WalkBuildingRegistry(), Vector3.Zero, Vector3.UnitY,
|
|
firstProjection, 1024f, 768f);
|
|
IWalkRayCaster retainedRays = ctx.Rays;
|
|
|
|
var eye = new Vector3(4f, 5f, 6f);
|
|
Vector3 forward = Vector3.UnitX;
|
|
Matrix4x4 secondProjection =
|
|
Matrix4x4.CreateLookAt(eye, eye + forward, Vector3.UnitZ)
|
|
* Matrix4x4.CreatePerspectiveFieldOfView(MathF.PI / 4f, 16f / 9f, 0.1f, 500f);
|
|
|
|
ctx.Reset(eye, forward, secondProjection, 1760f, 990f);
|
|
|
|
Assert.Same(retainedRays, ctx.Rays);
|
|
Assert.Equal(eye, ctx.WorldViewpoint);
|
|
Assert.Equal(1760f, ctx.ViewportWidth);
|
|
Assert.Equal(990f, ctx.ViewportHeight);
|
|
Assert.Equal(forward, ctx.CyPlane.Normal);
|
|
Assert.Equal(-Vector3.Dot(eye, forward) - WalkProductionFrameContext.ZNear, ctx.CyPlane.D);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Campaign OVERHAUL S3 chunk 4 (O1/O2): <see
|
|
/// cref="WalkProductionFrameContext"/> is the ONE place production
|
|
/// converts <c>RetailPViewFrameInput.ViewerCellId</c> and the
|
|
/// pre-computed <c>RetailPViewPassExecutor.ShouldDrawWeatherOnce</c>
|
|
/// gate into <see cref="IRetailFrameWalkContext.ViewerCellId"/> / <see
|
|
/// cref="IRetailFrameWalkContext.WeatherGateOpen"/> — this pins that
|
|
/// both the constructor and <see cref="WalkProductionFrameContext.Reset"/>
|
|
/// store and expose EXACTLY what they were given, both at construction
|
|
/// and after a later rebind (this driver instance is retained and
|
|
/// re-bound once per frame — <see
|
|
/// cref="Reset_RebindsCameraValuesAndRetainsTheRayCaster"/>'s own
|
|
/// pattern). Defaults (0 / false) match every OTHER caller that never
|
|
/// wires the weather turn (fixtures, the FW1 conformance harness).
|
|
/// MUTATION: swapping either argument for the other's default at the
|
|
/// production call site (<c>RetailPViewRenderer.DrawInside</c>) is a
|
|
/// code-visible defect this test does not itself catch (that requires
|
|
/// reading the production call site — the real end-to-end proof lives
|
|
/// in <c>WalkFrameDriverTranscriptTests</c>'s printed-transcript pins,
|
|
/// which observe whatever value actually reaches the walk); this test
|
|
/// instead pins that the STORAGE seam itself never drops or swaps its
|
|
/// own two arguments.
|
|
/// </summary>
|
|
[Fact]
|
|
public void Constructor_and_Reset_StoreTheViewerCellIdAndWeatherGate()
|
|
{
|
|
var ctx = new WalkProductionFrameContext(
|
|
new CellVisibility(), new WalkBuildingRegistry(), Vector3.Zero, Vector3.UnitY,
|
|
SimpleViewProjection(), 1024f, 768f);
|
|
Assert.Equal(0u, ctx.ViewerCellId);
|
|
Assert.False(ctx.WeatherGateOpen);
|
|
|
|
var ctxArmed = new WalkProductionFrameContext(
|
|
new CellVisibility(), new WalkBuildingRegistry(), Vector3.Zero, Vector3.UnitY,
|
|
SimpleViewProjection(), 1024f, 768f,
|
|
viewerCellId: 0xF4180003u, weatherGateOpen: true);
|
|
Assert.Equal(0xF4180003u, ctxArmed.ViewerCellId);
|
|
Assert.True(ctxArmed.WeatherGateOpen);
|
|
|
|
// A later Reset (the per-frame rebind) fully replaces both — no
|
|
// stale carry-over from the previous frame's values.
|
|
ctxArmed.Reset(
|
|
Vector3.Zero, Vector3.UnitY, SimpleViewProjection(), 1024f, 768f,
|
|
viewerCellId: 0xA9B40100u, weatherGateOpen: false);
|
|
Assert.Equal(0xA9B40100u, ctxArmed.ViewerCellId);
|
|
Assert.False(ctxArmed.WeatherGateOpen);
|
|
}
|
|
|
|
[Fact]
|
|
public void Reset_WithBadProjectionLeavesThePreviousBindingUsable()
|
|
{
|
|
Matrix4x4 projection = SimpleViewProjection();
|
|
var eye = new Vector3(1f, 2f, 3f);
|
|
var ctx = new WalkProductionFrameContext(
|
|
new CellVisibility(), new WalkBuildingRegistry(), eye, Vector3.UnitY,
|
|
projection, 1024f, 768f);
|
|
Vector3 rayBeforeFailure = ctx.Rays.RayThrough(200f, 300f);
|
|
|
|
Assert.Throws<ArgumentException>(() =>
|
|
ctx.Reset(new Vector3(9f), Vector3.UnitX, default, 1f, 1f));
|
|
|
|
Assert.Equal(eye, ctx.WorldViewpoint);
|
|
Assert.Equal(1024f, ctx.ViewportWidth);
|
|
Assert.Equal(768f, ctx.ViewportHeight);
|
|
Assert.Equal(Vector3.UnitY, ctx.CyPlane.Normal);
|
|
Assert.Equal(rayBeforeFailure, ctx.Rays.RayThrough(200f, 300f));
|
|
}
|
|
}
|