Three-lens review of c7ab5b6d8 found the round-0 port of
DrawPortalPolyInternal's degenerate-input guard inverted retail's
quantifier: it rejected a polygon when ANY vertex sat on ANY of the
four +/-12 planes. The PDB-paired bytes (VA 0x59BCD6-0x59BD66: four
per-plane fld/fcomp 12.0/fnstsw/test ah,0x44/jnp sequences, each
accumulating a "every vertex on this plane" boolean, cleared by the
first vertex NOT on it) and the named decomp's own nested-if reading
of the post-loop branch (0x59bd42-0x59bd6c) agree: retail rejects only
when EVERY local vertex lies on the SAME one of the four planes — a
polygon degenerate onto a plane, not one that merely touches a plane
at one vertex.
F1 — the quantifier. WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard
now tracks four booleans (everyVertexOnPlusX/MinusX/PlusY/MinusY), each
starting true and cleared by the first vertex that fails its own plane
test; the guard rejects iff any of the four survives the whole vertex
loop. T2 (WalkVisibilityMathTests.cs) is rewritten to the five §7
cases: every vertex on x=+12 -> rejected; every vertex on y=-12 ->
rejected; one vertex on x=+12, rest inside -> KEPT; every vertex at
11.999 -> kept; vertices split across x=+12 and y=+12 (no common
plane) -> kept. WalkFrameDriverTests' punch-side pin
(OnPunchGeometry_RejectsOnlyWhenEveryVertexSharesOnePlane_ButPunchesAnyOtherShape)
carries the same correction — its old "one vertex on the plane"
polygon was wrongly rejected under round 0; it punches now, and a new
all-on-plane polygon is added as the actual rejected case. The doc
comment's "essentially never" sentence is deleted and replaced with a
fresh DAT-scan citation (see datScan below).
MUTATION (verified): restore the any-vertex form -> the one-vertex and
split-plane admit cases fail (Assert.False Expected:False Actual:True,
x2; the WalkFrameDriverTests punch pin's Assert.Equal Expected:2
Actual:1). Delete the guard (always return false) -> the all-on-plane
reject cases fail (Assert.True Expected:True Actual:False, x3 —
plusX-plane, minusY-plane, and the vacuous-empty-polygon pin; the
punch pin's Assert.Equal Expected:2 Actual:3).
datScan (DAT counts backing F1's doc comment): a throwaway xunit test
(deleted after this commit — not part of the diff) walked the full
0x0000-0xFFFF landblock-prefix space (LandBlockInfo/EnvCell carry no
usable DatCollection.GetAllIdsOfType range — both are landblock-
relative — matching the method CellStructSurfaceConstructionInstalledDatTests
already established), resolved each EnvCell's Environment/CellStruct,
and for every CellPortal's polygon tested each vertex's local x/y
against +/-12. Against the installed client_cell_1.dat: 3,405
landblocks with cells, 1,854,237 total portal polygons. 2,889 polygons
have at least one vertex on a +/-12 plane; 2,163 have EVERY vertex on
the SAME plane — all 2,163 are EXIT portals (OtherCellId==0xFFFF, out
of 16,939 exit portals total); 0 interior (non-exit) portals ever
qualify (interior: 1,837,298 total, 340 any-vertex, 0 all-on-plane).
Duration ~15s. This exactly reproduces the review's cited numbers.
F2 — seal-path count order. RetailPViewPassExecutor's exit-seal loop
dropped the `localVertices.Length < 3` pre-filter that ran BEFORE both
the boundary guard and the `submitted` increment (round 0's order:
length check -> guard -> transform -> draw -> count). Retail's real
order is guard -> transform -> clip -> count, with the counter
incremented BEFORE polyClipFinish runs (0x59BD70-0x59BD74 precedes
0x59BDB0) — the counter records accepted ATTEMPTS, not successful GPU
fans. `submitted++` now runs immediately after the guard-gated
transform+EmitSeamMask, before DrawDepthFan (whose own `< 3` guard is
this port's stand-in for retail's post-clip `var_4 >= 3` check). A new
pin (RetailPViewPassExecutorTests.DrawExitPortalMask_CountsAnUnclippableTwoVertexPolygon_ButDrawsNothing)
constructs a real PortalDepthMaskRenderer (RecordingGpuDevice-backed)
and a constructor-bypassed executor (RuntimeHelpers.GetUninitializedObject,
the same pattern WalkOutsideViewReassemblyTests already uses) with a
2-vertex ordinary-coordinate portal polygon: DrawExitPortalMask returns
submitted=1 while zero draw calls reach the device.
MUTATION (verified): restore the `< 3` pre-filter ahead of the guard
-> the pin's Assert.Equal fails, Expected:1 Actual:0.
F3 — the depth-event transcript gate, automated. New
WalkAlphaDepthTrace.cs (beside WalkOracleTrace.cs) parses the four
`PM poly=<ptr> mode=<0|1> counterBefore=<hex>` / `PC ov=<n>
counter=<hex> fc=<0|1>` lines per oh-capture/*.alphadepth.log frame,
using the same F-marker frame delimiting as WalkOracleTrace. New
WalkTraceConformanceTests.AlphaDepthTranscript.cs (beside
WalkTraceConformanceTests.cs, which is now `partial`) replays each
pose's frame 2 through the real WalkFrameDriver/RetailFrameWalk over
WalkLandscapeDatBuilder-built installed-DAT world data, with:
- AlphaDepthCollectSink: an IWalkEventSink decorator forwarding every
hook to the real driver unchanged, while recording mode=1 punch
events (OnPunchGeometry, counterBefore = driver.PortalsDrawnCount
at COLLECT time — Collect finishes entirely before Replay starts,
so reading the counter at Replay time would already see the SAME
turn's post-reset value; holtburg-doorway-still's own capture
proves this: its punches show counterBefore=2 immediately followed
by its seals' counterBefore=0/1 in the identical turn) and every
PC tuple (Emit, WalkEventKind.DrawCells — fires for the root's own
turn AND every building look-in's re-entrant call, matching
retail's breakpoint not distinguishing them).
- AlphaDepthLeaf: an IWalkFrameLeafRenderer whose DrawExitSeals
reproduces DrawPortalDepthWrite's real per-portal loop (F1's guard,
F2's count-before-clip order) directly over
driver.InteriorFloodCells / WalkCell.Portals / .PortalPolygons —
the identical data the render-side production path reads — since
the interface gives DrawExitSeals no cell/portal parameter at all.
- Two Collect+Replay passes per pose (a fresh driver reaches the SAME
steady-state a running session already carries by the time
retail's capture began — holtburg-doorway-still and foundry-deep
both show a stable per-frame accepted-seal count that regenerates
identically every captured frame): the first pass's own output is
discarded, only the second is compared.
Five Facts (one per pose — the spec's "four" undercounts the five
*.alphadepth.log files actually present in oh-capture/):
AlphaDepthTranscript_CathedralArrival_MatchesRetailFrame2 — PASS
AlphaDepthTranscript_CathedralLeak_MatchesRetailFrame2 — PASS
AlphaDepthTranscript_FoundryDeep_MatchesRetailFrame2 — PASS
AlphaDepthTranscript_HoltburgDoorwayStill_MatchesRetailFrame2 — PASS
(proves the incrementing 0/1/2 accepted-seal sequence AND the
pre-reset counterBefore=2 punches, end to end, against real DAT
portal data)
AlphaDepthTranscript_TerraceEdge_MatchesRetailFrame2 — KnownFailure
(tagged [Trait("Status","KnownFailure")]): terrace-edge never runs
a qualifying interior turn in its own capture (every PC line reads
ov=0) — its punches' counterBefore=2 is a session value carried
over from BEFORE the capture even started (the file's own pre-"F 1"
content already reads 2; nothing inside the captured frames ever
touches the counter). A fresh two-pass replay from
PortalsDrawnCount=0 has no mechanism to derive that leftover value.
Printed divergence: expected [(mode=1,counterBefore=2) x2], actual
[(mode=1,counterBefore=0) x2]; the PC sequence (ov=0 both times)
matches exactly. An initial-condition gap in the fixture, not a
guard/count defect — the identical harness reproduces holtburg-
doorway-still's and foundry-deep's own steady states exactly from
cold starts.
MUTATION (verified): swap AlphaDepthLeaf's recorded seal mode from 0
to 1 -> AlphaDepthTranscript_HoltburgDoorwayStill_MatchesRetailFrame2
fails (Assert.Equal Collections differ at pos 5: expected
(0,0),(0,1)... actual (1,0),(1,1)...).
DispatcherFixture and DrawScope (WalkFrameDriverTests.cs) are widened
from private to internal so the new gate can drive a real
WbDrawDispatcher without duplicating that fixture.
F4 — manifest comment truth. VulkanShaderManifestTests.cs's
portal_depth.vert.spv re-pin comment had the ULP direction backward
(claimed the OLD 0x3F7FFFFE literal was NEARER the camera than
retail's real 0x3F7FFFEF constant; portal_depth.vert's own shader-
source comment already had this correct — FARTHER, larger z/w) and
cited T1 as living in WalkVisibilityMathTests when
PortalDepthVert_FarPunchConstant_MatchesRetailExactBits is in this
same file. Both corrected.
Gates: dotnet build tests/AcDream.App.Tests -c Release -> 0 warnings/0
errors. Hermetic lane (Lane!=InstalledDat&...&Status!=KnownFailure):
6829/6829 passed. InstalledDat lane (real client_cell_1.dat): 253
total, 247 passed, 5 failed — exactly the two #383 layout tests
(LayoutImporterMediaBearingChildSweepTests/LayoutImporterInvisibleSweepTests),
TowerAscentReplayTests' KnownFailure, Oh_doorway_still_first_frame_diff
(#458), and this round's own AlphaDepthTranscript_TerraceEdge
KnownFailure — no other divergence. Shader tests (VulkanShaderDescriptorContractTests
+ VulkanShaderManifestTests + RenderPackSpirvValidatorTests +
SkyVertexLayoutTests): 35/35. Register/divergence tests: 52/52.
dotnet build src/AcDream.App -c Release: 0 warnings/0 errors.
Register: no new AD row. F1/F2 are straight bug fixes toward retail
fidelity (not deviations); AD-119 (the C2 blend-vs-write-mask row)
does not describe the count order, so its neighbour needed no
correction per F2's own instruction.
Deviation from the fix-round spec's literal text: §7 F1 said "swap the
any-vertex hits instead of the four per-plane accumulators" as the
split-plane case's mutation — the code actually ported uses per-vertex
`if (x != 12f) flag = false;` clears, not a per-vertex OR; the mutation
verified above (restore the any-vertex FORM entirely) exercises the
same defect class and is the mutation actually run. §7 also names
"four" alphadepth.log fixtures where five exist on disk
(cathedral-arrival, cathedral-leak, foundry-deep,
holtburg-doorway-still, terrace-edge); all five are gated rather than
four, with cathedral-leak an unremarked addition alongside the four
the spec names.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
617 lines
31 KiB
C#
617 lines
31 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: move the increment back to AFTER
|
|
/// <see cref="PortalDepthMaskRenderer.DrawDepthFan"/> (or restore the
|
|
/// old <c>localVertices.Length < 3</c> pre-filter) — this pin's
|
|
/// synthetic 2-vertex polygon is no longer counted and the assertion
|
|
/// fails (<c>submitted</c> comes back 0, not 1).
|
|
/// </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;
|
|
}
|
|
}
|