fix(render): S4 chunk 1 round 1 — the ±12 guard with retail's quantifier, seal count order, the PM/PC depth-event transcript gate
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>
This commit is contained in:
parent
d1e3e64f61
commit
5de42a12f3
9 changed files with 747 additions and 96 deletions
|
|
@ -394,16 +394,16 @@ public RetailPViewPassExecutor(
|
||||||
if (index >= cell.PortalPolygons.Count)
|
if (index >= cell.PortalPolygons.Count)
|
||||||
break;
|
break;
|
||||||
Vector3[] localVertices = cell.PortalPolygons[index];
|
Vector3[] localVertices = cell.PortalPolygons[index];
|
||||||
if (localVertices.Length < 3)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
// S4-c1 C1: DrawPortalPolyInternal's degenerate-input guard
|
// S4-c1 C1 (fix round 1 F1): DrawPortalPolyInternal's
|
||||||
|
// degenerate-input guard
|
||||||
// (WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard's
|
// (WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard's
|
||||||
// own doc comment) — tested on the LOCAL portal-polygon
|
// own doc comment) — tested on the LOCAL portal-polygon
|
||||||
// vertices, BEFORE the world-transform loop below. A hit drops
|
// vertices, BEFORE the world-transform loop below. A hit drops
|
||||||
// the whole polygon: no transform, no fan submission, no
|
// the whole polygon: no transform, no fan submission, no
|
||||||
// `submitted` increment (retail's reject -> transform -> clip
|
// `submitted` increment (retail's reject -> transform -> clip
|
||||||
// -> count order).
|
// -> count order). No length pre-filter runs before this any
|
||||||
|
// more (fix round 1 F2 — see the count comment below).
|
||||||
if (WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard(localVertices))
|
if (WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard(localVertices))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
|
@ -425,12 +425,25 @@ public RetailPViewPassExecutor(
|
||||||
forceFarZ,
|
forceFarZ,
|
||||||
world[..count]);
|
world[..count]);
|
||||||
|
|
||||||
|
// S4-c1 fix round 1 F2: retail increments portalsDrawnCount
|
||||||
|
// (0x59BD70-0x59BD74) BEFORE polyClipFinish runs (0x59BDB0) —
|
||||||
|
// the counter records accepted ATTEMPTS, not successful GPU
|
||||||
|
// fans (oh1-depth-lifecycle.md's arbitration table). A polygon
|
||||||
|
// with fewer than 3 vertices is counted here even though
|
||||||
|
// DrawDepthFan below draws nothing — its own `< 3` guard is
|
||||||
|
// this port's stand-in for retail's post-clip `var_4 >= 3`
|
||||||
|
// check. Round 0 counted AFTER that guard (a `< 3` continue
|
||||||
|
// ahead of both the boundary guard and this increment), which
|
||||||
|
// silently dropped the count for such a polygon — never
|
||||||
|
// observed on authored dat data (every real portal polygon has
|
||||||
|
// >= 3 vertices) but wrong order all the same.
|
||||||
|
submitted++;
|
||||||
|
|
||||||
_portalDepthMask.DrawDepthFan(
|
_portalDepthMask.DrawDepthFan(
|
||||||
world[..count],
|
world[..count],
|
||||||
frame.ViewProjection,
|
frame.ViewProjection,
|
||||||
clipPlanes,
|
clipPlanes,
|
||||||
forceFarZ);
|
forceFarZ);
|
||||||
submitted++;
|
|
||||||
}
|
}
|
||||||
return submitted;
|
return submitted;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -203,33 +203,60 @@ public static class WalkVisibilityMath
|
||||||
/// degenerate-input guard (Ghidra-arbitrated:
|
/// degenerate-input guard (Ghidra-arbitrated:
|
||||||
/// docs/research/2026-09-01-overhaul/oh1-depth-lifecycle.md's "Ghidra
|
/// docs/research/2026-09-01-overhaul/oh1-depth-lifecycle.md's "Ghidra
|
||||||
/// branch arbitration table", row <c>0x59BCD6–0x59BD28</c> then
|
/// branch arbitration table", row <c>0x59BCD6–0x59BD28</c> then
|
||||||
/// <c>0x59BD40–0x59BD66</c> — S4-c1 C1). The pseudo-C's own nested-if
|
/// <c>0x59BD40–0x59BD66</c> — S4-c1 C1, QUANTIFIER corrected at fix
|
||||||
/// reading of the four x87 FCOM results (via <c>test ah, 0x44</c>
|
/// round 1 F1: the round-0 "any vertex on any plane" reading was
|
||||||
/// against a Binary Ninja-synthesized condition byte) is FPU-flag
|
/// inverted). PDB-paired bytes VA <c>0x59BCD6–0x59BD66</c>: four
|
||||||
/// ambiguous and reads backward if taken at face value; the arbitration
|
/// per-plane <c>fld</c>/<c>fcomp 12.0</c>/<c>fnstsw</c>/
|
||||||
/// table's sense governs — per <c>feedback_bn_decomp_field_names.md</c>,
|
/// <c>test ah,0x44</c>/<c>jnp</c> sequences accumulate four booleans —
|
||||||
/// a decompiler's flag-mush around x87 compares is a known artifact
|
/// <c>ebx</c> ("every vertex at local x == +12"), <c>var_7</c> (x ==
|
||||||
/// class, not semantics.
|
/// -12), <c>var_5</c> (y == +12), <c>var_6</c> (y == -12) — each
|
||||||
|
/// initialized true and cleared to false by the FIRST vertex that fails
|
||||||
|
/// its own plane test; the polygon draws only if <c>ebx==0 &&
|
||||||
|
/// var_7==0 && var_6==0 && var_5==0</c> (pseudo-C
|
||||||
|
/// 0x59bd42-0x59bd6c), i.e. it is REJECTED only when EVERY local vertex
|
||||||
|
/// lies on the SAME one of the four bounding planes — a polygon
|
||||||
|
/// degenerate onto a plane, not a polygon that merely touches one at a
|
||||||
|
/// single vertex.
|
||||||
///
|
///
|
||||||
/// <para>Retail tests every SOURCE vertex's LOCAL x and y — BEFORE
|
/// <para>Retail tests every SOURCE vertex's LOCAL x and y — BEFORE
|
||||||
/// <c>xformStart</c>, the world transform — against exactly <c>+12</c>
|
/// <c>xformStart</c>, the world transform — against exactly <c>+12</c>
|
||||||
/// and <c>-12</c>. ANY hit on ANY vertex rejects the WHOLE polygon: no
|
/// and <c>-12</c>. Retail's order is reject → transform → clip → count
|
||||||
/// transform, no clip, no <c>portalsDrawnCount</c> increment (retail's
|
/// (no transform, no clip, no <c>portalsDrawnCount</c> increment on a
|
||||||
/// order is reject → transform → clip → count). Ordinary authored dat
|
/// reject). A scan of the installed cell DAT (full 0x0000-0xFFFF
|
||||||
/// portal polygons essentially never land a vertex on that exact
|
/// landblock-prefix walk, 3,405 landblocks with cells, 1,854,237 portal
|
||||||
/// boundary — this is a degenerate-input guard, not a clip rule — but
|
/// polygons; S4-c1 fix round 1 F1) found 2,889 polygons with at least
|
||||||
/// it is retail's code, so it is ported as retail's code.</para>
|
/// one vertex on a +/-12 plane and 2,163 polygons with EVERY vertex on
|
||||||
|
/// the SAME plane — all 2,163 are EXIT portals (<c>OtherCellId ==
|
||||||
|
/// 0xFFFF</c>; 0 interior portals qualify), out of 16,939 exit portals
|
||||||
|
/// total. This guard therefore rejects real content: retail never
|
||||||
|
/// punches or seals those 2,163 exit portals, which is very likely the
|
||||||
|
/// "never-drawn portal polygon = panel" mechanism the PV campaign named
|
||||||
|
/// (#456).</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static bool IsRejectedByPortalPolygonBoundaryGuard(ReadOnlySpan<Vector3> localVertices)
|
public static bool IsRejectedByPortalPolygonBoundaryGuard(ReadOnlySpan<Vector3> localVertices)
|
||||||
{
|
{
|
||||||
|
// Four accumulating per-plane predicates (ebx/var_7/var_5/var_6):
|
||||||
|
// each starts true and is cleared by the first vertex NOT on that
|
||||||
|
// plane. An empty polygon (retail's num_pts>0 wrap around the whole
|
||||||
|
// predicate+count block) leaves every predicate vacuously true —
|
||||||
|
// rejected, matching retail's "nothing happens for num_pts==0".
|
||||||
|
bool everyVertexOnPlusX = true;
|
||||||
|
bool everyVertexOnMinusX = true;
|
||||||
|
bool everyVertexOnPlusY = true;
|
||||||
|
bool everyVertexOnMinusY = true;
|
||||||
|
|
||||||
for (int i = 0; i < localVertices.Length; i++)
|
for (int i = 0; i < localVertices.Length; i++)
|
||||||
{
|
{
|
||||||
float x = localVertices[i].X;
|
float x = localVertices[i].X;
|
||||||
float y = localVertices[i].Y;
|
float y = localVertices[i].Y;
|
||||||
if (x == 12f || x == -12f || y == 12f || y == -12f)
|
if (x != 12f) everyVertexOnPlusX = false;
|
||||||
return true;
|
if (x != -12f) everyVertexOnMinusX = false;
|
||||||
|
if (y != 12f) everyVertexOnPlusY = false;
|
||||||
|
if (y != -12f) everyVertexOnMinusY = false;
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
|
return everyVertexOnPlusX || everyVertexOnMinusX
|
||||||
|
|| everyVertexOnPlusY || everyVertexOnMinusY;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -77,8 +77,10 @@ public sealed class VulkanShaderManifestTests
|
||||||
// retail's EXACT bits (uintBitsToFloat(0x3F7FFFEFu),
|
// retail's EXACT bits (uintBitsToFloat(0x3F7FFFEFu),
|
||||||
// D3DPolyRender::DrawPortalPolyInternal @0x0059bc90's tail) instead of
|
// D3DPolyRender::DrawPortalPolyInternal @0x0059bc90's tail) instead of
|
||||||
// the decimal literal 0.99999988f, which round-tripped to a DIFFERENT
|
// the decimal literal 0.99999988f, which round-tripped to a DIFFERENT
|
||||||
// bit pattern (0x3F7FFFFE, fifteen ULPs nearer the camera than retail's
|
// bit pattern (0x3F7FFFFE — FARTHER from the camera, larger z/w, than
|
||||||
// real constant — see T1 in WalkVisibilityMathTests/the commit body).
|
// retail's real constant by fifteen ULPs; fix round 1 F4 corrected this
|
||||||
|
// comment's direction — see T1,
|
||||||
|
// PortalDepthVert_FarPunchConstant_MatchesRetailExactBits, in THIS file).
|
||||||
["portal_depth.vert.spv"] = "51c60d0924d62c61548efcf5f9e7672a121b1b68ca0a06755e32f1a4d73a8acf",
|
["portal_depth.vert.spv"] = "51c60d0924d62c61548efcf5f9e7672a121b1b68ca0a06755e32f1a4d73a8acf",
|
||||||
// sky.frag re-pinned 2026-08-23: the dome's fog blend lost its
|
// sky.frag re-pinned 2026-08-23: the dome's fog blend lost its
|
||||||
// 0.2 floor and is now applied only under an AdminEnvirons fog
|
// 0.2 floor and is now applied only under an AdminEnvirons fog
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
|
using System.Linq;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Reflection.Emit;
|
using System.Reflection.Emit;
|
||||||
using AcDream.App.Composition;
|
using AcDream.App.Composition;
|
||||||
using AcDream.App.Rendering;
|
using AcDream.App.Rendering;
|
||||||
|
using AcDream.App.Rendering.Gpu;
|
||||||
|
using AcDream.App.Rendering.Gpu.Vk;
|
||||||
using AcDream.App.Rendering.Sky;
|
using AcDream.App.Rendering.Sky;
|
||||||
using AcDream.App.Rendering.Walk;
|
using AcDream.App.Rendering.Walk;
|
||||||
using AcDream.App.Tests.Architecture;
|
using AcDream.App.Tests.Architecture;
|
||||||
|
using AcDream.App.Tests.Rendering.Gpu;
|
||||||
|
|
||||||
namespace AcDream.App.Tests.Rendering;
|
namespace AcDream.App.Tests.Rendering;
|
||||||
|
|
||||||
|
|
@ -479,6 +483,126 @@ public sealed class RetailPViewPassExecutorTests
|
||||||
|| branch.OpCode == OpCodes.Brfalse || branch.OpCode == OpCodes.Brfalse_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(
|
private static int RequiredCallIndex(
|
||||||
IReadOnlyList<CompiledCall> calls,
|
IReadOnlyList<CompiledCall> calls,
|
||||||
Type declaringType,
|
Type declaringType,
|
||||||
|
|
|
||||||
124
tests/AcDream.App.Tests/Rendering/Walk/WalkAlphaDepthTrace.cs
Normal file
124
tests/AcDream.App.Tests/Rendering/Walk/WalkAlphaDepthTrace.cs
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.Rendering.Walk;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// S4-c1 fix round 1, F3: parser for the OH alpha/depth captures
|
||||||
|
/// (docs/research/2026-09-01-overhaul/oh-capture/*.alphadepth.log —
|
||||||
|
/// tools/walk-oracle/oh/oh-capture-alpha-depth.cdb.template documents the
|
||||||
|
/// line formats). Reads the two lines this round's gate cares about:
|
||||||
|
///
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item><c>PM poly=<ptr> mode=<0|1> counterBefore=<hex></c> —
|
||||||
|
/// one per <c>D3DPolyRender::DrawPortalPolyInternal</c> @0x0059bc90 ENTRY
|
||||||
|
/// (mode 1 = far-Z building punch, mode 0 = true-depth exit seal). The
|
||||||
|
/// breakpoint is at function ENTRY, before the boundary guard runs inside
|
||||||
|
/// the function — a PM line is emitted for every ATTEMPT, guard-rejected
|
||||||
|
/// or not; <c>counterBefore</c> is the persistent
|
||||||
|
/// <c>portalsDrawnCount</c> global sampled before THIS call's own possible
|
||||||
|
/// increment.</item>
|
||||||
|
/// <item><c>PC ov=<n> counter=<hex> fc=<0|1></c> — one per
|
||||||
|
/// <c>PView::DrawCells</c> @0x005a4840 ENTRY (root turn AND every building
|
||||||
|
/// look-in's own re-entrant call alike — the breakpoint doesn't
|
||||||
|
/// distinguish). <c>counter</c> is <c>portalsDrawnCount</c> sampled at
|
||||||
|
/// THIS call's entry, before its own possible read-then-zero.</item>
|
||||||
|
/// </list>
|
||||||
|
///
|
||||||
|
/// Frame delimiting mirrors <see cref="WalkOracleTrace"/> exactly (same
|
||||||
|
/// <c>F <n></c> marker, same "events between F_n and F_(n+1) belong to
|
||||||
|
/// frame n" rule, same final-frame drop) so a caller can parse the SAME
|
||||||
|
/// capture file with both parsers and get position-consistent frame
|
||||||
|
/// numbering — <see cref="WalkOracleTrace"/> for the pose, this type for
|
||||||
|
/// the PM/PC sequences.
|
||||||
|
/// </summary>
|
||||||
|
public static class WalkAlphaDepthTrace
|
||||||
|
{
|
||||||
|
public static IReadOnlyList<WalkAlphaDepthFrame> Parse(IEnumerable<string> lines)
|
||||||
|
{
|
||||||
|
var frames = new List<WalkAlphaDepthFrame>();
|
||||||
|
List<(int Mode, int CounterBefore)>? currentPm = null;
|
||||||
|
List<(int Ov, int Counter, int ForceClear)>? currentPc = null;
|
||||||
|
int currentNumber = 0;
|
||||||
|
|
||||||
|
foreach (string line in lines)
|
||||||
|
{
|
||||||
|
Match frameMatch = FramePattern.Match(line);
|
||||||
|
if (frameMatch.Success)
|
||||||
|
{
|
||||||
|
if (currentPm is not null)
|
||||||
|
{
|
||||||
|
frames.Add(new WalkAlphaDepthFrame(currentNumber, currentPm, currentPc!));
|
||||||
|
}
|
||||||
|
currentNumber = int.Parse(
|
||||||
|
frameMatch.Groups[1].Value, CultureInfo.InvariantCulture);
|
||||||
|
currentPm = new List<(int, int)>();
|
||||||
|
currentPc = new List<(int, int, int)>();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (currentPm is null)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Match pm = PmPattern.Match(line);
|
||||||
|
if (pm.Success)
|
||||||
|
{
|
||||||
|
currentPm.Add((
|
||||||
|
int.Parse(pm.Groups[1].Value, CultureInfo.InvariantCulture),
|
||||||
|
int.Parse(pm.Groups[2].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Match pc = PcPattern.Match(line);
|
||||||
|
if (pc.Success)
|
||||||
|
{
|
||||||
|
currentPc!.Add((
|
||||||
|
int.Parse(pc.Groups[1].Value, CultureInfo.InvariantCulture),
|
||||||
|
int.Parse(pc.Groups[2].Value, NumberStyles.HexNumber, CultureInfo.InvariantCulture),
|
||||||
|
int.Parse(pc.Groups[3].Value, CultureInfo.InvariantCulture)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// Anything else (FL/AM lines, cdb chrome) is out of this
|
||||||
|
// parser's scope — ignored, matching WalkOracleTrace's own
|
||||||
|
// catch-all.
|
||||||
|
}
|
||||||
|
|
||||||
|
// The last STARTED frame is deliberately never appended — the
|
||||||
|
// truncated detach frame, same rule as WalkOracleTrace.
|
||||||
|
return frames;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<WalkAlphaDepthFrame> Load(string root, string fixtureName)
|
||||||
|
{
|
||||||
|
string repoRoot = FindRepositoryRoot();
|
||||||
|
string path = Path.Combine(
|
||||||
|
repoRoot, Path.Combine(root.Split('/')), fixtureName + ".log");
|
||||||
|
return Parse(File.ReadLines(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FindRepositoryRoot()
|
||||||
|
{
|
||||||
|
DirectoryInfo? dir = new(AppContext.BaseDirectory);
|
||||||
|
while (dir is not null)
|
||||||
|
{
|
||||||
|
if (File.Exists(Path.Combine(dir.FullName, "AcDream.slnx")))
|
||||||
|
return dir.FullName;
|
||||||
|
dir = dir.Parent;
|
||||||
|
}
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
"AcDream.slnx not found above the test base directory; walk-oracle fixtures unavailable.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly Regex FramePattern = new(@"^F (\d+)\s*$", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex PmPattern = new(
|
||||||
|
@"^PM poly=[0-9a-f]+ mode=([01]) counterBefore=([0-9a-f]{4})\s*$", RegexOptions.Compiled);
|
||||||
|
private static readonly Regex PcPattern = new(
|
||||||
|
@"^PC ov=(\d+) counter=([0-9a-f]{4}) fc=([01])\s*$", RegexOptions.Compiled);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>One capture frame's PM (mode, counterBefore) and PC (ov,
|
||||||
|
/// counter, forceClear) sequences, in the exact order the retail trace
|
||||||
|
/// recorded them. Pointers are intentionally not carried — S4-c1 F3's own
|
||||||
|
/// comparison ignores them.</summary>
|
||||||
|
public sealed record WalkAlphaDepthFrame(
|
||||||
|
int Number,
|
||||||
|
IReadOnlyList<(int Mode, int CounterBefore)> PmEvents,
|
||||||
|
IReadOnlyList<(int Ov, int Counter, int ForceClear)> PcEvents);
|
||||||
|
|
@ -1032,15 +1032,16 @@ public sealed partial class WalkFrameDriverTests
|
||||||
Assert.Equal(3, mdiCalls.Sum(c => (int)c.DrawCount));
|
Assert.Equal(3, mdiCalls.Sum(c => (int)c.DrawCount));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── S4-c1 C1/T2: DrawPortalPolyInternal's degenerate-input guard, ported
|
// ── S4-c1 C1/T2 (quantifier corrected at fix round 1 F1): DrawPortalPoly
|
||||||
// at the punch-fan producer (WalkFrameDriver.OnPunchGeometry — the
|
// Internal's degenerate-input guard, ported at the punch-fan producer
|
||||||
// handler that owns the LOCAL polygon before TransformToWorld). A source
|
// (WalkFrameDriver.OnPunchGeometry — the handler that owns the LOCAL
|
||||||
// vertex with local x/y exactly +/-12 drops the WHOLE polygon before any
|
// polygon before TransformToWorld). The polygon is dropped before any
|
||||||
// transform, event, or counter effect; the same shape at 11.999 is an
|
// transform, event, or counter effect ONLY when EVERY local vertex lies
|
||||||
// ordinary polygon and punches normally. ─────────────────────────────
|
// on the SAME +/-12 plane; a polygon with just one vertex there — or at
|
||||||
|
// 11.999, just inside — is ordinary and punches normally. ────────────
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void OnPunchGeometry_RejectsWholePolygonOnExactPlusMinus12LocalVertex_ButPunchesJustInside()
|
public void OnPunchGeometry_RejectsOnlyWhenEveryVertexSharesOnePlane_ButPunchesAnyOtherShape()
|
||||||
{
|
{
|
||||||
using var fx = new DispatcherFixture();
|
using var fx = new DispatcherFixture();
|
||||||
var log = new List<string>();
|
var log = new List<string>();
|
||||||
|
|
@ -1052,12 +1053,23 @@ public sealed partial class WalkFrameDriverTests
|
||||||
using DrawScope draw = fx.BeginDraw();
|
using DrawScope draw = fx.BeginDraw();
|
||||||
driver.BeginFrame(new TestContext(), Matrix4x4.Identity, Vector3.Zero);
|
driver.BeginFrame(new TestContext(), Matrix4x4.Identity, Vector3.Zero);
|
||||||
|
|
||||||
// Degenerate: one vertex sits exactly on the local x == +12 boundary.
|
// Degenerate: EVERY vertex sits exactly on the local x == +12
|
||||||
// MUTATION: relax the guard's exact equality to a tolerance/
|
// boundary (degenerate onto the plane) — the only shape retail
|
||||||
// inequality (e.g. x >= 12f) and this test's second assertion group
|
// actually rejects.
|
||||||
// (the admitted 11.999 polygon) starts failing instead — 11.999 is a
|
// MUTATION: restore the any-vertex form (any single vertex ==
|
||||||
// real, non-degenerate local coordinate a fifth of a millimeter
|
// +/-12 rejects the whole polygon) — the "one vertex only" case
|
||||||
// (retail units) inside the exact boundary.
|
// below starts failing (wrongly rejected) instead.
|
||||||
|
sink.OnPunchGeometry(
|
||||||
|
building,
|
||||||
|
new WalkPolygon
|
||||||
|
{
|
||||||
|
Vertices = [new(12f, -3f, 3f), new(12f, 0f, 3f), new(12f, 5f, 3f)],
|
||||||
|
Plane = new WalkPlane(Vector3.UnitZ, -3f),
|
||||||
|
},
|
||||||
|
activeViewIndex: 0);
|
||||||
|
|
||||||
|
// Admitted: only ONE vertex sits on x == +12; the other two do not
|
||||||
|
// share that plane. Retail draws this polygon (punched, counted).
|
||||||
sink.OnPunchGeometry(
|
sink.OnPunchGeometry(
|
||||||
building,
|
building,
|
||||||
new WalkPolygon
|
new WalkPolygon
|
||||||
|
|
@ -1081,12 +1093,14 @@ public sealed partial class WalkFrameDriverTests
|
||||||
driver.EndFrame();
|
driver.EndFrame();
|
||||||
driver.Replay(draw.Frame, draw.Pass);
|
driver.Replay(draw.Frame, draw.Pass);
|
||||||
|
|
||||||
// Exactly ONE punch reached the leaf — the rejected polygon produced
|
// Exactly TWO punches reached the leaf — the all-on-plane polygon
|
||||||
// no PunchFan event at all (not a punch that draws zero vertices; no
|
// produced no PunchFan event at all (not a punch that draws zero
|
||||||
// event, full stop).
|
// vertices; no event, full stop); the other two (one-vertex-on-plane,
|
||||||
WalkPolygon punched = Assert.Single(leaf.Punches);
|
// just-inside) are ordinary and both punched.
|
||||||
Assert.Equal(new Vector3(11.999f, 0f, 3f), punched.Vertices[1]);
|
Assert.Equal(2, leaf.Punches.Count);
|
||||||
Assert.Equal(1, log.Count(entry => entry == "PUNCH:3@v0"));
|
Assert.Equal(new Vector3(12f, 0f, 3f), leaf.Punches[0].Vertices[1]);
|
||||||
|
Assert.Equal(new Vector3(11.999f, 0f, 3f), leaf.Punches[1].Vertices[1]);
|
||||||
|
Assert.Equal(2, log.Count(entry => entry == "PUNCH:3@v0"));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -2004,7 +2018,12 @@ public sealed partial class WalkFrameDriverTests
|
||||||
dict[id] = data;
|
dict[id] = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly struct DrawScope : IDisposable
|
// S4-c1 fix round 1 F3: DrawScope/DispatcherFixture widened to internal
|
||||||
|
// so WalkTraceConformanceTests.AlphaDepthTranscriptTests.cs can drive a
|
||||||
|
// REAL WbDrawDispatcher for the depth-event transcript gate without
|
||||||
|
// duplicating this entire fixture (RecordingGpuDevice + WbMeshAdapter +
|
||||||
|
// TextureCache + EntitySpawnAdapter wiring) a second time.
|
||||||
|
internal readonly struct DrawScope : IDisposable
|
||||||
{
|
{
|
||||||
private readonly IDisposable _publication;
|
private readonly IDisposable _publication;
|
||||||
private readonly IGpuPassEncoder _pass;
|
private readonly IGpuPassEncoder _pass;
|
||||||
|
|
@ -2035,7 +2054,7 @@ public sealed partial class WalkFrameDriverTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class DispatcherFixture : IDisposable
|
internal sealed class DispatcherFixture : IDisposable
|
||||||
{
|
{
|
||||||
private readonly WbMeshAdapter _meshAdapter;
|
private readonly WbMeshAdapter _meshAdapter;
|
||||||
private readonly TextureCache _textures;
|
private readonly TextureCache _textures;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,340 @@
|
||||||
|
using System.Numerics;
|
||||||
|
using AcDream.App.Rendering.Gpu;
|
||||||
|
using AcDream.App.Rendering.Walk;
|
||||||
|
using DatReaderWriter;
|
||||||
|
|
||||||
|
namespace AcDream.App.Tests.Rendering.Walk;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// S4-c1 fix round 1, F3: the automated depth-event transcript gate.
|
||||||
|
/// Replays each OH alpha/depth capture's frame 2 through the SAME ported
|
||||||
|
/// walk (<see cref="RetailFrameWalk"/> + <see cref="WalkFrameDriver"/>,
|
||||||
|
/// <see cref="WalkTraceReplayContext"/> + <see cref="WalkLandscapeDatBuilder"/>
|
||||||
|
/// — the identical replay harness <see cref="WalkTraceConformanceTests"/>
|
||||||
|
/// already uses) with a RECORDING sink/leaf pair that reproduces retail's
|
||||||
|
/// own PM (mode, counterBefore) and PC (ov, counter, forceClear) sequences,
|
||||||
|
/// then compares them against <see cref="WalkAlphaDepthTrace"/>'s parse of
|
||||||
|
/// the real capture, exactly (pointers ignored).
|
||||||
|
///
|
||||||
|
/// <para><b>Why two observation points, not one.</b> Retail's PM line fires
|
||||||
|
/// at <c>DrawPortalPolyInternal</c>'s function ENTRY — before the four-plane
|
||||||
|
/// boundary guard (S4-c1 F1) has run — so a PM line is emitted for every
|
||||||
|
/// ATTEMPT, guard-rejected or not. A building punch's guard decision runs
|
||||||
|
/// INSIDE <see cref="WalkFrameDriver.OnPunchGeometry"/> (a Collect-time
|
||||||
|
/// hook): a rejected punch never reaches <see cref="IWalkFrameLeafRenderer.DrawPunchFan"/>
|
||||||
|
/// at all, so <see cref="AlphaDepthCollectSink"/> intercepts
|
||||||
|
/// <c>OnPunchGeometry</c> ITSELF, at Collect time, before the guard runs —
|
||||||
|
/// reading <see cref="WalkFrameDriver.PortalsDrawnCount"/> there gives the
|
||||||
|
/// PRE-reset value retail's own punches observe (Collect completes entirely
|
||||||
|
/// before Replay starts, so reading the counter at Replay time would already
|
||||||
|
/// see the root's own post-reset value — the wrong number, confirmed against
|
||||||
|
/// holtburg-doorway-still's own counterBefore=0002 punches immediately
|
||||||
|
/// followed by counterBefore=0000/0001 seals in the SAME turn). Exit seals
|
||||||
|
/// have no such Collect-time per-portal hook at all (<see
|
||||||
|
/// cref="IWalkFrameLeafRenderer.DrawExitSeals"/> takes no cell/portal
|
||||||
|
/// argument — <c>RetailPViewPassExecutor.DrawPortalDepthWrite</c> owns the
|
||||||
|
/// real per-portal loop entirely at Replay time), so <see cref="AlphaDepthLeaf"/>
|
||||||
|
/// reproduces that SAME loop (S4-c1 F1's guard, F2's count-before-clip order)
|
||||||
|
/// directly over the walk's own <see cref="WalkFrameDriver.InteriorFloodCells"/>
|
||||||
|
/// / <see cref="WalkCell.Portals"/> / <see cref="WalkCell.PortalPolygons"/> —
|
||||||
|
/// the identical LOCAL portal-polygon data the production seal path reads,
|
||||||
|
/// at Replay time, which is also where retail's own exit-seal loop runs
|
||||||
|
/// (established: punches always precede the single per-root ExitSeals
|
||||||
|
/// Collect-time event, so simple list concatenation reproduces the exact
|
||||||
|
/// interleaved PM order without needing a shared Collect/Replay timeline).
|
||||||
|
/// </para>
|
||||||
|
///
|
||||||
|
/// <para><b>Why two Collect+Replay passes per pose, not one.</b> Retail's
|
||||||
|
/// <c>portalsDrawnCount</c> is a PERSISTENT global, unaffected by any reset
|
||||||
|
/// between captured frames whose interior turn never fires (see
|
||||||
|
/// terrace-edge below). holtburg-doorway-still and foundry-deep both show a
|
||||||
|
/// STEADY-STATE cycle (the same N seals accepted every captured frame,
|
||||||
|
/// carrying the SAME counter value into the next frame's own PC read) —
|
||||||
|
/// reachable from a cold <c>PortalsDrawnCount=0</c> start after exactly one
|
||||||
|
/// full "priming" pass, so this gate discards a first Collect+Replay pass's
|
||||||
|
/// own output and compares only the second.</para>
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class WalkTraceConformanceTests
|
||||||
|
{
|
||||||
|
/// <summary>Collect-time <see cref="IWalkEventSink"/> decorator: forwards
|
||||||
|
/// every hook to the real <see cref="WalkFrameDriver"/> unchanged (so its
|
||||||
|
/// own event collection stays complete and correct), while separately
|
||||||
|
/// recording the two families this gate needs — see this file's own
|
||||||
|
/// class doc comment for why punches must be captured HERE rather than
|
||||||
|
/// at Replay.</summary>
|
||||||
|
private sealed class AlphaDepthCollectSink : IWalkEventSink
|
||||||
|
{
|
||||||
|
private readonly WalkFrameDriver _driver;
|
||||||
|
private readonly IWalkEventSink _inner;
|
||||||
|
|
||||||
|
public AlphaDepthCollectSink(WalkFrameDriver driver)
|
||||||
|
{
|
||||||
|
_driver = driver;
|
||||||
|
_inner = driver;
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly List<(int Mode, int CounterBefore)> Punches = new();
|
||||||
|
public readonly List<(int Ov, int Counter, int ForceClear)> PcEvents = new();
|
||||||
|
|
||||||
|
public void Emit(in WalkEvent walkEvent)
|
||||||
|
{
|
||||||
|
// PView::DrawCells @0x005a4840's own PC print fires at function
|
||||||
|
// ENTRY for every call — the root's own turn AND every building
|
||||||
|
// look-in's re-entrant call alike (oh1-depth-lifecycle.md: the
|
||||||
|
// breakpoint doesn't distinguish). WalkEventKind.DrawCells's
|
||||||
|
// Emit call is the walk's own "this DrawCells call happened"
|
||||||
|
// signal at that SAME entry-order point (its own doc comment:
|
||||||
|
// "sits at breakpoint-ENTRY order"); forceClear (oh1: no write
|
||||||
|
// site found anywhere in the binary) is always 0.
|
||||||
|
if (walkEvent.Kind == WalkEventKind.DrawCells)
|
||||||
|
PcEvents.Add((walkEvent.OutsideViewCount, _driver.PortalsDrawnCount, 0));
|
||||||
|
_inner.Emit(walkEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnLandscapeViews(WalkPortalView activeViews) =>
|
||||||
|
_inner.OnLandscapeViews(activeViews);
|
||||||
|
|
||||||
|
public void OnLandCellTurn(uint landblockId, int sideCellCount, int cellIndex) =>
|
||||||
|
_inner.OnLandCellTurn(landblockId, sideCellCount, cellIndex);
|
||||||
|
|
||||||
|
public void OnSortCellTurn(uint landblockId, int sideCellCount, int cellIndex) =>
|
||||||
|
_inner.OnSortCellTurn(landblockId, sideCellCount, cellIndex);
|
||||||
|
|
||||||
|
public void OnLandscapeCellTurn(uint cellId) =>
|
||||||
|
_inner.OnLandscapeCellTurn(cellId);
|
||||||
|
|
||||||
|
public void OnLandscapeCellTurn(uint landblockId, int sideCellCount, int cellIndex) =>
|
||||||
|
_inner.OnLandscapeCellTurn(landblockId, sideCellCount, cellIndex);
|
||||||
|
|
||||||
|
public void OnBuildingTurn(WalkBuilding building) =>
|
||||||
|
_inner.OnBuildingTurn(building);
|
||||||
|
|
||||||
|
public void OnBuildingShellTurn(WalkBuilding building) =>
|
||||||
|
_inner.OnBuildingShellTurn(building);
|
||||||
|
|
||||||
|
public void OnPunchGeometry(
|
||||||
|
WalkBuilding building, WalkPolygon polygon, int activeViewIndex)
|
||||||
|
{
|
||||||
|
// Far-Z punches never touch portalsDrawnCount (R2); this reads
|
||||||
|
// it purely as a snapshot for the transcript, BEFORE the SAME
|
||||||
|
// interior root's own OnInteriorFloodDrawTurn reset — see class
|
||||||
|
// doc comment.
|
||||||
|
Punches.Add((1, _driver.PortalsDrawnCount));
|
||||||
|
_inner.OnPunchGeometry(building, polygon, activeViewIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnInteriorFloodDrawTurn(IReadOnlyList<uint> cells, int outsideViewCount) =>
|
||||||
|
_inner.OnInteriorFloodDrawTurn(cells, outsideViewCount);
|
||||||
|
|
||||||
|
public void OnWeatherTurn(uint viewerCellId) =>
|
||||||
|
_inner.OnWeatherTurn(viewerCellId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Replay-time <see cref="IWalkFrameLeafRenderer"/>: every
|
||||||
|
/// member besides <see cref="DrawExitSeals"/> is a plain no-op (this
|
||||||
|
/// gate cares only about the depth/count mechanism, not mesh content).
|
||||||
|
/// <see cref="DrawExitSeals"/> reproduces
|
||||||
|
/// <c>RetailPViewPassExecutor.DrawPortalDepthWrite</c>'s real per-portal
|
||||||
|
/// loop — S4-c1 F1's guard, F2's count-before-clip order — directly over
|
||||||
|
/// the walk's own flood/cell data (no fake GPU clip needed: F2
|
||||||
|
/// established the count happens unconditionally once the guard
|
||||||
|
/// passes).</summary>
|
||||||
|
private sealed class AlphaDepthLeaf : IWalkFrameLeafRenderer
|
||||||
|
{
|
||||||
|
private readonly Dictionary<uint, WalkCell> _cells;
|
||||||
|
|
||||||
|
public AlphaDepthLeaf(Dictionary<uint, WalkCell> cells) => _cells = cells;
|
||||||
|
|
||||||
|
/// <summary>Set once, immediately after construction — the leaf and
|
||||||
|
/// its owning <see cref="WalkFrameDriver"/> are mutually referential
|
||||||
|
/// (the driver's constructor requires the leaf), so this cannot be a
|
||||||
|
/// constructor parameter.</summary>
|
||||||
|
public WalkFrameDriver Driver { get; set; } = null!;
|
||||||
|
|
||||||
|
public readonly List<(int Mode, int CounterBefore)> Seals = new();
|
||||||
|
|
||||||
|
public void DrawSky() { }
|
||||||
|
public void DrawLandCellBatch(
|
||||||
|
IReadOnlyList<(uint LandblockId, int SideCellCount, int CellIndex)> cells) { }
|
||||||
|
public bool HasRenderableEmittersInCell(uint cellId) => false;
|
||||||
|
public void DrawCellShell(uint cellId) { }
|
||||||
|
public void DrawStaticParticles(uint cellId) { }
|
||||||
|
public void DrawCellParticles(uint cellId) { }
|
||||||
|
public void ClearInteriorDepth() { }
|
||||||
|
public void FlushLandscape() { }
|
||||||
|
public void DrawPunchFan(WalkPolygon worldPolygon, int activeViewIndex) { }
|
||||||
|
public void AlphaBarrier() { }
|
||||||
|
|
||||||
|
public int DrawExitSeals()
|
||||||
|
{
|
||||||
|
int accepted = 0;
|
||||||
|
List<uint> flood = Driver.InteriorFloodCells;
|
||||||
|
// Reverse order — the SAME direction DrawWalkExitPortalMasks
|
||||||
|
// walks driver.InteriorFloodCells in production
|
||||||
|
// (RetailPViewRenderer.cs).
|
||||||
|
for (int i = flood.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (!_cells.TryGetValue(flood[i], out WalkCell? cell))
|
||||||
|
continue;
|
||||||
|
int sliceCount = Driver.InteriorFloodViewSliceCountAt(i);
|
||||||
|
for (int slice = 0; slice < sliceCount; slice++)
|
||||||
|
{
|
||||||
|
for (int p = 0; p < cell.Portals.Length; p++)
|
||||||
|
{
|
||||||
|
WalkCellPortal portal = cell.Portals[p];
|
||||||
|
// Walk-side exit sentinel is 32-bit 0xFFFFFFFF
|
||||||
|
// (WalkCellPortal's own doc comment) — the render-
|
||||||
|
// side CellPortalInfo's 16-bit 0xFFFF is a DIFFERENT
|
||||||
|
// representation of the same fact.
|
||||||
|
if (portal.OtherCellId != 0xFFFFFFFFu)
|
||||||
|
continue;
|
||||||
|
if ((uint)portal.PolygonIndex >= (uint)cell.PortalPolygons.Length)
|
||||||
|
continue;
|
||||||
|
WalkPolygon poly = cell.PortalPolygons[portal.PolygonIndex];
|
||||||
|
|
||||||
|
// F2: count happens BEFORE clip (unconditionally,
|
||||||
|
// once the guard passes) — counterBefore is the
|
||||||
|
// running total BEFORE this specific portal's own
|
||||||
|
// decision.
|
||||||
|
Seals.Add((0, Driver.PortalsDrawnCount + accepted));
|
||||||
|
if (!WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard(
|
||||||
|
poly.Vertices))
|
||||||
|
{
|
||||||
|
accepted++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return accepted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Minimal <see cref="IWalkFrameWorldData"/>: this gate never
|
||||||
|
/// inspects mesh content, only the depth/count event sequence, so every
|
||||||
|
/// query returns the shared empty record.</summary>
|
||||||
|
private sealed class EmptyAlphaDepthWorldData : IWalkFrameWorldData
|
||||||
|
{
|
||||||
|
public WalkFrameStaticRecords GetCellStatics(uint cellId) => WalkFrameStaticRecords.Empty;
|
||||||
|
public WalkFrameStaticRecords GetCellDynamics(uint cellId) => WalkFrameStaticRecords.Empty;
|
||||||
|
public WalkFrameStaticRecords GetOutdoorStatics(uint cellId) => WalkFrameStaticRecords.Empty;
|
||||||
|
public WalkFrameStaticRecords GetOutdoorDynamics(uint cellId) => WalkFrameStaticRecords.Empty;
|
||||||
|
public WalkFrameStaticRecords GetBuildingShellStatics(WalkBuilding building) =>
|
||||||
|
WalkFrameStaticRecords.Empty;
|
||||||
|
public Matrix4x4 GetBuildingWorldTransform(WalkBuilding building) => Matrix4x4.Identity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Runs the two-pass replay (see class doc comment for why two)
|
||||||
|
/// and asserts the SECOND pass's PM/PC sequences against
|
||||||
|
/// <paramref name="fixtureName"/>'s captured frame 2
|
||||||
|
/// (<see cref="WalkAlphaDepthTrace"/>'s <c>frames[1]</c>, matching every
|
||||||
|
/// other OH-capture row's own frame-2 convention in this file).</summary>
|
||||||
|
private void RunAlphaDepthTranscriptGate(string fixtureName)
|
||||||
|
{
|
||||||
|
IReadOnlyList<WalkOracleFrame> poseFrames = WalkOracleTrace.Load(OhCaptureRoot, fixtureName);
|
||||||
|
Assert.NotEmpty(poseFrames);
|
||||||
|
Assert.True(poseFrames.Count > 1, $"{fixtureName}: need a captured frame 2.");
|
||||||
|
WalkOracleFrame poseFrame = poseFrames[1];
|
||||||
|
Assert.NotNull(poseFrame.Pose);
|
||||||
|
WalkOraclePose pose = poseFrame.Pose!;
|
||||||
|
|
||||||
|
IReadOnlyList<WalkAlphaDepthFrame> depthFrames = WalkAlphaDepthTrace.Load(OhCaptureRoot, fixtureName);
|
||||||
|
Assert.True(depthFrames.Count > 1, $"{fixtureName}: need a captured frame 2's PM/PC content.");
|
||||||
|
WalkAlphaDepthFrame expected = depthFrames[1];
|
||||||
|
|
||||||
|
using DatCollection dats = OpenDats();
|
||||||
|
WalkLandscapeDatBuilder.BuiltWorld world =
|
||||||
|
WalkLandscapeDatBuilder.Build(dats, pose.CellId, pose.Origin);
|
||||||
|
var ctx = new WalkTraceReplayContext(pose, world.Cells) { Buildings = world.Buildings };
|
||||||
|
WalkCell? camera = (pose.CellId & 0xFFFFu) >= 0x100
|
||||||
|
? Assert.Contains(pose.CellId, world.Cells)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
using var fx = new WalkFrameDriverTests.DispatcherFixture();
|
||||||
|
var worldData = new EmptyAlphaDepthWorldData();
|
||||||
|
var leaf = new AlphaDepthLeaf(world.Cells);
|
||||||
|
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, worldData);
|
||||||
|
leaf.Driver = driver;
|
||||||
|
var sink = new AlphaDepthCollectSink(driver);
|
||||||
|
var walk = new RetailFrameWalk();
|
||||||
|
|
||||||
|
void RunOnePass()
|
||||||
|
{
|
||||||
|
using (WalkFrameDriverTests.DrawScope draw = fx.BeginDraw())
|
||||||
|
{
|
||||||
|
driver.BeginFrame(ctx, Matrix4x4.Identity, pose.Origin);
|
||||||
|
walk.WalkFrame(pose.CellId, camera, world.Landscape, ctx, sink);
|
||||||
|
driver.EndFrame();
|
||||||
|
driver.Replay(draw.Frame, draw.Pass);
|
||||||
|
}
|
||||||
|
// DrawScope.Dispose ends only the pass/publication — the frame
|
||||||
|
// itself (RecordingGpuDevice's "one open frame" invariant) is
|
||||||
|
// ended separately, same order WalkOutsideViewReassemblyTests'
|
||||||
|
// own multi-draw loop uses.
|
||||||
|
fx.FrameLifetime.EndFrame();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priming pass: reaches the steady state a real running session
|
||||||
|
// would already be in by the time retail's capture began (see class
|
||||||
|
// doc comment) — its own recorded output is discarded.
|
||||||
|
RunOnePass();
|
||||||
|
sink.Punches.Clear();
|
||||||
|
sink.PcEvents.Clear();
|
||||||
|
leaf.Seals.Clear();
|
||||||
|
|
||||||
|
// Comparison pass.
|
||||||
|
RunOnePass();
|
||||||
|
|
||||||
|
var actualPm = new List<(int Mode, int CounterBefore)>(sink.Punches);
|
||||||
|
actualPm.AddRange(leaf.Seals);
|
||||||
|
|
||||||
|
Assert.Equal(expected.PmEvents, actualPm);
|
||||||
|
Assert.Equal(expected.PcEvents, sink.PcEvents);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AlphaDepthTranscript_CathedralArrival_MatchesRetailFrame2()
|
||||||
|
=> RunAlphaDepthTranscriptGate("cathedral-arrival.alphadepth");
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AlphaDepthTranscript_FoundryDeep_MatchesRetailFrame2()
|
||||||
|
=> RunAlphaDepthTranscriptGate("foundry-deep.alphadepth");
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AlphaDepthTranscript_HoltburgDoorwayStill_MatchesRetailFrame2()
|
||||||
|
=> RunAlphaDepthTranscriptGate("holtburg-doorway-still.alphadepth");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// PINNED KnownFailure (S4-c1 fix round 1, F3): terrace-edge never runs
|
||||||
|
/// a qualifying interior turn anywhere in its own capture (every PC line
|
||||||
|
/// reads <c>ov=0</c>) — its two building punches' <c>counterBefore=2</c>
|
||||||
|
/// is a PERSISTENT session value carried over from BEFORE the capture
|
||||||
|
/// even started (the file's own pre-"F 1" content already reads 2; no
|
||||||
|
/// mechanism inside the captured frames ever changes it). A fresh two-
|
||||||
|
/// pass replay from <c>PortalsDrawnCount=0</c> has no way to derive that
|
||||||
|
/// leftover value — there is no seal activity anywhere in this fixture
|
||||||
|
/// to prime it — so this pose's own comparison pass reads
|
||||||
|
/// <c>counterBefore=0</c> where retail shows 2. This is an initial-
|
||||||
|
/// condition gap in the fixture itself, not a guard/count defect: the
|
||||||
|
/// SAME replay harness reproduces holtburg-doorway-still's and foundry-
|
||||||
|
/// deep's own steady-state counter values exactly from a cold start.
|
||||||
|
/// Observed divergence (both PM events, the fixture's only two):
|
||||||
|
/// expected <c>[(mode=1, counterBefore=2), (mode=1, counterBefore=2)]</c>,
|
||||||
|
/// actual <c>[(mode=1, counterBefore=0), (mode=1, counterBefore=0)]</c>;
|
||||||
|
/// the PC sequence (ov=0 both times) matches exactly.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
[Trait("Status", "KnownFailure")]
|
||||||
|
public void AlphaDepthTranscript_TerraceEdge_MatchesRetailFrame2()
|
||||||
|
=> RunAlphaDepthTranscriptGate("terrace-edge.alphadepth");
|
||||||
|
|
||||||
|
/// <summary>See <see cref="AlphaDepthTranscript_TerraceEdge_MatchesRetailFrame2"/>'s
|
||||||
|
/// own doc comment — cathedral-leak carries the exact same steady state
|
||||||
|
/// (counter pinned at 0, every exit portal degenerate-onto-plane
|
||||||
|
/// rejected) as cathedral-arrival, so it is expected to pass; listed
|
||||||
|
/// here because the spec's own fixture count (four) undercounts the
|
||||||
|
/// five <c>*.alphadepth.log</c> files actually present in
|
||||||
|
/// <c>oh-capture/</c> — see this round's commit body.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void AlphaDepthTranscript_CathedralLeak_MatchesRetailFrame2()
|
||||||
|
=> RunAlphaDepthTranscriptGate("cathedral-leak.alphadepth");
|
||||||
|
}
|
||||||
|
|
@ -14,7 +14,7 @@ namespace AcDream.App.Tests.Rendering.Walk;
|
||||||
/// lands.
|
/// lands.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Trait("Lane", "InstalledDat")]
|
[Trait("Lane", "InstalledDat")]
|
||||||
public sealed class WalkTraceConformanceTests
|
public sealed partial class WalkTraceConformanceTests
|
||||||
{
|
{
|
||||||
/// <summary>S3 chunk 1 fix round 1 (G10): besides the plain
|
/// <summary>S3 chunk 1 fix round 1 (G10): besides the plain
|
||||||
/// <see cref="WalkEvent"/> list the four-kind
|
/// <see cref="WalkEvent"/> list the four-kind
|
||||||
|
|
|
||||||
|
|
@ -217,66 +217,58 @@ public sealed class WalkVisibilityMathTests
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- DrawPortalPolyInternal @0x0059bc90's degenerate-input guard
|
// ---- DrawPortalPolyInternal @0x0059bc90's degenerate-input guard
|
||||||
// (S4-c1 C1, T2): ANY source vertex whose LOCAL x or y lands exactly on
|
// (S4-c1 C1, T2 — quantifier corrected at fix round 1 F1): the polygon
|
||||||
// +/-12 rejects the WHOLE polygon. ----
|
// is rejected only when EVERY local vertex lies on the SAME one of the
|
||||||
|
// four planes x=+12, x=-12, y=+12, y=-12. A polygon with merely ONE
|
||||||
|
// vertex on a bound is an ordinary polygon and is kept. ----
|
||||||
|
|
||||||
[Theory]
|
[Fact]
|
||||||
[InlineData(12f, 0f)] // x == +12 exactly
|
public void Boundary_guard_rejects_a_polygon_whose_every_vertex_sits_on_the_same_plusX_plane()
|
||||||
[InlineData(-12f, 0f)] // x == -12 exactly
|
|
||||||
[InlineData(0f, 12f)] // y == +12 exactly
|
|
||||||
[InlineData(0f, -12f)] // y == -12 exactly
|
|
||||||
public void Boundary_guard_rejects_a_polygon_with_one_vertex_exactly_on_plus_minus_12(
|
|
||||||
float x, float y)
|
|
||||||
{
|
{
|
||||||
// MUTATION (verified during S4-c1's own implementation): narrow the
|
// MUTATION: delete the guard (always return false) — this case,
|
||||||
// guard's exact equality to a strict `x > 12f` / `x < -12f` (no
|
// and every other all-on-plane case below, fails to reject.
|
||||||
// boundary-inclusive case at all) — the exact +/-12 boundary this
|
Vector3[] polygon = [new Vector3(12f, -3f, 3f), new Vector3(12f, 0f, 3f), new Vector3(12f, 5f, 3f)];
|
||||||
// theory's four rows probe stops rejecting and all four fail.
|
|
||||||
Vector3[] polygon = [new Vector3(0, 0, 3), new Vector3(x, y, 3), new Vector3(5, 5, 3)];
|
|
||||||
Assert.True(WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard(polygon));
|
Assert.True(WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard(polygon));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Fact]
|
||||||
[InlineData(11.999f, 0f)]
|
public void Boundary_guard_rejects_a_polygon_whose_every_vertex_sits_on_the_same_minusY_plane()
|
||||||
[InlineData(-11.999f, 0f)]
|
|
||||||
[InlineData(0f, 11.999f)]
|
|
||||||
[InlineData(0f, -11.999f)]
|
|
||||||
public void Boundary_guard_admits_a_polygon_whose_nearest_vertex_is_just_inside_12(
|
|
||||||
float x, float y)
|
|
||||||
{
|
{
|
||||||
// MUTATION (verified during S4-c1's own implementation): widen the
|
// The y=-12 plane specifically (§7 F1's fourth case) — not just x.
|
||||||
// guard's exact equality to a near-boundary tolerance, e.g.
|
Vector3[] polygon = [new Vector3(-3f, -12f, 3f), new Vector3(0f, -12f, 3f), new Vector3(5f, -12f, 3f)];
|
||||||
// `MathF.Abs(x) >= 11.99f` instead of `x == 12f` — a "close enough
|
Assert.True(WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard(polygon));
|
||||||
// to the boundary" mistake that still leaves ordinary far-from-12
|
}
|
||||||
// coordinates alone. 11.999 sits inside that widened band, so this
|
|
||||||
// theory's four rows fail; a strict `x >= 12f` (no tolerance at all)
|
[Fact]
|
||||||
// does NOT catch this test — 11.999 < 12 either way — which is
|
public void Boundary_guard_admits_a_polygon_with_only_one_vertex_on_plusX12_the_rest_inside()
|
||||||
// exactly why Boundary_guard_rejects_a_polygon_with_one_vertex_
|
{
|
||||||
// exactly_on_plus_minus_12 above exists as the other half of the
|
// The round-1 defect: the round-0 "any vertex" reading would have
|
||||||
// pin: it fails instead if the guard is narrowed to a strict `>`
|
// rejected this. Retail draws it (punched/sealed, counted) — only
|
||||||
// that lets the exact +/-12 boundary through.
|
// the DEGENERATE all-on-one-plane polygon is dropped.
|
||||||
Vector3[] polygon = [new Vector3(0, 0, 3), new Vector3(x, y, 3), new Vector3(5, 5, 3)];
|
// MUTATION: restore the any-vertex form (any vertex == +/-12
|
||||||
|
// rejects) — this case starts failing (wrongly rejected) even
|
||||||
|
// though the guard still correctly rejects the all-on-plane cases.
|
||||||
|
Vector3[] polygon = [new Vector3(0f, 0f, 3f), new Vector3(12f, 0f, 3f), new Vector3(5f, 5f, 3f)];
|
||||||
Assert.False(WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard(polygon));
|
Assert.False(WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard(polygon));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Boundary_guard_rejects_the_whole_polygon_even_when_only_one_of_several_vertices_hits_it()
|
public void Boundary_guard_admits_a_polygon_whose_every_vertex_is_just_inside_11_999()
|
||||||
{
|
{
|
||||||
// Retail's four var_* flags are OR'd across the WHOLE vertex loop
|
Vector3[] polygon = [new Vector3(11.999f, 0f, 3f), new Vector3(11.999f, 5f, 3f), new Vector3(11.999f, -5f, 3f)];
|
||||||
// before the single post-loop decision (0x59BD42-0x59BD66) — a
|
Assert.False(WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard(polygon));
|
||||||
// degenerate vertex anywhere in the fan condemns every vertex in it,
|
}
|
||||||
// not just its own. MUTATION (verified during S4-c1's own
|
|
||||||
// implementation): check only localVertices[0] instead of looping
|
[Fact]
|
||||||
// every vertex — the degenerate vertex here is the LAST of four, so
|
public void Boundary_guard_admits_a_polygon_split_across_plusX12_and_plusY12_no_common_plane()
|
||||||
// the guard would wrongly return false and this fails.
|
{
|
||||||
Vector3[] polygon =
|
// Two different vertices sit on two DIFFERENT bounding planes; no
|
||||||
[
|
// single plane holds every vertex, so nothing rejects it.
|
||||||
new Vector3(-3f, -3f, 3f),
|
// MUTATION: OR the four per-vertex hits instead of the four
|
||||||
new Vector3(3f, -3f, 3f),
|
// per-plane accumulators (i.e. revert to "any vertex on any plane")
|
||||||
new Vector3(3f, 3f, 3f),
|
// — this case starts failing (wrongly rejected).
|
||||||
new Vector3(12f, 3f, 3f), // the one degenerate vertex
|
Vector3[] polygon = [new Vector3(12f, 0f, 3f), new Vector3(0f, 12f, 3f), new Vector3(-5f, -5f, 3f)];
|
||||||
];
|
Assert.False(WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard(polygon));
|
||||||
Assert.True(WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard(polygon));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
@ -291,8 +283,18 @@ public sealed class WalkVisibilityMathTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Boundary_guard_admits_the_empty_polygon()
|
public void Boundary_guard_rejects_the_empty_polygon_because_every_plane_predicate_survives_vacuously()
|
||||||
{
|
{
|
||||||
Assert.False(WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard([]));
|
// S4-c1 fix round 1 F1 consequence, not an explicit §7 case: retail
|
||||||
|
// wraps the whole predicate+count block in `if (num_pts > 0)`
|
||||||
|
// (0x0059bcc9) — for num_pts==0 NOTHING runs: no count, no clip, no
|
||||||
|
// draw. The four-accumulator port reproduces that "nothing happens"
|
||||||
|
// effect for free: with no vertex to clear any of them, all four
|
||||||
|
// per-plane predicates stay at their initial `true`, so the guard
|
||||||
|
// rejects (bails before transform/clip/count) — the same net effect
|
||||||
|
// as retail's outer num_pts>0 gate. Real dat portal polygons are
|
||||||
|
// never empty (the doc comment's DAT scan enumerates only >=3-vertex
|
||||||
|
// polygons); this is a defensive corner, not a live path.
|
||||||
|
Assert.True(WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard([]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue