Round-1 three-lens FAIL at d60ca4ea0 found: the K2 fix was right in
production but nothing pinned the draw side (restoring the pre-fix gate
left every lane green); a stale "still ends an active scissor" comment
asserted a mechanism the same round had already deleted; the sky.vert
comment claimed retail clips the sky when it draws unclipped; the K6
pin ran through ClipFrameAssembler.Assemble, which has zero production
callers, so it proved nothing about the producer that actually runs.
This round closes L1-L9.
L1 (BLOCKING) — draw-side weather-gate pin. Added
RetailPViewPassExecutorTests.DrawLandscapeDynamicsPhase_GatesDrawWeatherOnceOnWalkDriverWeatherTurnFired:
reads DrawLandscapeDynamicsPhase's compiled call graph and asserts (a)
the call immediately before DrawWeatherOnce is
WalkFrameDriver.get_WeatherTurnFired and (b) exactly one
brfalse/brfalse.s branch sits between that call and the draw, jumping
forward past it. Three mutations, each shown failing:
M1 (restore `if (clipAssembly.OutsideViewSlices.Length != 0)`):
Assert.Equal() Failure: Values differ
Expected: typeof(AcDream.App.Rendering.Walk.WalkFrameDriver)
Actual: typeof(AcDream.App.Rendering.ClipFrameAssembly)
M2 (drop the gate, unconditional call):
Assert.Equal() Failure: Values differ
Expected: typeof(AcDream.App.Rendering.Walk.WalkFrameDriver)
Actual: typeof(AcDream.App.Rendering.RetailPViewPassExecutor)
M3 (invert to `if (!walkDriver.WeatherTurnFired)`):
Assert.Single() Failure: The collection did not contain any matching items
Expected: (predicate expression)
Collection: [CompiledBranch { Offset = 7, OpCode = brfalse.s, TargetOffset = 17 }, CompiledBranch { Offset = 24, OpCode = brtrue.s, TargetOffset = 33 }]
Corrected WalkFrameDriverTranscriptTests.cs's doc comment (the
Collect_WeatherTurnFiredMatchesThePrintedOcLineExactly block): it pins
the print half and the flag only, and now names the real draw-side pin
instead of falsely claiming to double as one.
L2 (MAJOR) — scissor stack deleted, K4 comment corrected. Deleted
IWorldPassSurface.BeginScissor/EndScissor and their RhiWorldPassSurface
bodies, the scissor call inside ClearInteriorDepth, NdcScissorRect.cs +
NdcScissorRectTests.cs (BeginScissor had zero remaining callers, and
RhiWorldPassSurface.BeginScissor was NdcScissorRect.ToPixels' only
production caller). This left RhiWorldPassSurface's 4th constructor
parameter (IRetailPViewFramebufferSource) and RetailPViewPassExecutor.cs's
RetailPViewFramebufferSize/IRetailPViewFramebufferSource/
SilkRetailPViewFramebufferSource types entirely dead (framebuffer size
was needed only for the scissor's NDC-to-pixel conversion) — removed
them and their one call site in FrameRootComposition.cs. Rewrote
RetailPViewPassExecutor.cs's K4 comment: it no longer asserts a live
scissor mechanism; it states that VulkanGpuPassEncoder's constructor
sets the full-attachment scissor once, at pass begin
(VulkanGpuPassEncoder.cs:87), and nothing narrows it after that. KEPT
per the round-1 verdict: ScissorFallbacks, OutsidePlaneCount,
OutsideViewNdcAabb, HasOutsideView, OutdoorVisible, OutdoorSlot,
ClipViewSlice.NdcAabb (the >8-edge zero-plane slice case the punch fans
still consume) and the VulkanViewportMapping.ScissorToVulkan pass-begin
path (IGpuPassEncoder.SetScissor stays). Also deleted
ClipFrameAssembly.TerrainMode/TerrainScissorNdcAabb and their writers
in ClipFrameAssembler.cs (both Assemble and ReassembleOutsideViewFromWalk);
OutsidePlaneCount's formula reduces to `outsideHasScissorFallback ? 0 :
outsideMaxPlaneCount` without needing the deleted TerrainClipMode
comparison (a scissor-fallback slice can only exist when outdoorVisible
is already true, so the three original cases collapse identically).
Deleted the two "terrain=" / "outMode=" diagnostic reads in
WorldRenderDiagnostics.cs (:345, :409) and the now-vestigial
EmitClipRouteProbe "ubo: n=..." segment that read ClipFrame's deleted
terrain bytes.
DEVIATION from L2's literal text: kept the TerrainClipMode enum type
itself — WorldSceneRenderer.cs's flat-world safety path (explicitly
"out of this chunk's scope" per K4's own round-1 comment) still uses it
for an unrelated "did the flat terrain draw" diagnostic flag, with its
own WorldSceneDiagnosticsController/WorldRenderDiagnostics/test
consumers. Deleting the type would require rewriting files outside
every round's declared file list. Reworded its doc comment to state
this plainly.
L3 (MAJOR, completes K3) — dead TerrainClip UBO deleted from the sky
and terrain shaders. Deleted the block, both gl_ClipDistance loops, and
the gl_PerVertex redeclaration (nothing else needs it) from sky.vert,
terrain_modern.vert, terrain_atmospheric.vert. Recompiled via
tools/compile-shaders.ps1 (glslc via the Vulkan SDK, managed shaderc
fallback also runs) — 24/24 pairs compiled; only sky.vert.spv,
terrain_atmospheric.vert.spv and terrain_modern.vert.spv changed.
Re-pinned VulkanShaderManifestTests.cs's frozen retail-oracle hashes
for sky.vert.spv (7d67a9e3624d198b370d402b5c12e4ce925bf9b8e646ef5123636a86d5985ab5)
and terrain_modern.vert.spv (8a73d89ef0e51e550327b9ff8c24857e309103b1d491030cf0d4d8594b45068c)
with dated comments, matching the existing re-pin convention.
Deleted WorldFrameSectionBinding.BindTerrainClip and
WorldFrameSections.TerrainClip (+ its Reset) with its two callers
(SkyRenderer.Rhi.cs:258, TerrainModernRenderer.Rhi.cs:259). DEVIATION
from L3's literal text: kept the shared Zeroed(...) helper in
WorldPassScope.cs — it is also called by BindSceneLighting and
BindClipRegions, both of which stay; deleting it would have broken
those two live bindings. Deleted IWorldPassSurface.BindTerrainClip
(no-op interface method + RhiWorldPassSurface body) and
WorldScenePassExecutor.cs's two calls to it (:121, :235).
Deleted ClipFrame's _terrainBytes field, TerrainBytes/TerrainBytesForTest
properties, Reset's Array.Clear(_terrainBytes), and the stale
header/K3 comment paragraphs. DEVIATION from L3's literal text: kept
ClipFrame.TerrainUboBytes and ClipFrame.TerrainClipUboBinding —
PortalDepthMaskRenderer.Rhi.cs (:140, :187) is a live production
consumer of both constants for the KEPT exit-seal/punch-fan clip block,
which was never in scope for deletion (portal_depth.vert's own
TerrainClip UBO declaration is the KEEP block, untouched). Reworded
both constants' doc comments (ClipFrame.cs, VulkanPipelineLayouts.cs's
UniformTerrainClip) to say only the portal-depth clip block uses
binding 2 now, and corrected the two GpuBindingModel.cs comments that
explained why terrain-tiling/sky-params bindings are 3/4 by naming "the
terrain clip block". Updated VulkanShaderDescriptorContractTests.cs's
TerrainVertexShaderDeclaresItsClipBlockInTheUniformSet (renamed
TerrainVertexShaderDeclaresOnlySceneLightingInTheUniformSet):
terrain_modern.vert's uniform set is now {SceneLighting} only. Deleted
ClipFrameLayoutTests.NoClip_TerrainBytes_Count0_AllZeros and its K3
comment; corrected the class doc comment and LayoutConstants_MatchShaderStruct's
"terrain UBO"/binding-contract comments to describe the portal-depth
consumer instead.
REQUIRED L3 fact (declared-but-never-bound dynamic uniform binding):
VulkanFrameBindings's constructor seeds EVERY declared uniform binding
(0..UniformBindingCount-1, including binding 2) with the shared dummy
buffer's range before any renderer runs (VulkanFrameBindings.cs:128-130,
`_arena.SeedUniform(binding, dummy.Handle.Handle, dummyUniformRange)`
in a loop over every binding) — "Every binding is always bound, whether
a renderer uses it or not... unused ones point at a shared dummy range"
(VulkanFrameBindings.cs:27-31). So after this round, when the sky or
terrain pipeline draws, set 1's shared descriptor layout still declares
binding 2 (portal_depth.vert's own declaration keeps
IsDeclaredUniformBinding(2) true), and its descriptor still points at a
valid (dummy) range from that seed — vkCmdBindDescriptorSets stays
legal even though neither shader statically uses binding 2 any more.
No SPIR-V-side change was needed to keep this legal.
L4 (MAJOR) — one outside-view slice constructor. Extracted
ClipFrameAssembler.AppendOutsideSlice (frame, ViewPolygon,
outsideSlicesList, ref maxPlaneCount, ref hasScissorFallback, ref
scissorFallbacks) — the exact ClipPlaneSet.From/AppendSlot/
ClipViewSlice-construction body — and call it from both Assemble's
outside_view loop and ReassembleOutsideViewFromWalk's outside_view
loop. Rewrote ClipFrameLayoutTests's K6 pin
(ClipViewSlicePlanes_PunchFanPath_EqualsCpuViewPolygonEdgePlanes_ForASyntheticView):
builds a synthetic WalkPortalView from pixel-space points (the
WalkCopyViewTests pattern) via a trivial IWalkRayCaster, runs
BeginWalkFrame(frame, outdoorRoot: false) then
ReassembleOutsideViewFromWalk(assembly, walkView, 640, 480), and reads
assembly.OutsideViewSlices[0].Planes — the exact production pair
RetailPViewPassExecutor.cs's BeginWalkFrame call and
RetailPViewRenderer.cs's ReassembleOutsideViewFromWalk call make.
Mutation (perturbed planes[0].W inside the shared helper, right after
`planes = cps.PlaneArray;`):
edge 0 midpoint (-0.3,0.10000001) should lie ~on its OWN GPU plane; the closest plane was only 0.5251073 away
The pin's own source contains no `Assemble(` call — grep-checked by
extracting the method body and searching it for the literal text; no
match.
L5 — shader comments corrected. sky.vert/terrain_modern.vert/
terrain_atmospheric.vert now state retail draws the sky once
(LScape::draw @0x00506330 -> GameSky::Draw(sky,0) @0x0050633c, before
draw_check_blocks) and the landscape is view-culled per cell, never
GPU-clipped (RenderDeviceD3D::DrawBlock @0x005a17c0). The "Phase W
Stage 4"/"U.3 default" clip narratives are deleted along with the
blocks they described.
L6 — grep sweep. `grep -rn "<token>" src tests docs/architecture`
returns nothing for SetTerrainClip, BeginDoorwayScissor,
BindTerrainClip, TerrainBytes, "active scissor", and NdcScissorRect —
confirmed after this commit (docs/plans and docs/research keep the
historical record, untouched). TerrainClipMode is the one deliberate
exception (see the L2 deviation note above); every one of its 9
remaining hits is a live, non-stale reference (the enum declaration,
WorldSceneRenderer's flat-path local, or their diagnostic/test
plumbing), not a stale mention of a deleted mechanism.
L7 — Issue130DoorwayStripTests. Deleted AnySliceAdmitsScissor,
worstScissorGapPx and its PIN 1 assertion, the header's scissor
sentences, and the scissorGap half of MeasureTopEdgeGap (dropped the
fbW parameter it alone needed). Rewrote the header: the scissor
mechanism is retired (the sky and landscape draw unclipped; aperture
exactness comes from the depth clear, the exit seals and the interior
repaint). The remaining plane-gap half (the canary PIN,
`worstPlaneGapPx <= 1.2f`) still pins something production reads:
AnySliceAdmitsPlanes walks slice.Planes from the SAME ProjectToClip ->
ClipToRegion -> ClipPlaneSet.From pipeline
RetailPViewPassExecutor.DrawWalkPunchFan reads through
clipAssembly.OutsideViewSlices[activeViewIndex].Planes — so the test
was kept, not deleted.
L8 — DrawWalkSky loop-shape pin. Added
DrawWalkSky_RenderSkyCallSiteHasNoEnclosingBackwardBranch (same
backward-branch-span shape as K1's DrawWeatherOnce pin), plus a note in
both pins that CompiledCallGraph.ReadBranches does not decode a
compiled switch jump table, but no C# loop construct compiles to one.
Mutation (wrapped the RenderSky call in `for (int i = 0; i < 2; i++)`):
Assert.DoesNotContain() Failure: Filter matched in collection
Collection: [..., CompiledBranch { Offset = 20, OpCode = brtrue.s, TargetOffset = 25 }, CompiledBranch { Offset = 23, OpCode = br.s, TargetOffset = 66 }, CompiledBranch { Offset = 72, OpCode = blt.s, TargetOffset = 13 }, CompiledBranch { Offset = 80, OpCode = brfalse.s, TargetOffset = 116 }, CompiledBranch { Offset = 88, OpCode = brfalse.s, TargetOffset = 116 }]
(the Offset=72 -> TargetOffset=13 entry is the injected loop's backward
branch spanning the call).
L9 — this message.
Gates: dotnet build 0 warnings / 0 errors. Hermetic filter
(Lane!=InstalledDat&...&Status!=KnownFailure): 6827 passed, 0 failed.
InstalledDat lane against a real DAT directory: 244 passed, 4 failed —
exactly the four known failures (LayoutImporterMediaBearingChildSweepTests
+ LayoutImporterInvisibleSweepTests, both #383; TowerAscentReplayTests,
KnownFailure; WalkTraceConformanceTests.Oh_doorway_still_first_frame_diff,
#458 KnownFailure). No register row added (every deletion here removes
an acdream-only rule; the KEEP items already had their equivalence
pins from round 1). No new flag/probe.
OWED: the lead's ACDREAM_DEVTOOLS=1 four-pose visual self-gate against
the running client is not run by this agent — CLAUDE.md and this
task's own instructions forbid launching the graphical client from
here; that visual/validation-layer pass remains the user's to run
before this round is accepted.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
364 lines
18 KiB
C#
364 lines
18 KiB
C#
using System.Reflection;
|
|
using System.Reflection.Emit;
|
|
using AcDream.App.Composition;
|
|
using AcDream.App.Rendering;
|
|
using AcDream.App.Rendering.Sky;
|
|
using AcDream.App.Rendering.Walk;
|
|
using AcDream.App.Tests.Architecture;
|
|
|
|
namespace AcDream.App.Tests.Rendering;
|
|
|
|
public sealed class RetailPViewPassExecutorTests
|
|
{
|
|
[Fact]
|
|
public void Extracted_contracts_retain_no_window_callbacks_or_visibility_owner()
|
|
{
|
|
Assert.DoesNotContain(
|
|
typeof(RetailPViewFrameInput).GetProperties(),
|
|
property => typeof(Delegate).IsAssignableFrom(property.PropertyType));
|
|
|
|
FieldInfo[] fields = typeof(RetailPViewPassExecutor).GetFields(
|
|
BindingFlags.Instance | BindingFlags.NonPublic);
|
|
Assert.DoesNotContain(fields, field => field.FieldType == typeof(GameWindow));
|
|
Assert.DoesNotContain(fields, field => field.FieldType == typeof(CellVisibility));
|
|
Assert.DoesNotContain(fields, field => field.FieldType == typeof(RetailPViewFrameInput));
|
|
Assert.DoesNotContain(fields, field => field.FieldType == typeof(RetailPViewFrameResult));
|
|
Assert.DoesNotContain(fields, field => field.FieldType == typeof(ClipFrameAssembly));
|
|
Assert.DoesNotContain(
|
|
fields,
|
|
field => typeof(Delegate).IsAssignableFrom(field.FieldType));
|
|
}
|
|
|
|
[Fact]
|
|
public void Concrete_executor_accumulates_walk_terrain_batch_timing()
|
|
{
|
|
// S3 chunk 3 fix round 1 (F3): the walk leaf no longer brackets
|
|
// itself with Begin()/Complete() (that stopwatch-restart pair would
|
|
// push one timing SAMPLE per batch, not one per frame) — it times
|
|
// itself with a raw Stopwatch.GetTimestamp() delta and hands the
|
|
// elapsed ticks to AccumulateWalkBatch, which only accumulates.
|
|
MethodInfo landscape = typeof(RetailPViewPassExecutor).GetMethod(
|
|
"DrawWalkLandCellBatch",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
|
IReadOnlyList<CompiledCall> landscapeCalls = CompiledCallGraph.Read(landscape);
|
|
int terrainDraw = RequiredCallIndex(
|
|
landscapeCalls,
|
|
typeof(TerrainModernRenderer),
|
|
nameof(TerrainModernRenderer.DrawLandCells));
|
|
int accumulate = RequiredCallIndex(
|
|
landscapeCalls,
|
|
typeof(TerrainDrawDiagnosticsController),
|
|
nameof(TerrainDrawDiagnosticsController.AccumulateWalkBatch));
|
|
|
|
Assert.True(terrainDraw < accumulate);
|
|
Assert.DoesNotContain(
|
|
landscapeCalls,
|
|
call => call.Target.DeclaringType == typeof(TerrainDrawDiagnosticsController)
|
|
&& call.Target.Name == nameof(TerrainDrawDiagnosticsController.Begin));
|
|
Assert.DoesNotContain(
|
|
landscapeCalls,
|
|
call => call.Target.DeclaringType == typeof(TerrainDrawDiagnosticsController)
|
|
&& call.Target.Name == nameof(TerrainDrawDiagnosticsController.Complete));
|
|
}
|
|
|
|
[Fact]
|
|
public void Concrete_executor_pushes_the_walk_terrain_frame_sample_at_replay_end()
|
|
{
|
|
// S3 chunk 3 fix round 1 (F3): DrawWalkDrivenStatics is the ONE call
|
|
// site of driver.Replay in production — CompleteWalkTerrainFrame
|
|
// must run immediately after it, so the frame's sample is pushed
|
|
// exactly once, at "the end of the walk replay".
|
|
MethodInfo drawWalkDrivenStatics = typeof(RetailPViewRenderer).GetMethod(
|
|
"DrawWalkDrivenStatics",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(drawWalkDrivenStatics);
|
|
int replay = RequiredCallIndex(
|
|
calls,
|
|
typeof(AcDream.App.Rendering.Walk.WalkFrameDriver),
|
|
nameof(AcDream.App.Rendering.Walk.WalkFrameDriver.Replay));
|
|
int completeWalkFrame = RequiredCallIndex(
|
|
calls,
|
|
typeof(RetailPViewPassExecutor),
|
|
nameof(RetailPViewPassExecutor.CompleteWalkTerrainFrame));
|
|
|
|
Assert.True(replay < completeWalkFrame);
|
|
}
|
|
|
|
[Fact]
|
|
public void Frame_composition_constructs_one_walk_executor()
|
|
{
|
|
MethodInfo compose = typeof(FrameRootCompositionPhase).GetMethod(
|
|
"ComposeCore",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(compose);
|
|
|
|
int executor = RequiredCallIndex(
|
|
calls,
|
|
typeof(RetailPViewPassExecutor),
|
|
".ctor");
|
|
int renderer = RequiredCallIndex(
|
|
calls,
|
|
typeof(WorldScenePViewRenderer),
|
|
".ctor");
|
|
|
|
Assert.True(executor < renderer);
|
|
Assert.Single(
|
|
calls,
|
|
call => call.Target.DeclaringType == typeof(RetailPViewPassExecutor)
|
|
&& call.Target.Name == ".ctor");
|
|
Assert.Single(
|
|
calls,
|
|
call => call.Target.DeclaringType == typeof(WorldScenePViewRenderer)
|
|
&& call.Target.Name == ".ctor");
|
|
}
|
|
|
|
/// <summary>
|
|
/// S3 chunk 1 fix round 2 (§11.6 H1): the weather MESH draw + its OC
|
|
/// print moved OUT of <see cref="RetailPViewPassExecutor.DrawWeatherOnce"/>
|
|
/// entirely — S3 chunk 4 (O3) relocated the print to
|
|
/// <c>WalkFrameDriver.OnWeatherTurn</c>, fired at Collect time by
|
|
/// <c>RetailFrameWalk.DrawLandscape</c> (see the real transcript pins in
|
|
/// <c>WalkFrameDriverTranscriptTests</c>: <c>Collect_OutdoorRoot_...</c>
|
|
/// and <c>Collect_InteriorRoot_...</c>). This method now draws the
|
|
/// weather MESH and the rain PARTICLES only — the former per-outside-
|
|
/// view-slice loop that used to run before this call (the walk's own
|
|
/// screen-space terrain-clip writer + <c>ClearClipRouting</c> + the old
|
|
/// <c>DrawLandscapeSliceLate</c> leaf) is deleted outright (§10.2): retail
|
|
/// draws the weather mesh and its
|
|
/// rain particles ONCE, unclipped, never once per doorway aperture.
|
|
/// MUTATION: re-inlining a
|
|
/// <c>WalkTranscriptDump.PrintObjectCellTurn</c> call back into this
|
|
/// method makes the <c>Assert.DoesNotContain</c> below fail; deleting
|
|
/// either the mesh or the particle call makes the matching
|
|
/// <c>Assert.Single</c> fail (zero matches instead of one).
|
|
/// </summary>
|
|
[Fact]
|
|
public void DrawWeatherOnce_DrawsTheWeatherMeshAndParticlesButNeverPrints()
|
|
{
|
|
MethodInfo method = typeof(RetailPViewPassExecutor).GetMethod(
|
|
nameof(RetailPViewPassExecutor.DrawWeatherOnce),
|
|
BindingFlags.Instance | BindingFlags.Public)!;
|
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(method);
|
|
|
|
Assert.Single(
|
|
calls,
|
|
call => call.Target.DeclaringType == typeof(SkyRenderer)
|
|
&& call.Target.Name == nameof(SkyRenderer.RenderWeather));
|
|
Assert.Single(
|
|
calls,
|
|
call => call.Target.DeclaringType == typeof(ParticleRenderer)
|
|
&& call.Target.Name == nameof(ParticleRenderer.Draw));
|
|
Assert.DoesNotContain(
|
|
calls,
|
|
call => call.Target.DeclaringType == typeof(WalkTranscriptDump));
|
|
}
|
|
|
|
/// <summary>
|
|
/// S3 chunk 4 (§10.2): the former per-outside-view-slice loop
|
|
/// (the walk's own screen-space terrain-clip writer +
|
|
/// <c>ClearClipRouting</c> + the old <c>DrawLandscapeSliceLate</c> leaf,
|
|
/// one call per active landscape view) is deleted — <c>DrawLandscapeDynamicsPhase</c> now calls
|
|
/// <see cref="RetailPViewPassExecutor.DrawWeatherOnce"/> exactly once,
|
|
/// unconditionally, with no loop of any kind around it. This
|
|
/// <c>Assert.Single</c> alone proved insufficient at fix round 1 (K1):
|
|
/// it counts DISTINCT call-site offsets, so it stays green even with a
|
|
/// <c>foreach</c> wrapped around the one call site (the exact round-1
|
|
/// regression this file's review caught) — see
|
|
/// <see cref="DrawLandscapeDynamicsPhase_DrawWeatherOnceCallSiteHasNoEnclosingBackwardBranch"/>
|
|
/// for the pin that actually rules that out. Kept as a cheap first-line
|
|
/// check: MUTATION: adding a second, textually distinct call site (e.g.
|
|
/// a duplicated call, not a loop) makes <c>Assert.Single</c> fail.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DrawLandscapeDynamicsPhase_CallsDrawWeatherOnceExactlyOnce()
|
|
{
|
|
MethodInfo method = typeof(RetailPViewRenderer).GetMethod(
|
|
"DrawLandscapeDynamicsPhase",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(method);
|
|
|
|
Assert.Single(
|
|
calls,
|
|
call => call.Target.DeclaringType == typeof(RetailPViewPassExecutor)
|
|
&& call.Target.Name == nameof(RetailPViewPassExecutor.DrawWeatherOnce));
|
|
}
|
|
|
|
/// <summary>
|
|
/// S3 chunk 4 fix round 1 (K1, blocking): the real loop-shape pin.
|
|
/// <see cref="DrawLandscapeDynamicsPhase_CallsDrawWeatherOnceExactlyOnce"/>'s
|
|
/// <c>Assert.Single</c> over call-site offsets still passes when the ONE
|
|
/// call site sits inside a <c>foreach</c> — an IL-offset ORDER pin has
|
|
/// now failed three times to be discriminating for this exact class of
|
|
/// regression, so this asks the LOOP-SHAPE question directly: does any
|
|
/// BACKWARD branch (a branch whose target offset is lower than its own
|
|
/// offset — the shape every C# loop compiles to, whether
|
|
/// <c>for</c>/<c>foreach</c>/<c>while</c>) enclose the
|
|
/// <c>DrawWeatherOnce</c> call's own IL offset? A call sitting strictly
|
|
/// between a backward branch's target and its own offset is inside that
|
|
/// loop's body and can run more than once per method invocation; a call
|
|
/// outside every backward branch's span cannot. MUTATION: wrap the call
|
|
/// in <c>foreach (var slice in clipAssembly.OutsideViewSlices)</c> —
|
|
/// the compiled <c>foreach</c> emits a backward branch (the
|
|
/// condition-check jump back to the loop body) whose span now contains
|
|
/// the call's offset, so this test fails; restore the single
|
|
/// unconditional call to make it pass again.
|
|
/// </summary>
|
|
[Fact]
|
|
public void DrawLandscapeDynamicsPhase_DrawWeatherOnceCallSiteHasNoEnclosingBackwardBranch()
|
|
{
|
|
MethodInfo method = typeof(RetailPViewRenderer).GetMethod(
|
|
"DrawLandscapeDynamicsPhase",
|
|
BindingFlags.Instance | BindingFlags.NonPublic)!;
|
|
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(method);
|
|
int callIndex = RequiredCallIndex(
|
|
calls,
|
|
typeof(RetailPViewPassExecutor),
|
|
nameof(RetailPViewPassExecutor.DrawWeatherOnce));
|
|
int callOffset = calls[callIndex].Offset;
|
|
|
|
IReadOnlyList<CompiledBranch> branches = CompiledCallGraph.ReadBranches(method);
|
|
Assert.DoesNotContain(
|
|
branches,
|
|
branch => branch.TargetOffset < branch.Offset
|
|
&& branch.TargetOffset <= callOffset
|
|
&& callOffset < branch.Offset);
|
|
}
|
|
|
|
/// <summary>
|
|
/// S3 chunk 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 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));
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|