acdream/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs
Erik ff607a1e04 fix(render): S3 chunk 4 round 1 — loop-shape weather pin, one weather gate, dead terrain-clip stack deleted, sky drawn once
Campaign OVERHAUL S3 chunk 4 fix round 1 (docs/research/2026-09-01-overhaul/s3-walk-ownership-map.md
§10.4), on top of 6ba4b0b87. K1, K2, K3, K4, K6, K7, K8 (K5 was the lead's own plan-doc note, already
done).

K1 (blocking) — the Assert.Single pin over DrawLandscapeDynamicsPhase's DrawWeatherOnce call site
stayed green even wrapped in a foreach (it counts distinct call-site offsets, not loop shape). Added
a real LOOP-SHAPE pin reusing CompiledCallGraph.ReadBranches: asserts no backward branch (target
offset < its own offset) spans the DrawWeatherOnce call's IL offset — the shape every C#
for/foreach/while loop compiles to.

K2 (minor) — the Collect-time "OC" print fired on ctx.WeatherGateOpen alone while the Replay-time
draw independently re-derived clipAssembly.OutsideViewSlices.Length > 0, which could diverge on an
interior root whose landscape turn ran but whose reassembled outside-view slices ended up empty.
WalkFrameDriver now owns one WeatherTurnFired flag, set unconditionally inside OnWeatherTurn (so it
tracks "did the print's own gate hold" regardless of the transcript flag) and reset every
BeginFrame/AbortFrame; RetailPViewRenderer.DrawLandscapeDynamicsPhase gates DrawWeatherOnce on it
instead of re-deriving its own condition.

K3 (minor) — ClipFrame.SetTerrainClip (the terrain OutsideView writer) had no remaining caller after
chunk 4's original round and K4 below; deleted along with IWorldPassSurface.SetTerrainClip,
RhiWorldPassSurface.SetTerrainClip, the now-orphaned PublishTerrainClip helper, and PrepareClipFrame's
terrain-clip publish call. The terrain/sky shaders still declare the TerrainClip UBO (verified:
terrain_modern.vert, terrain_atmospheric.vert, sky.vert all read uTerrainClipCount/uTerrainClipPlanes),
so the section binding itself (WorldFrameSectionBinding.BindTerrainClip) stays — its existing
zeroed-ring fallback (for when nothing published the section) now binds that same all-zero disabled
block on every frame, identical bytes to the old permanent NoClip/Reset default.

K4 (major) — DrawWalkSky still looped the OUTDOOR case once per active landscape view under a doorway
scissor + BindTerrainClip + EnableClipDistances (the INTERIOR case already drew once unclipped, FW4
slice 6). Retail draws GameSky::Draw(sky,0) ONCE, unconditionally, before LScape::draw's block loop,
for both root kinds. Collapsed DrawWalkSky to one unconditional, unclipped draw; deleted
BeginDoorwayScissor and the RetailPViewPassExecutor.EnableClipDistances wrapper (both lost their only
caller). _surface.BeginScissor/EndScissor and IWorldPassSurface.EnableClipDistances stay:
RhiWorldPassSurface.ClearInteriorDepth still ends an active scissor, and WorldScenePassExecutor (the
separate flat-world path) still calls EnableClipDistances directly.

K6 (minor) — the punch-fan CPU/GPU equivalence pin hand-built a ClipViewSlice from ClipPlaneSet.From's
raw output, which could pass even if ClipFrameAssembler.Assemble's own packing/array-construction
diverged from that output. Rewrote it to build a real PortalVisibilityFrame and run it through
ClipFrameAssembler.Assemble, reading the planes back through assembly.OutsideViewSlices[0].Planes —
the exact outsideSlicesList.Add(new ClipViewSlice(slot, AabbOf(poly), planes)) path
ReassembleOutsideViewFromWalk (the walk's real interior-root producer) shares.

K7 (note) — restored the "a real look-in slice never reuses the reserved no-clip slot 0" assertion
the deleted VisibleClipSlotsInLookInTurn used to prove, via a new internal test-only accessor
(WalkFrameDriver.LookInSliceClipSlotAt) reading the same _lookInSlices storage
InteriorFloodViewClipPlanesAt resolves through _clipFrame.GetSlotPlanes(slice.ClipSlot).

K8 (note) — documented in DrawWeatherOnce's own comment that the weather mesh drawing before the rain
particles is this method's own call-order choice; retail's single GameSky::Draw(sky,1) imposes no
order between acdream's two substitutes.

Every new/changed pin's mutation was hand-verified this session (temporarily applied, ran the
specific test, confirmed the exact failing assertion, then reverted — see the parent task's structured
report for the four failing-assertion texts).

No register row added or removed — every change here deletes an acdream-only rule or repairs a pin;
none introduces a new deviation.

Full solution build: 0 warnings/0 errors. App hermetic (Lane!=InstalledDat/PreparedPackage/Live/
Manual/Timing/Windows/Linux/SystemFont & Purpose!=Diagnostic & Status!=KnownFailure): 6831/6831.
InstalledDat: 244 pass/1 skip/4 known — identical to 6ba4b0b87's own baseline (2x #383 layout tests,
TowerAscent, and the pre-existing #458 WalkLandscape.CheckBlocks block-visibility divergence,
unrelated to and untouched by this round).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-03 16:18:27 +02:00

265 lines
13 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. This
/// <c>Assert.Single</c> alone proved insufficient at fix round 1 (K1):
/// it counts DISTINCT call-site offsets, so it stays green even with a
/// <c>foreach</c> wrapped around the one call site (the exact round-1
/// regression this file's review caught) — see
/// <see cref="DrawLandscapeDynamicsPhase_DrawWeatherOnceCallSiteHasNoEnclosingBackwardBranch"/>
/// for the pin that actually rules that out. Kept as a cheap first-line
/// check: MUTATION: adding a second, textually distinct call site (e.g.
/// a duplicated call, not a loop) 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 4 fix round 1 (K1, blocking): the real loop-shape pin.
/// <see cref="DrawLandscapeDynamicsPhase_CallsDrawWeatherOnceExactlyOnce"/>'s
/// <c>Assert.Single</c> over call-site offsets still passes when the ONE
/// call site sits inside a <c>foreach</c> — an IL-offset ORDER pin has
/// now failed three times to be discriminating for this exact class of
/// regression, so this asks the LOOP-SHAPE question directly: does any
/// BACKWARD branch (a branch whose target offset is lower than its own
/// offset — the shape every C# loop compiles to, whether
/// <c>for</c>/<c>foreach</c>/<c>while</c>) enclose the
/// <c>DrawWeatherOnce</c> call's own IL offset? A call sitting strictly
/// between a backward branch's target and its own offset is inside that
/// loop's body and can run more than once per method invocation; a call
/// outside every backward branch's span cannot. MUTATION: wrap the call
/// in <c>foreach (var slice in clipAssembly.OutsideViewSlices)</c> —
/// the compiled <c>foreach</c> emits a backward branch (the
/// condition-check jump back to the loop body) whose span now contains
/// the call's offset, so this test fails; restore the single
/// unconditional call to make it pass again.
/// </summary>
[Fact]
public void DrawLandscapeDynamicsPhase_DrawWeatherOnceCallSiteHasNoEnclosingBackwardBranch()
{
MethodInfo method = typeof(RetailPViewRenderer).GetMethod(
"DrawLandscapeDynamicsPhase",
BindingFlags.Instance | BindingFlags.NonPublic)!;
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(method);
int callIndex = RequiredCallIndex(
calls,
typeof(RetailPViewPassExecutor),
nameof(RetailPViewPassExecutor.DrawWeatherOnce));
int callOffset = calls[callIndex].Offset;
IReadOnlyList<CompiledBranch> branches = CompiledCallGraph.ReadBranches(method);
Assert.DoesNotContain(
branches,
branch => branch.TargetOffset < branch.Offset
&& branch.TargetOffset <= callOffset
&& callOffset < branch.Offset);
}
/// <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;
}
}