acdream/tests/AcDream.App.Tests/Rendering/RetailPViewPassExecutorTests.cs
Erik bf53e2ad6e refactor(rendering): delete superseded visibility probes
Delete the callerless portal-BFS research graph and spent renderer probe families while retaining the production RetailFrameWalk path, terrain diagnostics, membership invariant, and walk transcript.

Mutation evidence (all restored):

1. Restored PortalVisibilityBuilder type -> AppAssembly_ContainsNoSupersededPortalGraphTypes first failed Assert.Empty with AcDream.App.Rendering.PortalVisibilityBuilder.

2. Restored ACDREAM_PROBE_FACILITY_STAIRS -> ProductionSource_ContainsNoDeletedRendererProbe_AndRetainsWalkTranscriptProof first failed Assert.Empty on RenderingDiagnostics.cs.

3. Added a second RetailFrameWalk field -> WalkFrameOwners_AreUnique first failed Assert.Single with _frameWalk and _mutatedSecondFrameWalk.

4. Added OrderBy to OrderedStream -> OrderedWalkStream_HasNoCrossStreamReorder first failed Assert.DoesNotContain on OrderBy(.

5. Added IDatReaderWriter parameter -> FrameTimeWalkOwners_HaveNoRawDatDependency first failed Assert.Empty on RetailFrameWalk.MutatedRawDatParameter.
2026-09-05 05:18:17 +02:00

631 lines
32 KiB
C#

using System.Linq;
using System.Numerics;
using System.Reflection;
using System.Reflection.Emit;
using AcDream.App.Composition;
using AcDream.App.Rendering;
using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Gpu.Vk;
using AcDream.App.Rendering.Sky;
using AcDream.App.Rendering.Walk;
using AcDream.App.Rendering.Wb;
using AcDream.App.Tests.Architecture;
using AcDream.App.Tests.Rendering.Gpu;
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 (the walk's own
/// screen-space terrain-clip writer + its per-frame clip-routing reset
/// call + 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>
/// S4-c2 fix round 2 (M6): supplemental structure pin linking the real
/// cell submitter to the retained-batch scan and the functional production
/// dispatcher proved by <c>EnvCellAlphaDrawSourceTests</c>. The per-subset
/// <see cref="RetailAlphaMeshRouter"/> calls now correctly live inside
/// <see cref="EnvCellRenderer.GetTransparentRoutes"/> (one call per real
/// retained batch), not here once per whole cell. Mutation check: bypassing
/// that scan for a hand-coded cell-wide mask removes the required call and
/// fails before the dispatch-order assertion.
/// </summary>
[Fact]
public void SubmitOrDrawTransparentCellShell_ScansRetainedBatchesBeforeProductionDispatch()
{
MethodInfo method = typeof(RetailPViewPassExecutor).GetMethod(
"SubmitOrDrawTransparentCellShell",
BindingFlags.Instance | BindingFlags.NonPublic)!;
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(method);
int detailProbe = RequiredCallIndex(
calls, typeof(EnvCellRenderer), "get_TransparentDetailEnabled");
int scan = RequiredCallIndex(
calls, typeof(EnvCellRenderer), nameof(EnvCellRenderer.GetTransparentRoutes));
int dispatch = RequiredCallIndex(
calls, typeof(RetailPViewPassExecutor), nameof(RetailPViewPassExecutor.DispatchTransparentCellShell));
Assert.True(detailProbe < scan);
Assert.True(scan < dispatch);
}
/// <summary>
/// S3 chunk 4 (§10.2): the former per-outside-view-slice loop
/// (the walk's own screen-space terrain-clip writer + its per-frame
/// clip-routing reset call + 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,
/// conditional on <see cref="AcDream.App.Rendering.Walk.WalkFrameDriver.WeatherTurnFired"/>
/// (see <see cref="DrawLandscapeDynamicsPhase_GatesDrawWeatherOnceOnWalkDriverWeatherTurnFired"/>
/// — the L1 pin), 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. Note for reviewers (added
/// at S3 landing hygiene, H4, matching <see cref="DrawWalkSky_RenderSkyCallSiteHasNoEnclosingBackwardBranch"/>'s
/// own note): <see cref="CompiledCallGraph.ReadBranches"/> reads only
/// <c>br</c>/<c>brtrue</c>/<c>brfalse</c>-family single-target branches —
/// it does not decode a compiled <c>switch</c> jump table, but no C# loop
/// construct (<c>for</c>/<c>foreach</c>/<c>while</c>/<c>do</c>) ever
/// compiles to one, so this pin's blind spot is not a loop shape it
/// could miss.
/// </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 4 fix round 2 (L1, BLOCKING). Round 1's three-lens review
/// found that neither existing pin above actually looks at the
/// production CONDITION gating <c>DrawWeatherOnce</c>: restoring the
/// pre-fix gate <c>if (clipAssembly.OutsideViewSlices.Length != 0)</c> —
/// the exact regression K2 was supposed to close — leaves both green,
/// because both only ask "is the call site shaped correctly", never
/// "does the call site read <c>WalkFrameDriver.WeatherTurnFired</c>".
/// This pin reads the compiled condition directly: with <c>c</c> the
/// index of the <c>DrawWeatherOnce</c> call, (a) <c>calls[c-1]</c> must
/// be the <c>WeatherTurnFired</c> getter — the LAST call before the draw
/// — and (b) exactly one branch must sit strictly between that getter
/// call and the draw call, be a <c>brfalse</c>/<c>brfalse.s</c>, and
/// jump FORWARD past the draw call — the compiled shape of
/// <c>if (walkDriver.WeatherTurnFired) passes.DrawWeatherOnce(ctx);</c>
/// and nothing else (an inverted test, an unconditional call, or a
/// different condition entirely all fail one of the two checks).
/// MUTATION M1 (restores the pre-fix regression): change the gate back
/// to <c>if (clipAssembly.OutsideViewSlices.Length != 0)</c> — check (a)
/// fails because <c>calls[c-1]</c> is no longer the
/// <c>WeatherTurnFired</c> getter. MUTATION M2 (drops the gate
/// entirely): make the call unconditional — check (b) fails because no
/// branch sits between the getter call and the draw call (in fact the
/// getter call itself disappears with the gate, so check (a) fails
/// first). MUTATION M3 (inverts the condition): change the gate to
/// <c>if (!walkDriver.WeatherTurnFired)</c> — <c>calls[c-1]</c> is still
/// the getter (check (a) passes), but the compiler emits a
/// <c>brtrue</c>/<c>brtrue.s</c> to skip the draw instead of a
/// <c>brfalse</c>/<c>brfalse.s</c>, so check (b)'s opcode filter finds
/// nothing and <c>Assert.Single</c> fails on zero matches.
/// </summary>
[Fact]
public void DrawLandscapeDynamicsPhase_GatesDrawWeatherOnceOnWalkDriverWeatherTurnFired()
{
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));
Assert.True(callIndex > 0, "Expected a call before DrawWeatherOnce — the gate condition.");
CompiledCall condition = calls[callIndex - 1];
Assert.Equal(typeof(AcDream.App.Rendering.Walk.WalkFrameDriver), condition.Target.DeclaringType);
Assert.Equal("get_WeatherTurnFired", condition.Target.Name);
int conditionOffset = condition.Offset;
int drawOffset = calls[callIndex].Offset;
IReadOnlyList<CompiledBranch> branches = CompiledCallGraph.ReadBranches(method);
Assert.Single(
branches,
branch => branch.Offset > conditionOffset
&& branch.Offset < drawOffset
&& (branch.OpCode == OpCodes.Brfalse || branch.OpCode == OpCodes.Brfalse_S)
&& branch.TargetOffset > drawOffset);
}
/// <summary>
/// S3 landing hygiene (H5): strengthens the L1 pin above against a
/// conjoined gate the post-hoc three-lens review found it does not
/// reject. L1's check (b) only looks at branches STRICTLY BETWEEN the
/// <c>WeatherTurnFired</c> getter call and the <c>DrawWeatherOnce</c>
/// call — so <c>if (clipAssembly.OutsideViewSlices.Length != 0 &amp;&amp;
/// walkDriver.WeatherTurnFired) passes.DrawWeatherOnce(ctx);</c> still
/// passes it: the compiler evaluates <c>OutsideViewSlices.Length != 0</c>
/// FIRST, so ITS OWN <c>brfalse</c> lands BEFORE the getter call's
/// offset — outside L1's window — while <c>calls[c-1]</c> is still the
/// getter (the added condition reads <c>OutsideViewSlices</c>, a
/// property getter, then <c>.Length</c>, a non-call <c>ldlen</c>, so no
/// OTHER call intervenes before the getter). This pin widens the window
/// to start at the call immediately before the whole gate —
/// <see cref="RetailPViewPassExecutor.DrawUnattachedSceneParticles"/>,
/// the outdoor-emitters call the method's own comment names as the last
/// call before the gate — and counts EVERY branch in that wider window
/// with <c>Assert.Single</c>: production has exactly one, the forward
/// <c>brfalse</c>/<c>brfalse.s</c> right after the getter. A conjoined
/// gate's extra, earlier condition adds a second branch this wider
/// window catches but L1's narrower one cannot.
/// MUTATION: change the gate to <c>if
/// (clipAssembly.OutsideViewSlices.Length != 0 &amp;&amp;
/// walkDriver.WeatherTurnFired)</c> — the branch count in the window
/// goes from 1 to 2 and <c>Assert.Single</c> fails (see the commit body
/// for the exact recorded failure text).
/// </summary>
[Fact]
public void DrawLandscapeDynamicsPhase_ExactlyOneBranchGuardsDrawWeatherOnce()
{
MethodInfo method = typeof(RetailPViewRenderer).GetMethod(
"DrawLandscapeDynamicsPhase",
BindingFlags.Instance | BindingFlags.NonPublic)!;
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(method);
int particlesIndex = RequiredCallIndex(
calls,
typeof(RetailPViewPassExecutor),
nameof(RetailPViewPassExecutor.DrawUnattachedSceneParticles));
int drawIndex = RequiredCallIndex(
calls,
typeof(RetailPViewPassExecutor),
nameof(RetailPViewPassExecutor.DrawWeatherOnce));
int particlesOffset = calls[particlesIndex].Offset;
int drawOffset = calls[drawIndex].Offset;
IReadOnlyList<CompiledBranch> branches = CompiledCallGraph.ReadBranches(method);
CompiledBranch onlyGuard = Assert.Single(
branches,
branch => branch.Offset > particlesOffset && branch.Offset < drawOffset);
Assert.True(
onlyGuard.OpCode == OpCodes.Brfalse || onlyGuard.OpCode == OpCodes.Brfalse_S,
$"Expected the sole branch between DrawUnattachedSceneParticles and "
+ $"DrawWeatherOnce to be a brfalse, was {onlyGuard.OpCode}.");
Assert.True(
onlyGuard.TargetOffset > drawOffset,
"Expected the guard branch to skip forward past DrawWeatherOnce.");
}
/// <summary>
/// S3 chunk 4 fix round 2 (L8): the identical loop-shape question K1
/// asked of <see cref="RetailPViewPassExecutor.DrawWeatherOnce"/>'s call
/// site, applied to <see cref="RetailPViewPassExecutor.DrawWalkSky"/>'s
/// own <see cref="SkyRenderer.RenderSky"/> call — retail draws the sky
/// dome exactly once per frame too (K4's own doc comment on
/// <c>DrawWalkSky</c>), so nothing may wrap this call in a loop either.
/// Note for reviewers: <see cref="CompiledCallGraph.ReadBranches"/> reads
/// only <c>br</c>/<c>brtrue</c>/<c>brfalse</c>-family single-target
/// branches — it does not decode a compiled <c>switch</c> jump table,
/// but no C# loop construct (<c>for</c>/<c>foreach</c>/<c>while</c>/
/// <c>do</c>) ever compiles to one, so this pin's blind spot is not a
/// loop shape it could miss. MUTATION: wrapping the call in
/// <c>for (int i = 0; i &lt; 2; i++) { _sky?.RenderSky(...); }</c> makes
/// this fail; restoring the single unconditional call makes it pass
/// again.
/// </summary>
[Fact]
public void DrawWalkSky_RenderSkyCallSiteHasNoEnclosingBackwardBranch()
{
MethodInfo method = typeof(RetailPViewPassExecutor).GetMethod(
"DrawWalkSky",
BindingFlags.Instance | BindingFlags.NonPublic)!;
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(method);
int callIndex = RequiredCallIndex(
calls,
typeof(SkyRenderer),
nameof(SkyRenderer.RenderSky));
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));
}
/// <summary>
/// S4-c1 C1 (T2, seal half): <c>D3DPolyRender::DrawPortalPolyInternal</c>
/// @0x0059bc90's degenerate-input guard, ported at the exit-seal
/// enumeration (<c>DrawPortalDepthWrite</c> — the SAME loop that reads
/// <c>cell.PortalPolygons[index]</c>, the LOCAL portal-polygon
/// vertices, and is the only production caller of
/// <see cref="RetailPViewPassExecutor.DrawExitPortalMask"/>). A live
/// functional test of this private method needs a real
/// <see cref="PortalDepthMaskRenderer"/> the suite has no fake for (see
/// <c>WalkFrameDriverTests.OnPunchGeometry_RejectsWholePolygonOn...</c>
/// for the punch-fan half's functional proof instead), so this pin asks
/// the compiled-call-graph question this file's other tests already use
/// for exactly this situation: does
/// <see cref="AcDream.App.Rendering.Walk.WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard"/>
/// run BEFORE the vertex loop's <see cref="Vector3.Transform(Vector3,
/// Matrix4x4)"/> calls and BEFORE <see cref="PortalDepthMaskRenderer.DrawDepthFan"/>
/// (retail's reject -&gt; transform -&gt; clip -&gt; count order — a hit
/// must never reach either), with a conditional branch gating that
/// order directly off the guard's own return value.
/// MUTATION: move the guard call to AFTER the transform loop (or
/// delete it) — either check below fails because the guard call index
/// is no longer the smallest, or (deletion) <see cref="RequiredCallIndex"/>
/// throws for finding no call at all.
/// </summary>
[Fact]
public void DrawPortalDepthWrite_RejectsDegenerateLocalPolygons_BeforeTransformOrSubmission()
{
MethodInfo method = typeof(RetailPViewPassExecutor).GetMethod(
"DrawPortalDepthWrite",
BindingFlags.Instance | BindingFlags.NonPublic)!;
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(method);
int guardIndex = RequiredCallIndex(
calls,
typeof(AcDream.App.Rendering.Walk.WalkVisibilityMath),
nameof(AcDream.App.Rendering.Walk.WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard));
int transformIndex = RequiredCallIndex(calls, typeof(Vector3), nameof(Vector3.Transform));
int drawIndex = RequiredCallIndex(
calls, typeof(PortalDepthMaskRenderer), nameof(PortalDepthMaskRenderer.DrawDepthFan));
Assert.True(
guardIndex < transformIndex,
"The boundary guard must run BEFORE the world-transform loop "
+ "(retail's reject -> transform -> clip -> count order).");
Assert.True(
guardIndex < drawIndex,
"The boundary guard must run BEFORE the fan is submitted "
+ "(no draw, no `submitted` increment on a hit).");
int guardOffset = calls[guardIndex].Offset;
int transformOffset = calls[transformIndex].Offset;
IReadOnlyList<CompiledBranch> branches = CompiledCallGraph.ReadBranches(method);
Assert.Contains(
branches,
branch => branch.Offset > guardOffset
&& branch.Offset < transformOffset
&& (branch.OpCode == OpCodes.Brtrue || branch.OpCode == OpCodes.Brtrue_S
|| branch.OpCode == OpCodes.Brfalse || branch.OpCode == OpCodes.Brfalse_S));
}
/// <summary>
/// S4-c1 fix round 1, F2: retail increments <c>portalsDrawnCount</c>
/// (0x59BD70-0x59BD74) BEFORE <c>polyClipFinish</c> runs (0x59BDB0) —
/// the counter records accepted ATTEMPTS, not successful GPU fans
/// (<c>oh1-depth-lifecycle.md</c>'s "Far-Z punches and true-depth exit
/// seals" section). A polygon that survives the boundary guard but has
/// fewer than 3 vertices is still COUNTED by
/// <see cref="RetailPViewPassExecutor.DrawExitPortalMask"/>'s returned
/// <c>submitted</c> total, even though
/// <see cref="PortalDepthMaskRenderer.DrawDepthFan"/> draws nothing (its
/// own <c>&lt; 3</c> guard stands in for retail's post-clip
/// <c>var_4 &gt;= 3</c> check). Round 0 dropped the count too — a
/// <c>&lt; 3</c> continue ahead of both the boundary guard and the
/// count — never observed on authored dat data (every real portal
/// polygon has &gt;= 3 vertices) but the wrong order all the same.
/// MUTATION (verified, S4-c1 fix round 2 R2-5b): restore the old
/// <c>localVertices.Length &lt; 3</c> pre-filter ahead of the guard —
/// this pin's synthetic 2-vertex polygon is no longer counted and the
/// assertion fails (<c>submitted</c> comes back 0, not 1). Moving the
/// <c>submitted++</c> increment itself to AFTER
/// <see cref="PortalDepthMaskRenderer.DrawDepthFan"/> — the mutation
/// this doc comment used to name alongside the pre-filter one — does
/// NOT fail this pin: <c>DrawDepthFan</c> has no effect on the local
/// <c>submitted</c> counter either way, so the final returned count is
/// identical regardless of which side of that call the increment sits
/// on. That reordering is unobservable to any assertion on the return
/// value; only the pre-filter mutation is a genuine regression check.
/// </summary>
[Fact]
public void DrawExitPortalMask_CountsAnUnclippableTwoVertexPolygon_ButDrawsNothing()
{
var cell = new LoadedCell
{
CellId = 0xA9B40105u,
WorldTransform = Matrix4x4.Identity,
};
cell.Portals.Add(new CellPortalInfo(OtherCellId: 0xFFFF, PolygonId: 0, Flags: 0, OtherPortalId: 0));
// Ordinary (non-degenerate) coordinates — the boundary guard admits
// this polygon — but only TWO vertices: retail's post-clip
// `var_4 >= 3` check (this port's DrawDepthFan `< 3` guard) drops
// the fan submission even though the count already happened.
cell.PortalPolygons.Add(
[
new Vector3(1f, 1f, 0f),
new Vector3(2f, 1f, 0f),
]);
using var device = new RecordingGpuDevice();
var frames = new GpuDeviceFrameLifetime(device);
var scope = new VulkanWorldPassScope(sampleCount: 1);
using var portalDepthMask = new PortalDepthMaskRenderer(device, frames, scope);
// DrawPortalDepthWrite only reads _portalDepthMask, frame.Cells,
// frame.RootCell.IsOutdoorNode, and frame.ViewProjection — the same
// constructor-bypass pattern WalkOutsideViewReassemblyTests already
// uses for exercising a real leaf without a full GL/DAT renderer
// graph.
var executor = (RetailPViewPassExecutor)System.Runtime.CompilerServices.RuntimeHelpers
.GetUninitializedObject(typeof(RetailPViewPassExecutor));
typeof(RetailPViewPassExecutor)
.GetField("_portalDepthMask", BindingFlags.NonPublic | BindingFlags.Instance)!
.SetValue(executor, portalDepthMask);
var root = new LoadedCell { CellId = 0xF4180003u, IsOutdoorNode = false };
RetailPViewFrameInput frame = new RetailPViewFrameInput().Reset(
rootCell: root,
nearbyBuildingCells: null,
viewerEyePos: Vector3.Zero,
viewProjection: Matrix4x4.Identity,
cells: new SingleCellSource(cell),
camera: null!,
cameraWorldPosition: Vector3.Zero,
frustum: null,
playerLandblockId: null,
animatedEntityIds: null,
renderCenterLbX: 0,
renderCenterLbY: 0,
renderRadius: 0,
landblockEntries: Array.Empty<(uint, Vector3, Vector3,
IReadOnlyList<AcDream.Core.World.WorldEntity>,
IReadOnlyDictionary<uint, AcDream.Core.World.WorldEntity>?)>(),
renderSky: false,
renderWeather: false,
dayFraction: 0f,
activeDayGroup: null,
skyKeyframe: default,
environOverrideActive: false,
viewerCellId: 0,
playerCellId: 0,
playerViewPosition: Vector3.Zero,
cameraView: Matrix4x4.Identity,
cameraCellResolution: default);
int drawsBefore = device.Calls.OfType<GpuRecordedDraw>().Count();
int submitted = executor.DrawExitPortalMask(frame, cell.CellId, ReadOnlySpan<Vector4>.Empty);
int drawsAfter = device.Calls.OfType<GpuRecordedDraw>().Count();
Assert.Equal(1, submitted);
Assert.Equal(drawsBefore, drawsAfter);
}
private sealed class SingleCellSource(LoadedCell cell) : IRetailPViewCellSource
{
public LoadedCell? Find(uint cellId) => cellId == cell.CellId ? cell : null;
}
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;
}
}