acdream/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs
Erik 62c2231d0a refactor(render): S3 chunk 4 — delete the clip rules retail does not have and the cathedral discriminators
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>
2026-09-03 13:32:36 +02:00

220 lines
10 KiB
C#

using System.Reflection;
using AcDream.App.Composition;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Sky;
using AcDream.App.Rendering.Walk;
using AcDream.App.Tests.Architecture;
namespace AcDream.App.Tests.Rendering;
public sealed class RetailPViewPassExecutorTests
{
[Fact]
public void Extracted_contracts_retain_no_window_callbacks_or_visibility_owner()
{
Assert.DoesNotContain(
typeof(RetailPViewFrameInput).GetProperties(),
property => typeof(Delegate).IsAssignableFrom(property.PropertyType));
FieldInfo[] fields = typeof(RetailPViewPassExecutor).GetFields(
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.DoesNotContain(fields, field => field.FieldType == typeof(GameWindow));
Assert.DoesNotContain(fields, field => field.FieldType == typeof(CellVisibility));
Assert.DoesNotContain(fields, field => field.FieldType == typeof(RetailPViewFrameInput));
Assert.DoesNotContain(fields, field => field.FieldType == typeof(RetailPViewFrameResult));
Assert.DoesNotContain(fields, field => field.FieldType == typeof(ClipFrameAssembly));
Assert.DoesNotContain(
fields,
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
}
[Fact]
public void Concrete_executor_accumulates_walk_terrain_batch_timing()
{
// S3 chunk 3 fix round 1 (F3): the walk leaf no longer brackets
// itself with Begin()/Complete() (that stopwatch-restart pair would
// push one timing SAMPLE per batch, not one per frame) — it times
// itself with a raw Stopwatch.GetTimestamp() delta and hands the
// elapsed ticks to AccumulateWalkBatch, which only accumulates.
MethodInfo landscape = typeof(RetailPViewPassExecutor).GetMethod(
"DrawWalkLandCellBatch",
BindingFlags.Instance | BindingFlags.NonPublic)!;
IReadOnlyList<CompiledCall> landscapeCalls = CompiledCallGraph.Read(landscape);
int terrainDraw = RequiredCallIndex(
landscapeCalls,
typeof(TerrainModernRenderer),
nameof(TerrainModernRenderer.DrawLandCells));
int accumulate = RequiredCallIndex(
landscapeCalls,
typeof(TerrainDrawDiagnosticsController),
nameof(TerrainDrawDiagnosticsController.AccumulateWalkBatch));
Assert.True(terrainDraw < accumulate);
Assert.DoesNotContain(
landscapeCalls,
call => call.Target.DeclaringType == typeof(TerrainDrawDiagnosticsController)
&& call.Target.Name == nameof(TerrainDrawDiagnosticsController.Begin));
Assert.DoesNotContain(
landscapeCalls,
call => call.Target.DeclaringType == typeof(TerrainDrawDiagnosticsController)
&& call.Target.Name == nameof(TerrainDrawDiagnosticsController.Complete));
}
[Fact]
public void Concrete_executor_pushes_the_walk_terrain_frame_sample_at_replay_end()
{
// S3 chunk 3 fix round 1 (F3): DrawWalkDrivenStatics is the ONE call
// site of driver.Replay in production — CompleteWalkTerrainFrame
// must run immediately after it, so the frame's sample is pushed
// exactly once, at "the end of the walk replay".
MethodInfo drawWalkDrivenStatics = typeof(RetailPViewRenderer).GetMethod(
"DrawWalkDrivenStatics",
BindingFlags.Instance | BindingFlags.NonPublic)!;
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(drawWalkDrivenStatics);
int replay = RequiredCallIndex(
calls,
typeof(AcDream.App.Rendering.Walk.WalkFrameDriver),
nameof(AcDream.App.Rendering.Walk.WalkFrameDriver.Replay));
int completeWalkFrame = RequiredCallIndex(
calls,
typeof(RetailPViewPassExecutor),
nameof(RetailPViewPassExecutor.CompleteWalkTerrainFrame));
Assert.True(replay < completeWalkFrame);
}
[Fact]
public void Frame_composition_constructs_one_walk_executor()
{
MethodInfo compose = typeof(FrameRootCompositionPhase).GetMethod(
"ComposeCore",
BindingFlags.Instance | BindingFlags.NonPublic)!;
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(compose);
int executor = RequiredCallIndex(
calls,
typeof(RetailPViewPassExecutor),
".ctor");
int renderer = RequiredCallIndex(
calls,
typeof(WorldScenePViewRenderer),
".ctor");
Assert.True(executor < renderer);
Assert.Single(
calls,
call => call.Target.DeclaringType == typeof(RetailPViewPassExecutor)
&& call.Target.Name == ".ctor");
Assert.Single(
calls,
call => call.Target.DeclaringType == typeof(WorldScenePViewRenderer)
&& call.Target.Name == ".ctor");
}
/// <summary>
/// S3 chunk 1 fix round 2 (§11.6 H1): the weather MESH draw + its OC
/// print moved OUT of <see cref="RetailPViewPassExecutor.DrawWeatherOnce"/>
/// entirely — S3 chunk 4 (O3) relocated the print to
/// <c>WalkFrameDriver.OnWeatherTurn</c>, fired at Collect time by
/// <c>RetailFrameWalk.DrawLandscape</c> (see the real transcript pins in
/// <c>WalkFrameDriverTranscriptTests</c>: <c>Collect_OutdoorRoot_...</c>
/// and <c>Collect_InteriorRoot_...</c>). This method now draws the
/// weather MESH and the rain PARTICLES only — the former per-outside-
/// view-slice loop that used to run before this call (<c>SetTerrainClip</c>
/// + <c>ClearClipRouting</c> + the old <c>DrawLandscapeSliceLate</c> leaf)
/// is deleted outright (§10.2): retail draws the weather mesh and its
/// rain particles ONCE, unclipped, never once per doorway aperture.
/// MUTATION: re-inlining a
/// <c>WalkTranscriptDump.PrintObjectCellTurn</c> call back into this
/// method makes the <c>Assert.DoesNotContain</c> below fail; deleting
/// either the mesh or the particle call makes the matching
/// <c>Assert.Single</c> fail (zero matches instead of one).
/// </summary>
[Fact]
public void DrawWeatherOnce_DrawsTheWeatherMeshAndParticlesButNeverPrints()
{
MethodInfo method = typeof(RetailPViewPassExecutor).GetMethod(
nameof(RetailPViewPassExecutor.DrawWeatherOnce),
BindingFlags.Instance | BindingFlags.Public)!;
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(method);
Assert.Single(
calls,
call => call.Target.DeclaringType == typeof(SkyRenderer)
&& call.Target.Name == nameof(SkyRenderer.RenderWeather));
Assert.Single(
calls,
call => call.Target.DeclaringType == typeof(ParticleRenderer)
&& call.Target.Name == nameof(ParticleRenderer.Draw));
Assert.DoesNotContain(
calls,
call => call.Target.DeclaringType == typeof(WalkTranscriptDump));
}
/// <summary>
/// S3 chunk 4 (§10.2): the former per-outside-view-slice loop
/// (<c>SetTerrainClip</c> + <c>ClearClipRouting</c> + the old
/// <c>DrawLandscapeSliceLate</c> leaf, one call per active landscape
/// view) is deleted — <c>DrawLandscapeDynamicsPhase</c> now calls
/// <see cref="RetailPViewPassExecutor.DrawWeatherOnce"/> exactly once,
/// unconditionally, with no loop of any kind around it (a deleted
/// symbol cannot be re-introduced without a compile error, so this pin
/// only needs to rule out a NEW multi-call path). MUTATION: adding a
/// second call site (e.g. reintroducing a per-slice loop around a new
/// leaf) makes <c>Assert.Single</c> fail.
/// </summary>
[Fact]
public void DrawLandscapeDynamicsPhase_CallsDrawWeatherOnceExactlyOnce()
{
MethodInfo method = typeof(RetailPViewRenderer).GetMethod(
"DrawLandscapeDynamicsPhase",
BindingFlags.Instance | BindingFlags.NonPublic)!;
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(method);
Assert.Single(
calls,
call => call.Target.DeclaringType == typeof(RetailPViewPassExecutor)
&& call.Target.Name == nameof(RetailPViewPassExecutor.DrawWeatherOnce));
}
/// <summary>
/// S3 chunk 1 fix round 2 (§11.6 H1): <see
/// cref="RetailPViewPassExecutor.ShouldDrawWeatherOnce"/> is
/// <see cref="DrawWeatherOnce_DrawsTheWeatherMeshAndPrintsExactlyOnce"/>'s
/// gate, extracted as a pure predicate so this suite can pin "no OC line
/// while the player stands indoors" without a live GL/DAT
/// <see cref="SkyRenderer"/> — combined with the two structural tests
/// above (moved out of the loop; drawn/printed exactly once per call),
/// this proves both halves of the spec's pin: an outdoor root (or an
/// interior root with several exit-view slices) prints exactly one OC
/// line, and an indoor player prints none. Retail's own check:
/// <c>SmartBox::is_player_outside</c> @0x00451e80,
/// <c>(player objcell_id &amp; 0xFFFF) &lt; 0x100</c>. MUTATION: negating
/// the <c>&lt; 0x100</c> comparison (or dropping either bool AND) makes
/// one of the four rows below fail.
/// </summary>
[Theory]
[InlineData(true, true, 0xF4180003u, true)] // outdoor root, player outside a land cell -> draws
[InlineData(true, true, 0xA9B40100u, false)] // player indoors (local id >= 0x100) -> no draw
[InlineData(false, true, 0xF4180003u, false)] // RenderSky off -> no draw
[InlineData(true, false, 0xF4180003u, false)] // RenderWeather off -> no draw
public void ShouldDrawWeatherOnce_MatchesRetailIsPlayerOutsideGate(
bool renderSky, bool renderWeather, uint playerCellId, bool expected)
{
Assert.Equal(
expected,
RetailPViewPassExecutor.ShouldDrawWeatherOnce(renderSky, renderWeather, playerCellId));
}
private static int RequiredCallIndex(
IReadOnlyList<CompiledCall> calls,
Type declaringType,
string methodName)
{
int index = CompiledCallGraph.IndexOf(calls, declaringType, methodName);
Assert.True(
index >= 0,
$"Expected call to {declaringType.Name}.{methodName}.");
return index;
}
}