Round 1 (2bd353234) was three-lens reviewed: retail-faithful PASS,
production PASS, gate-honesty FAIL. Every finding landed on the
terrace-edge KnownFailure row plus one pin gap. This is the last round.
R2-1 - gate honesty. WalkTraceConformanceTests.AlphaDepthTranscript.cs's
own KnownFailure doc comment claimed "the PC sequence matches exactly";
it never did - Assert.Equal(expected.PmEvents, actualPm) threw first, so
the PC Assert.Equal line never ran. RunAlphaDepthTranscriptGate now
computes BOTH diffs (SequenceEqual, not Assert.Equal) before asserting
either, and Assert.Fail's message prints both sequences together
whenever either diverges - a written claim about one sequence is now
only ever one this method actually evaluated. Re-running the OLD
(unseeded, cold PortalsDrawnCount=0) code path through this new combined
assertion shows terrace-edge's real divergence was WORSE than round 1
believed: both PM and PC diverged (PC expected counter=2, actual
counter=0) - round 1's "PC matches" claim was not just untested, it was
false.
R2-2 - the harness fix. Retail's portalsDrawnCount (wo(008719b4)) is a
PERSISTENT session global; each capture's own FIRST PM or PC line
(cathedral-arrival 0, cathedral-leak 0, foundry-deep 1,
holtburg-doorway-still 2, terrace-edge 2, cathedral-stair-arch 8) already
carries the pre-capture value - terrace-edge's own sample sits in the
"F 1" preamble text a cdb session prints before the first parsed frame
marker (line 1209, ahead of "F 1" at line 1298), so no amount of
replaying the CAPTURED frames from a cold start can ever reach it (this
fixture's own turns never touch the counter - every PC line reads
ov=0). New WalkAlphaDepthTrace.LoadInitialCounter(root, fixtureName)
scans a fixture's raw lines, ignoring frame boundaries, for the first
PM/PC match and returns its counter. RunAlphaDepthTranscriptGate seeds
WalkFrameDriver.PortalsDrawnCount (already an internal, test-assembly-
settable field) from this value, runs ONE Collect+Replay pass, and
deletes the two-pass "priming" replay outright. Result: terrace-edge
turns GREEN with no KnownFailure tag; every other pose stays exact
(a fixture with real steady-state seal activity computes the identical
value a priming pass used to converge to, since a steady state is by
definition unchanged by one more pass). No pose diverges after seeding,
so the packet's "write the divergence into section 9" fallback never
triggers - nothing needed there this round.
R2-3 - the SOME-vs-SAME-plane discriminator. T2's existing five cases
never separated retail's real quantifier ("every vertex lies on the SAME
one of the four +/-12 planes") from a weaker "every vertex lies on SOME
plane" (per-vertex OR across the four planes, ANDed across vertices) -
the existing split-plane case's third vertex sits on NO plane at all, so
both readings admit it for the same reason. New case in
WalkVisibilityMathTests.cs
(Boundary_guard_admits_a_polygon_whose_every_vertex_is_on_SOME_plane_but_not_the_SAME_one)
and a fourth sink.OnPunchGeometry call in WalkFrameDriverTests.cs's
OnPunchGeometry_RejectsOnlyWhenEveryVertexSharesOnePlane_ButPunchesAnyOtherShape
pin close the gap: [(12,0,3),(0,12,3),(12,5,3)] - every vertex touches
SOME plane (vertex1/3 on x=+12, vertex2 on y=+12) but no SINGLE plane
holds all three (x=+12 cleared by vertex2's x=0, y=+12 cleared by
vertex1's y=0) - retail's real four-accumulator guard admits it; the
weaker per-vertex-OR form rejects it.
R2-4 - the sixth pose. Cherry-picked 4f12cd06b (walk+alphadepth capture)
and 3a6aa7eef (pmverts capture) from claude/campaign-w-retail-frame-walk
onto 2bd353234 - both applied clean, no conflicts (README.md merged
without a manual row). New
AlphaDepthTranscript_CathedralStairArch_MatchesRetailFrame2 Fact: root
0xF4180114, seven outside views, sixteen mode=1 building punches (all
counterBefore=0008 - far-Z punches never touch the counter) of which
twelve are guard-rejected on local y=12.000, then eight mode=0 exit
seals with counterBefore running 0..7 against the root's own ov=7
read-then-zero, settling this fixture's own steady-state counter at 8 -
exactly the value LoadInitialCounter reads back out of its own preamble.
PASS.
R2-5 - comment truth. (a) RetailPViewPassExecutor.cs:
DrawPortalDepthWrite's and DrawExitPortalMask's summaries corrected -
neither the retired ">=3 vertices" pre-filter nor "always matches what
reached the GPU" describe F2's actual count-before-clip behavior any
more. (b) The F2 pin's doc comment
(RetailPViewPassExecutorTests.DrawExitPortalMask_CountsAnUnclippableTwoVertexPolygon_ButDrawsNothing)
named TWO alternative mutations as if both failed; verified only one
does (see mutations below) - the doc comment now says so explicitly and
keeps only the pre-filter mutation. (c) WalkVisibilityMath.cs's doc
comment stated DAT-scan figures as bare facts; independently re-verified
this round with a freshly-written scan (own code, walking the same
production EnvCell.CellPortals/CellStruct.Polygons shape
WalkCellFactory itself uses, over the full 0x0000-0xFFFF landblock-prefix
range) - every cited figure reproduced bit-exact (3,405 landblocks with
cells; 1,854,237 portal polygons; 2,889 any-vertex; 2,163 all-on-plane,
all exit, 0 interior; 16,939 exit portals total, 1,837,298 interior) -
so the doc comment now states the counting rule (id range, what's
counted, how unresolvable polygons/cells are skipped) alongside every
figure instead of leaving them unqualified; landblocksWithCells=3,405
also independently matches CellStructSurfaceConstructionInstalledDatTests'
own OH2 walk of the identical range (that test's own golden-count
comment). The scratch scan itself is not part of this diff (deleted
after use, same as F1's own datScan). (d) This commit body does not
cite "register tests" as evidence - no test reads the register.
Register truth is the human read: no row changes, AD-119 (the C2
blend-vs-write-mask row) unchanged.
Mutations run this round (all verified against production code, then
reverted):
- R2-1/R2-2 (disable seeding, driver.PortalsDrawnCount = 0 instead of
initialCounter): AlphaDepthTranscript_TerraceEdge_MatchesRetailFrame2
fails - "terrace-edge.alphadepth: PM DIVERGES - expected
[(mode=1,counterBefore=2), (mode=1,counterBefore=2)], actual
[(mode=1,counterBefore=0), (mode=1,counterBefore=0)]; PC DIVERGES -
expected [(ov=0,counter=2,fc=0), (ov=0,counter=2,fc=0)], actual
[(ov=0,counter=0,fc=0), (ov=0,counter=0,fc=0)]" - proving both the
seeding fix (R2-2) and the honest dual-sequence message (R2-1) at
once.
- R2-3 (replace the four per-plane accumulators with a single
per-vertex "on ANY plane" test ANDed across vertices):
Boundary_guard_admits_a_polygon_whose_every_vertex_is_on_SOME_plane_but_not_the_SAME_one
fails, Assert.False() Expected:False Actual:True;
OnPunchGeometry_RejectsOnlyWhenEveryVertexSharesOnePlane_ButPunchesAnyOtherShape
fails, Assert.Equal() Expected:3 Actual:2.
- R2-5b, mutation ONE (restore the old `localVertices.Length < 3`
pre-filter ahead of the guard):
DrawExitPortalMask_CountsAnUnclippableTwoVertexPolygon_ButDrawsNothing
fails, Assert.Equal() Expected:1 Actual:0 - kept as the pin's cited
mutation.
- R2-5b, mutation TWO (move `submitted++` to AFTER
`_portalDepthMask.DrawDepthFan(...)` instead of before): same test
STILL PASSES - DrawDepthFan has no effect on the local `submitted`
counter, so the final returned value is identical regardless of
which side of that call the increment sits on. Confirmed unobservable
exactly as R2-5b required; the doc comment no longer names this as a
failing mutation.
Gates: dotnet build tests/AcDream.App.Tests -c Release -> 0 warnings/0
errors. dotnet build src/AcDream.App -c Release -> 0 warnings/0 errors.
Hermetic lane (Lane!=InstalledDat&Lane!=PreparedPackage&Lane!=Live&
Lane!=Manual&Lane!=Timing&Lane!=Windows&Lane!=Linux&Lane!=SystemFont&
Purpose!=Diagnostic&Status!=KnownFailure): 6830/6830 passed (+1 over
round 1's 6829, the new T2 case). InstalledDat lane
(ACDREAM_RUN_INSTALLED_DAT_TESTS=1, --filter Lane=InstalledDat): 254
total (+1 over round 1's 253, the new sixth Fact), 249 passed, 4 failed -
exactly the pre-existing four (LayoutImporterMediaBearingChildSweepTests
and LayoutImporterInvisibleSweepTests, both #383;
TowerAscentReplayTests' KnownFailure; Oh_doorway_still_first_frame_diff,
#458) plus the pre-existing WalkLookInGateSweepTests skip - terrace-edge
no longer fails, no new divergence anywhere else. All six
AlphaDepthTranscript_*_MatchesRetailFrame2 Facts: 6/6 passed standalone.
Shader classes (VulkanShaderDescriptorContractTests +
VulkanShaderManifestTests + RenderPackSpirvValidatorTests +
SkyVertexLayoutTests): 35/35. RetailPViewPassExecutorTests +
WalkFrameDriverTests (full classes, regression check on the R2-3/R2-5b
edits): 57/57.
Commits: two cherry-picks (4f12cd06b, 3a6aa7eef) from
claude/campaign-w-retail-frame-walk, both clean, plus this one fix
commit on top of 2bd353234.
Register: no new AD row; AD-119 unchanged (human read, not a test -
see R2-5d above).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
624 lines
32 KiB
C#
624 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.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>
|
|
/// 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 &&
|
|
/// 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 &&
|
|
/// 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 < 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 & 0xFFFF) < 0x100</c>. MUTATION: negating
|
|
/// the <c>< 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 -> transform -> clip -> 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>< 3</c> guard stands in for retail's post-clip
|
|
/// <c>var_4 >= 3</c> check). Round 0 dropped the count too — a
|
|
/// <c>< 3</c> continue ahead of both the boundary guard and the
|
|
/// count — never observed on authored dat data (every real portal
|
|
/// polygon has >= 3 vertices) but the wrong order all the same.
|
|
/// MUTATION (verified, S4-c1 fix round 2 R2-5b): restore the old
|
|
/// <c>localVertices.Length < 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);
|
|
var diagnostics = new WorldRenderDiagnostics(new NeverCalledGlStateReader(), new NeverCalledDiagnosticLog());
|
|
|
|
// DrawPortalDepthWrite only reads _portalDepthMask, frame.Cells,
|
|
// frame.RootCell.IsOutdoorNode, frame.ViewProjection, and
|
|
// _diagnostics (short-circuited off by RenderingDiagnostics.
|
|
// ProbeSeamDrawEnabled's default-off value) — 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);
|
|
typeof(RetailPViewPassExecutor)
|
|
.GetField("_diagnostics", BindingFlags.NonPublic | BindingFlags.Instance)!
|
|
.SetValue(executor, diagnostics);
|
|
|
|
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 sealed class NeverCalledGlStateReader : IRenderGlStateReader
|
|
{
|
|
public RenderGlStateSnapshot CaptureState() =>
|
|
throw new InvalidOperationException("EmitSeamMask must short-circuit before reading GL state.");
|
|
|
|
public RenderGlScissorSnapshot CaptureScissor() =>
|
|
throw new InvalidOperationException("EmitSeamMask must short-circuit before reading GL state.");
|
|
}
|
|
|
|
private sealed class NeverCalledDiagnosticLog : IRenderFrameDiagnosticLog
|
|
{
|
|
public void WriteLine(string message) =>
|
|
throw new InvalidOperationException("EmitSeamMask must short-circuit before logging (ProbeSeamDrawEnabled defaults off).");
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|