feat(render): S4 chunk 1 — retail far-punch bits, the ±12 local-input reject, the depth truth table and cross-frame latch pins

S4-c1 per docs/research/2026-09-01-overhaul/s4-depth-alpha-packet.md §6.
S3 chunk 2 already landed the persistent portalsDrawnCount latch, the
gated clear, the exit-seal counting, and the look-in isolation — this
chunk covers only what §1/§2 of the packet name as still owed: C0-C3.

C0 — far-punch depth constant (R1: DrawPortalPolyInternal @0x0059bc90's
tail). portal_depth.vert's punch branch carried the decimal 0.99999988,
which reinterprets as bits 0x3F7FFFFE — fifteen ULPs FARTHER from the
camera than retail's real constant, bits 0x3F7FFFEF. Now writes
`uintBitsToFloat(0x3F7FFFEFu)` so the exact bits survive the GLSL/SPIR-V
compiler instead of trusting a decimal literal to round-trip unchanged.
Recompiled via tools/compile-shaders.ps1 (glslc 1.4.350.0 backend
recorded, managed shaderc path used); portal_depth.vert.spv's SHA-256
re-pinned in VulkanShaderManifestTests
(51c60d0924d62c61548efcf5f9e7672a121b1b68ca0a06755e32f1a4d73a8acf,
was 4ac1c452e7ac0d08a32f67fb03f21229af2d1605baa81f407240a3626251dfd7).

T1 (new Fact PortalDepthVert_FarPunchConstant_MatchesRetailExactBits in
VulkanShaderManifestTests.cs): a SOURCE pin — reads portal_depth.vert's
punch line and reinterprets whatever literal it carries (uintBitsToFloat
hex or a plain decimal) as raw bits, asserts == 0x3F7FFFEF. Verified
against the PRE-CHANGE source by hand-reverting the line to
`clipPos.z = clipPos.w * 0.99999988;` and re-running just this test:

    Assert.Equal() Failure: Values differ
    Expected: 1065353199
    Actual:   1065353214

(1065353199 = 0x3F7FFFEF, 1065353214 = 0x3F7FFFFE). Line restored and
the test re-confirmed green afterward. MUTATION: any other literal fails
the same way.

C1 — the ±12 local-input reject (R2: 0x59BCD6-0x59BD28 then
0x59BD40-0x59BD66). The Ghidra arbitration table in
oh1-depth-lifecycle.md governs over the pseudo-C's own nested-if reading
of the four x87 FCOM results (BinaryNinja's `test ah, 0x44` condition
synthesis is FPU-flag-ambiguous and reads backward at face value — see
feedback_bn_decomp_field_names.md on decompiler flag mush as an artifact
class, not semantics): the table's row says "whole poly on any
local-input x/y == +/-12 boundary is rejected before count/clip" — taken
as written, not re-derived from the pseudo-C's literal branch nesting.

Ported as one shared predicate,
WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard(ReadOnlySpan
<Vector3>): true iff any vertex's X or Y is exactly +12f/-12f (retail
tests LOCAL x/y before xformStart, the world transform). Wired at BOTH
producers that own the LOCAL polygon before it leaves cell/building
space:
  - WalkFrameDriver.OnPunchGeometry (the walk's punch-event producer,
    IWalkEventSink.OnPunchGeometry) — checked on the building-local
    WalkPolygon.Vertices before TransformToWorld; a hit returns before
    MarkIfGrown/any event append (retail's reject -> transform -> clip
    -> count order).
  - RetailPViewPassExecutor.DrawPortalDepthWrite (the exit-seal
    enumeration behind DrawExitPortalMask, the sole caller) — checked on
    cell.PortalPolygons[index]'s local vertices before the
    Vector3.Transform loop; a hit `continue`s with no `submitted++`.

T2 (three layers):
  1. WalkVisibilityMathTests.cs — direct unit tests of the predicate:
     Boundary_guard_rejects_a_polygon_with_one_vertex_exactly_on_plus_minus_12
     (Theory, x/y == +-12 each), Boundary_guard_admits_a_polygon_whose_
     nearest_vertex_is_just_inside_12 (Theory, x/y == +-11.999),
     Boundary_guard_rejects_the_whole_polygon_even_when_only_one_of_
     several_vertices_hits_it, Boundary_guard_ignores_the_vertical_z_
     component, Boundary_guard_admits_the_empty_polygon.
  2. WalkFrameDriverTests.OnPunchGeometry_RejectsWholePolygonOnExact
     PlusMinus12LocalVertex_ButPunchesJustInside — functional: feeds
     OnPunchGeometry a polygon with a vertex at x=12 (no PunchFan/no
     "PUNCH:" log line) then one at x=11.999 (punches normally,
     leaf.Punches has exactly one entry, log has exactly one "PUNCH:3@v0").
  3. RetailPViewPassExecutorTests.DrawPortalDepthWrite_RejectsDegenerate
     LocalPolygons_BeforeTransformOrSubmission — a real functional test of
     DrawPortalDepthWrite needs a live PortalDepthMaskRenderer the suite
     has no fake for, so this is a compiled-call-graph pin (this file's
     established pattern for exactly this situation): the guard call
     precedes both the Vector3.Transform loop and
     PortalDepthMaskRenderer.DrawDepthFan by IL offset, gated by a
     conditional branch immediately after it.

MUTATION texts, all verified live during this session then reverted:
  - OnPunchGeometry_RejectsWholePolygon... with the C1 guard deleted from
    OnPunchGeometry:
      Assert.Single() Failure: The collection contained 2 items
      Collection: [WalkPolygon { Plane = WalkPlane { Normal = <0, 0, 1>, D = -3 }, Vertices = [<0, 0, 3>, <12, 0, 3>, <5, 5, 3>] }, WalkPolygon { Plane = WalkPlane { Normal = <0, 0, 1>, D = -3 }, Vertices = [<0, 0, 3>, <11.999, 0, 3>, <5, 5, 3>] }]
  - DrawPortalDepthWrite_RejectsDegenerateLocalPolygons... with the C1
    guard deleted from DrawPortalDepthWrite:
      Expected call to WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard.
  - Boundary_guard_admits_a_polygon_whose_nearest_vertex_is_just_inside_12
    with the predicate widened to `MathF.Abs(x) >= 11.99f ||
    MathF.Abs(y) >= 11.99f` (all four rows):
      Assert.False() Failure
      Expected: False
      Actual:   True
  - Boundary_guard_rejects_a_polygon_with_one_vertex_exactly_on_plus_
    minus_12 with the predicate narrowed to strict `x > 12f || x < -12f
    || y > 12f || y < -12f` (all four rows):
      Assert.True() Failure
      Expected: True
      Actual:   False
  - Boundary_guard_rejects_the_whole_polygon_even_when_only_one_of_
    several_vertices_hits_it with the guard checking only
    localVertices[0] instead of looping every vertex:
      Assert.True() Failure
      Expected: True
      Actual:   False

C2 — no pipeline change for R3 (depth ALWAYS/write/no-cull, color writes
ENABLED with a zero-alpha SRCALPHA/INVSRCALPHA blend). acdream's
PortalDepthMaskRenderer.Rhi.cs:92,100 sets ColorWrite=false alongside
Blend=None; portal_depth.frag writes no color output at all. Provably
pixel-identical (retail's blend collapses to dst'=dst when srcAlpha is
fixed at 0, for any RGB) and the write mask is the SAFER mechanism going
forward (structurally blocks any future accidental color write,
independent of an authored zero-alpha invariant). Added register row
AD-119 to docs/architecture/retail-divergence-register.md (the next free
id after AD-118), citing DrawPortalPolyInternal @0x0059bc90 and
PortalDepthMaskRenderer.Rhi.cs; section 2's active-row count and running
header note updated (90 -> 91).

C3 — the truth table + cross-frame latch tests. The (root kind,
draw_landscape, outside-view count, previous count) table's cells are
mostly already covered by S3 chunk 2's own tests — this chunk adds only
the genuinely missing rows/cases, and leaves every existing test
untouched:

  Pre-existing coverage (named, not reproduced):
    - interior, ov==0, prior==0 ->
      RunFrame_InteriorFloodWithNoExitView_SkipsLandscapeAndNeverFlushesClearsOrSeals
    - interior, ov>0, prior==0 ->
      RunFrame_InteriorFloodWithExitView_FreshDriverSkipsTheGatedClearThenDrawsSealsAndFloodCells
      and OnInteriorFloodDrawTurn_FirstOvFrameSkipsClear_SecondFrameArmedByFirstsSealsClears
      (its own frame 1)
    - interior, ov>0, prior>0 (T4's "frame 1 seals N>0 -> frame 2
      clears" half) ->
      OnInteriorFloodDrawTurn_FirstOvFrameSkipsClear_SecondFrameArmedByFirstsSealsClears
      (its own frame 2)
    - T4's "frame 1 seals 0 -> frame 2 does not clear" half (repeated
      across three consecutive ov>0 frames, subsuming the two-frame
      case) -> OnInteriorFloodDrawTurn_FloodWithNoExitPortal_NeverClearsAcrossFrames
    - one look-in isolated from the root latch ->
      LookInDrawCells_NeitherArmsNorConsumesThePortalsDrawnCounter
  No further T4 test was added — the two existing facts above already
  prove both halves of the two-consecutive-frames latch case exactly.

  New rows added this chunk:
    - WalkFrame_OutdoorRoot_NeverFiresTheInteriorClearSealMachinery: root
      kind == OUTDOOR. RetailFrameWalk.WalkFrame's outdoor branch
      ((cameraCellId & 0xFFFF) < 0x100) calls DrawLandscape directly and
      never calls DrawInside/OnInteriorFloodDrawTurn at all, so the whole
      LFLUSH/stamp/CLEAR/SEALS mechanism structurally cannot fire —
      driven end-to-end through RunFrame with an outdoor cameraCellId,
      asserting SKY present, LFLUSH/CLEAR/SEALS absent, counter stays 0.
      MUTATION (verified, then reverted): added a stray
      `sink.OnInteriorFloodDrawTurn([], 1);` call to WalkFrame's outdoor
      branch:
        Assert.DoesNotContain() Failure: Item found in collection
                         ↓ (pos 1)
        Collection: ["SKY", "LFLUSH", "SEALS"]
        Found:      "LFLUSH"
    - OnInteriorFloodDrawTurn_OvZeroAfterAPriorArmedCounter_LeavesTheLatch
      CompletelyUntouched: interior, ov==0 immediately after an EARLIER
      ov>0 frame armed the counter — proves the counter is left EXACTLY
      as an earlier frame left it (not merely "not cleared this frame"),
      since S3 §8.1 R3 gates the ENTIRE outside_view.view_count>0 block,
      including the read-then-zero decision itself, on ov>0.
      MUTATION (verified, then reverted): moved
      `int armed = PortalsDrawnCount; PortalsDrawnCount = 0;` out of the
      `if (outsideViewCount > 0)` gate in
      WalkFrameDriver.OnInteriorFloodDrawTurn (unconditional
      read-then-zero every call):
        Assert.Equal() Failure: Values differ
        Expected: 1
        Actual:   0
      (every OTHER WalkFrameDriverTests fact stayed green under this same
      mutation — this new test is the only one that catches it).
    - MultipleLookIns_WithinOneFrameAndAcrossFrames_NeverTouchTheRootLatch
      (T5): extends the single-look-in fact to TWO look-ins in one frame
      then a THIRD in a later frame. MUTATION (verified, then reverted):
      a `_mutationLookInCalls` counter in HandleDrawCellsTurn's
      LookInStatic branch that resets PortalsDrawnCount on the SECOND
      look-in call:
        Assert.Equal() Failure: Values differ
        Expected: 1
        Actual:   0
      — while LookInDrawCells_NeitherArmsNorConsumesThePortalsDrawnCounter
      (one look-in only) stayed green under the identical mutation,
      confirming this test's incremental value over the existing single-
      look-in fact.

Gates: dotnet build (App.Tests and App) 0 warnings/0 errors; hermetic
lane 6832/6832 passed; InstalledDat lane against
C:/Users/erikn/Documents/Asheron's Call — exactly the four known
failures (TowerAscentReplayTests.TowerAscent_StaircaseStaysConeVisible_
EveryStep, LayoutImporterMediaBearingChildSweepTests.
MainGameUiAndChatInput_MediaBearingChildrenNowBuildAsRealWidgets and
LayoutImporterInvisibleSweepTests.EveryAuthoredInvisibleWidget_
StartsHiddenAcrossAllLayouts — both #383 — and
WalkTraceConformanceTests.Oh_doorway_still_first_frame_diff #458),
243 passed / 1 skipped / 4 failed / 248 total, no new failures; shader
tests (VulkanShaderDescriptorContractTests/VulkanShaderManifestTests/
RenderPackSpirvValidatorTests/SkyVertexLayoutTests) 35/35; register
tests (Divergence|Register filter) 52/52.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-09-03 20:47:53 +02:00
parent 2712a8b221
commit d1e3e64f61
11 changed files with 564 additions and 4 deletions

File diff suppressed because one or more lines are too long

View file

@ -3,6 +3,7 @@ using AcDream.App.Rendering.Gpu;
using AcDream.App.Rendering.Scene; using AcDream.App.Rendering.Scene;
using AcDream.App.Rendering.Sky; using AcDream.App.Rendering.Sky;
using AcDream.App.Rendering.Wb; using AcDream.App.Rendering.Wb;
using AcDream.App.Rendering.Walk;
using AcDream.Core.Rendering; using AcDream.Core.Rendering;
using AcDream.Core.Vfx; using AcDream.Core.Vfx;
using AcDream.Core.World; using AcDream.Core.World;
@ -396,6 +397,16 @@ public RetailPViewPassExecutor(
if (localVertices.Length < 3) if (localVertices.Length < 3)
continue; continue;
// S4-c1 C1: DrawPortalPolyInternal's degenerate-input guard
// (WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard's
// own doc comment) — tested on the LOCAL portal-polygon
// vertices, BEFORE the world-transform loop below. A hit drops
// the whole polygon: no transform, no fan submission, no
// `submitted` increment (retail's reject -> transform -> clip
// -> count order).
if (WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard(localVertices))
continue;
int count = Math.Min(localVertices.Length, world.Length); int count = Math.Min(localVertices.Length, world.Length);
for (int vertex = 0; vertex < count; vertex++) for (int vertex = 0; vertex < count; vertex++)
{ {

View file

@ -50,7 +50,15 @@ void main()
if (uRenderPass == 1) if (uRenderPass == 1)
{ {
clipPos.z = clipPos.w * 0.99999988; // retail far-z punch constant (0x0059bc90 tail) // S4-c1 C0: retail's D3DPolyRender::DrawPortalPolyInternal
// @0x0059bc90 tail writes the far-Z punch from the EXACT bit
// pattern 0x3F7FFFEF (0.999999f-ish, NOT the "obvious" 1-2^-23
// literal 0.99999988f/0x3F7FFFFE the punch used to carry — that
// decimal is fifteen ULPs farther from the camera than retail's
// real constant). uintBitsToFloat keeps the exact bits instead of
// trusting a decimal literal to round-trip through the GLSL/SPIR-V
// compiler unchanged.
clipPos.z = clipPos.w * uintBitsToFloat(0x3F7FFFEFu);
} }
gl_Position = clipPos; gl_Position = clipPos;
} }

View file

@ -295,7 +295,7 @@
"stages": [ "stages": [
{ {
"stage": "vert", "stage": "vert",
"sourceSha256": "1df7e2009cda8f84ba3d84546bf58baf71d10fe53e696655b4ca7b8292a32fa5", "sourceSha256": "afc70a0716ac9dbb3a514bfca6feeae8d0934f51e677b202b9049634645ba6ce",
"compiled": true "compiled": true
}, },
{ {

View file

@ -1514,6 +1514,15 @@ internal sealed class WalkFrameDriver : IWalkEventSink, IWalkLookInViewSource
ArgumentNullException.ThrowIfNull(polygon); ArgumentNullException.ThrowIfNull(polygon);
RequireOpenFrame(); RequireOpenFrame();
// S4-c1 C1: DrawPortalPolyInternal's degenerate-input guard
// (WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard's own
// doc comment) — tested on the polygon's LOCAL vertices, BEFORE the
// building -> world transform below. A hit drops the whole polygon:
// no punch event, no transform, no counter effect (retail's own
// reject -> transform -> clip -> count order).
if (WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard(polygon.Vertices))
return;
MarkIfGrown(); MarkIfGrown();
Matrix4x4 worldTransform = _worldData.GetBuildingWorldTransform(building); Matrix4x4 worldTransform = _worldData.GetBuildingWorldTransform(building);
_events.Add( _events.Add(

View file

@ -197,6 +197,40 @@ public static class WalkVisibilityMath
} }
return partial ? WalkBoundingType.PartiallyInside : WalkBoundingType.EntirelyInside; return partial ? WalkBoundingType.PartiallyInside : WalkBoundingType.EntirelyInside;
} }
/// <summary>
/// <c>D3DPolyRender::DrawPortalPolyInternal</c> @0x0059bc90's
/// degenerate-input guard (Ghidra-arbitrated:
/// docs/research/2026-09-01-overhaul/oh1-depth-lifecycle.md's "Ghidra
/// branch arbitration table", row <c>0x59BCD60x59BD28</c> then
/// <c>0x59BD400x59BD66</c> — S4-c1 C1). The pseudo-C's own nested-if
/// reading of the four x87 FCOM results (via <c>test ah, 0x44</c>
/// against a Binary Ninja-synthesized condition byte) is FPU-flag
/// ambiguous and reads backward if taken at face value; the arbitration
/// table's sense governs — per <c>feedback_bn_decomp_field_names.md</c>,
/// a decompiler's flag-mush around x87 compares is a known artifact
/// class, not semantics.
///
/// <para>Retail tests every SOURCE vertex's LOCAL x and y — BEFORE
/// <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
/// transform, no clip, no <c>portalsDrawnCount</c> increment (retail's
/// order is reject → transform → clip → count). Ordinary authored dat
/// portal polygons essentially never land a vertex on that exact
/// boundary — this is a degenerate-input guard, not a clip rule — but
/// it is retail's code, so it is ported as retail's code.</para>
/// </summary>
public static bool IsRejectedByPortalPolygonBoundaryGuard(ReadOnlySpan<Vector3> localVertices)
{
for (int i = 0; i < localVertices.Length; i++)
{
float x = localVertices[i].X;
float y = localVertices[i].Y;
if (x == 12f || x == -12f || y == 12f || y == -12f)
return true;
}
return false;
}
} }
/// <summary>Retail <c>Plane</c>: dot(N, p) + d, positive side = inside.</summary> /// <summary>Retail <c>Plane</c>: dot(N, p) + d, positive side = inside.</summary>

View file

@ -1,10 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.RegularExpressions;
namespace AcDream.App.Tests.Rendering.Gpu.Vk; namespace AcDream.App.Tests.Rendering.Gpu.Vk;
@ -71,7 +73,13 @@ public sealed class VulkanShaderManifestTests
// candidate: ALWAYS / write / no stencil; far-Z punch, true-depth seal). // candidate: ALWAYS / write / no stencil; far-Z punch, true-depth seal).
// Its acceptance belongs to OVERHAUL S4 (gate G3); this pin records the // Its acceptance belongs to OVERHAUL S4 (gate G3); this pin records the
// binary that is actually on the branch so accidental drift is still caught. // binary that is actually on the branch so accidental drift is still caught.
["portal_depth.vert.spv"] = "4ac1c452e7ac0d08a32f67fb03f21229af2d1605baa81f407240a3626251dfd7", // Re-pinned 2026-09-03 (S4-c1 C0): the far-Z punch constant now writes
// retail's EXACT bits (uintBitsToFloat(0x3F7FFFEFu),
// D3DPolyRender::DrawPortalPolyInternal @0x0059bc90's tail) instead of
// the decimal literal 0.99999988f, which round-tripped to a DIFFERENT
// bit pattern (0x3F7FFFFE, fifteen ULPs nearer the camera than retail's
// real constant — see T1 in WalkVisibilityMathTests/the commit body).
["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
// override, retail's GameSky::Draw @0x00506FF0 rule (see // override, retail's GameSky::Draw @0x00506FF0 rule (see
@ -292,4 +300,75 @@ public sealed class VulkanShaderManifestTests
// pipeline it can build at all. // pipeline it can build at all.
Assert.True(probe.VulkanReady, "vk_probe must compile — the whole Vulkan backend draws with it."); Assert.True(probe.VulkanReady, "vk_probe must compile — the whole Vulkan backend draws with it.");
} }
/// <summary>
/// S4-c1 C0 (T1): <c>D3DPolyRender::DrawPortalPolyInternal</c>
/// @0x0059bc90's tail writes the far-Z punch depth from the EXACT bit
/// pattern <c>0x3F7FFFEF</c> — the depth-lifecycle spec's "Current
/// acdream correspondence" table's "wrong constant" row, and
/// docs/research/2026-09-01-overhaul/s4-depth-alpha-packet.md §6's R1/C0.
/// This is a SOURCE pin, deliberately independent of
/// <see cref="PreCampaignRetailSpirvBinariesRemainByteExact"/>'s compiled
/// hash pin above: it reads the punch line directly out of the GLSL and
/// reinterprets whatever numeric literal it finds as raw bits, so a
/// future edit that swaps in an "equivalent-looking" decimal literal (or
/// any other constant) is caught even before anyone re-runs
/// tools/compile-shaders.ps1. Recognizes both the current
/// <c>uintBitsToFloat(0x...u)</c> form and a plain decimal float literal
/// (the pre-C0 shape), so it can also be pointed at old source to prove
/// it used to fail. At `d0c981212` (pre-C0) the line read
/// <c>clipPos.z = clipPos.w * 0.99999988;</c>, whose bits are
/// <c>0x3F7FFFFE</c> (confirmed via
/// <c>BitConverter.SingleToInt32Bits(0.99999988f)</c>) — FIFTEEN ULPs
/// off from retail's <c>0x3F7FFFEF</c>; running this exact assertion
/// against that source produces (verified by hand-editing the line back
/// to the old literal and re-running this test during S4-c1's own
/// implementation — the commit body carries the same transcript):
/// "Assert.Equal() Failure: Values differ\nExpected: 1065353199\nActual: 1065353214"
/// (1065353199 = 0x3F7FFFEF, 1065353214 = 0x3F7FFFFE).
/// MUTATION: change the punch literal to anything other than bits
/// <c>0x3F7FFFEF</c> (restore the old decimal, or substitute a different
/// hex constant) — <see cref="Assert.Equal(uint, uint)"/> below fails.
/// </summary>
[Fact]
public void PortalDepthVert_FarPunchConstant_MatchesRetailExactBits()
{
string source = File.ReadAllText(Path.Combine(ShadersDirectory(), "portal_depth.vert"));
string line = source
.Split('\n')
.Select(l => l.Trim())
.SingleOrDefault(l => l.StartsWith("clipPos.z = clipPos.w * ", StringComparison.Ordinal))
?? throw new InvalidOperationException(
"portal_depth.vert no longer has a 'clipPos.z = clipPos.w * <literal>;' punch "
+ "line for T1 to read — did the far-Z punch assignment move or get restructured?");
uint bits = ParsePunchLiteralBits(line);
Assert.Equal(0x3F7FFFEFu, bits);
}
private static uint ParsePunchLiteralBits(string assignmentLine)
{
// assignmentLine looks like:
// "clipPos.z = clipPos.w * uintBitsToFloat(0x3F7FFFEFu);" (post-C0)
// or:
// "clipPos.z = clipPos.w * 0.99999988;" (pre-C0)
const string prefix = "clipPos.z = clipPos.w * ";
string rhs = assignmentLine[prefix.Length..];
int commentStart = rhs.IndexOf("//", StringComparison.Ordinal);
if (commentStart >= 0)
rhs = rhs[..commentStart];
rhs = rhs.Trim().TrimEnd(';', ' ');
Match hexMatch = Regex.Match(rhs, @"uintBitsToFloat\(\s*0x([0-9A-Fa-f]+)u?\s*\)");
if (hexMatch.Success)
return Convert.ToUInt32(hexMatch.Groups[1].Value, 16);
string literal = rhs.TrimEnd('f', 'F');
if (float.TryParse(literal, NumberStyles.Float, CultureInfo.InvariantCulture, out float value))
return unchecked((uint)BitConverter.SingleToInt32Bits(value));
throw new InvalidOperationException(
$"portal_depth.vert's punch literal '{rhs}' is neither a uintBitsToFloat(0x...) call "
+ "nor a plain float literal T1 knows how to reinterpret as bits.");
}
} }

View file

@ -1,3 +1,4 @@
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;
@ -418,6 +419,66 @@ public sealed class RetailPViewPassExecutorTests
RetailPViewPassExecutor.ShouldDrawWeatherOnce(renderSky, renderWeather, playerCellId)); RetailPViewPassExecutor.ShouldDrawWeatherOnce(renderSky, renderWeather, playerCellId));
} }
/// <summary>
/// S4-c1 C1 (T2, seal half): <c>D3DPolyRender::DrawPortalPolyInternal</c>
/// @0x0059bc90's degenerate-input guard, ported at the exit-seal
/// enumeration (<c>DrawPortalDepthWrite</c> — the SAME loop that reads
/// <c>cell.PortalPolygons[index]</c>, the LOCAL portal-polygon
/// vertices, and is the only production caller of
/// <see cref="RetailPViewPassExecutor.DrawExitPortalMask"/>). A live
/// functional test of this private method needs a real
/// <see cref="PortalDepthMaskRenderer"/> the suite has no fake for (see
/// <c>WalkFrameDriverTests.OnPunchGeometry_RejectsWholePolygonOn...</c>
/// for the punch-fan half's functional proof instead), so this pin asks
/// the compiled-call-graph question this file's other tests already use
/// for exactly this situation: does
/// <see cref="AcDream.App.Rendering.Walk.WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard"/>
/// run BEFORE the vertex loop's <see cref="Vector3.Transform(Vector3,
/// Matrix4x4)"/> calls and BEFORE <see cref="PortalDepthMaskRenderer.DrawDepthFan"/>
/// (retail's reject -&gt; transform -&gt; clip -&gt; count order — a hit
/// must never reach either), with a conditional branch gating that
/// order directly off the guard's own return value.
/// MUTATION: move the guard call to AFTER the transform loop (or
/// delete it) — either check below fails because the guard call index
/// is no longer the smallest, or (deletion) <see cref="RequiredCallIndex"/>
/// throws for finding no call at all.
/// </summary>
[Fact]
public void DrawPortalDepthWrite_RejectsDegenerateLocalPolygons_BeforeTransformOrSubmission()
{
MethodInfo method = typeof(RetailPViewPassExecutor).GetMethod(
"DrawPortalDepthWrite",
BindingFlags.Instance | BindingFlags.NonPublic)!;
IReadOnlyList<CompiledCall> calls = CompiledCallGraph.Read(method);
int guardIndex = RequiredCallIndex(
calls,
typeof(AcDream.App.Rendering.Walk.WalkVisibilityMath),
nameof(AcDream.App.Rendering.Walk.WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard));
int transformIndex = RequiredCallIndex(calls, typeof(Vector3), nameof(Vector3.Transform));
int drawIndex = RequiredCallIndex(
calls, typeof(PortalDepthMaskRenderer), nameof(PortalDepthMaskRenderer.DrawDepthFan));
Assert.True(
guardIndex < transformIndex,
"The boundary guard must run BEFORE the world-transform loop "
+ "(retail's reject -> transform -> clip -> count order).");
Assert.True(
guardIndex < drawIndex,
"The boundary guard must run BEFORE the fan is submitted "
+ "(no draw, no `submitted` increment on a hit).");
int guardOffset = calls[guardIndex].Offset;
int transformOffset = calls[transformIndex].Offset;
IReadOnlyList<CompiledBranch> branches = CompiledCallGraph.ReadBranches(method);
Assert.Contains(
branches,
branch => branch.Offset > guardOffset
&& branch.Offset < transformOffset
&& (branch.OpCode == OpCodes.Brtrue || branch.OpCode == OpCodes.Brtrue_S
|| branch.OpCode == OpCodes.Brfalse || branch.OpCode == OpCodes.Brfalse_S));
}
private static int RequiredCallIndex( private static int RequiredCallIndex(
IReadOnlyList<CompiledCall> calls, IReadOnlyList<CompiledCall> calls,
Type declaringType, Type declaringType,

View file

@ -610,6 +610,141 @@ public sealed partial class WalkFrameDriverTests
Assert.Equal(0, driver.PortalsDrawnCount); Assert.Equal(0, driver.PortalsDrawnCount);
} }
// ── S4-c1 C3 (T3): the depth-alpha packet's truth table over (root kind,
// draw_landscape, outside-view count, previous count) -> the exact
// LFLUSH/stamp/CLEAR/SEALS event subsequence. Chunk 2 already covers
// most of the table as direct Facts, so chunk 1 adds only the rows that
// were genuinely missing rather than re-deriving them:
// - interior, ov==0 (draw_landscape==false), prior==0 ->
// RunFrame_InteriorFloodWithNoExitView_SkipsLandscapeAndNeverFlushesClearsOrSeals
// - interior, ov>0 (draw_landscape==true), prior==0 ->
// RunFrame_InteriorFloodWithExitView_FreshDriverSkipsTheGatedClearThenDrawsSealsAndFloodCells
// and OnInteriorFloodDrawTurn_FirstOvFrameSkipsClear_SecondFrameArmedByFirstsSealsClears
// (its own frame 1)
// - interior, ov>0, prior>0 ->
// OnInteriorFloodDrawTurn_FirstOvFrameSkipsClear_SecondFrameArmedByFirstsSealsClears
// (its own frame 2) — this pair IS T4, the two-consecutive-frames
// latch case, below
// The two rows below — root kind == OUTDOOR (where the entire LFLUSH/
// stamp/CLEAR/SEALS mechanism cannot fire at all, because
// RetailFrameWalk.WalkFrame's outdoor branch never calls DrawInside/
// OnInteriorFloodDrawTurn), and interior/ov==0 immediately AFTER a
// prior-armed nonzero counter (proving the counter is left completely
// UNTOUCHED, not merely "not cleared this frame") — were not yet pinned
// anywhere. ─────────────────────────────────────────────────────────
[Fact]
public void WalkFrame_OutdoorRoot_NeverFiresTheInteriorClearSealMachinery()
{
// Root kind == OUTDOOR: RetailFrameWalk.WalkFrame's
// (cameraCellId & 0xFFFF) < 0x100 branch calls DrawLandscape
// directly and never calls DrawInside — so OnInteriorFloodDrawTurn,
// the sole owner of LFLUSH/stamp/CLEAR/SEALS, never fires at all,
// for ANY outside-view-count/previous-count combination (there is
// no such combination reachable outdoors — this row of the table
// has no ov/prior axis).
using var fx = new DispatcherFixture();
var log = new List<string>();
var leaf = new RecordingLeafRenderer(log);
var ctx = new TestContext();
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, new FakeWorldData());
var walk = new RetailFrameWalk();
// Same minimal, no-op 1x1 unpublished landscape the interior exit-
// view fixtures use — LScape::draw still runs its full sky/terrain
// turn against it; there's nothing published to iterate for.
var landscape = new WalkLandscape { MidWidth = 1, Blocks = new WalkLandBlock?[1] };
using DrawScope draw = fx.BeginDraw();
driver.RunFrame(
walk, cameraCellId: 0x00000050u, cameraCell: null, landscape: landscape,
ctx, draw.Frame, draw.Pass, Matrix4x4.Identity, cameraWorldPosition: Vector3.Zero);
// MUTATION: add a stray sink.OnInteriorFloodDrawTurn(...) call to
// RetailFrameWalk.WalkFrame's outdoor branch (e.g. a copy-paste from
// the interior branch) — SKY still appears, but LFLUSH/SEALS would
// too, and this fails.
Assert.Contains("SKY", log);
Assert.DoesNotContain("LFLUSH", log);
Assert.DoesNotContain("CLEAR", log);
Assert.DoesNotContain("SEALS", log);
Assert.Equal(0, driver.PortalsDrawnCount);
}
[Fact]
public void OnInteriorFloodDrawTurn_OvZeroAfterAPriorArmedCounter_LeavesTheLatchCompletelyUntouched()
{
// Interior root, ov==0 immediately after an EARLIER ov>0 frame armed
// the counter: S3 §8.1 R3 gates the ENTIRE outside_view.view_count>0
// block — including the read-then-zero decision itself — so ov==0
// must leave the counter EXACTLY as an earlier frame left it, not
// merely "not cleared this frame" (a read-then-zero-back-to-the-
// same-nonzero-value mistake would also leave PortalsDrawnCount
// looking untouched from the OUTSIDE — this test's real target is
// that no clear/seal machinery runs at all, proven by the empty log
// alongside the unchanged counter).
using var fx = new DispatcherFixture();
var log = new List<string>();
var leaf = new RecordingLeafRenderer(log);
var ctx = new TestContext();
const uint cellId = 0xF4180310u;
var cell = new WalkCell { CellId = cellId };
cell.PushView();
WalkCopyView.AppendFullViewportQuad(
cell.TopView, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
ctx.Cells[cellId] = cell;
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, new FakeWorldData());
IWalkEventSink sink = driver;
using DrawScope draw = fx.BeginDraw();
// Frame 1: an ordinary ov>0 flood arms the counter.
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
sink.Emit(WalkEvent.Landscape(activeViewCount: 1));
var views = new WalkPortalView();
WalkCopyView.AppendFullViewportQuad(
views, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
sink.OnLandscapeViews(views);
sink.OnInteriorFloodDrawTurn([cellId], outsideViewCount: 1);
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
Assert.Equal(1, driver.PortalsDrawnCount);
log.Clear();
// Frame 2: ov==0 — retail's whole clear/seal gate is skipped by
// construction (no landscape turn either, matching DrawInside's own
// ov==0 shape).
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
sink.OnInteriorFloodDrawTurn([cellId], outsideViewCount: 0);
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
// The ov==0 flood still draws its OWN cell's shell + contents (R1's
// "straight to the flood's own cells" tail) — the four LFLUSH/
// stamp/CLEAR/SEALS tokens are what must be absent, not the whole
// log; see RunFrame_InteriorFloodWithNoExitView_... above for the
// same non-empty-but-gate-free shape.
Assert.DoesNotContain("SKY", log);
Assert.DoesNotContain("LFLUSH", log);
Assert.DoesNotContain("CLEAR", log);
Assert.DoesNotContain("SEALS", log);
// MUTATION: move the `int armed = PortalsDrawnCount; PortalsDrawnCount
// = 0;` read-then-zero in WalkFrameDriver.OnInteriorFloodDrawTurn
// outside the `if (outsideViewCount > 0)` gate — the counter reads
// back as 0 here instead of the untouched 1, and this fails.
Assert.Equal(1, driver.PortalsDrawnCount);
}
// ── S4-c1 C3 (T4): the same fixture as
// OnInteriorFloodDrawTurn_FirstOvFrameSkipsClear_SecondFrameArmedByFirstsSealsClears
// above already proves the "frame 1 seals N>0 -> frame 2 clears" half of
// the two-consecutive-frames latch, and
// OnInteriorFloodDrawTurn_FloodWithNoExitPortal_NeverClearsAcrossFrames
// already proves "frame 1 seals 0 -> frame 2 does not clear" (repeated
// across three consecutive ov>0 frames, which subsumes the two-frame
// case). No further T4 test is added — see the commit body for the
// pre-existing-coverage inventory. ────────────────────────────────────
// ── T3 (S3 chunk 2, R1): a building look-in's own DrawCells re-enters // ── T3 (S3 chunk 2, R1): a building look-in's own DrawCells re-enters
// with ov==0 unconditionally and neither ARMS nor CONSUMES the // with ov==0 unconditionally and neither ARMS nor CONSUMES the
// persistent PortalsDrawnCount counter — retail calls DrawCells // persistent PortalsDrawnCount counter — retail calls DrawCells
@ -669,6 +804,91 @@ public sealed partial class WalkFrameDriverTests
Assert.Equal(1, driver.PortalsDrawnCount); Assert.Equal(1, driver.PortalsDrawnCount);
} }
// ── S4-c1 C3 (T5): the ABOVE test proves one look-in is isolated from
// the root latch; this extends it to MULTIPLE look-ins — two in the
// SAME frame (two buildings' own portal passes), then a third in a
// LATER, separate frame — since retail's DrawCells re-entry (R1) has no
// per-call state of its own that could accumulate across repeats. ────
[Fact]
public void MultipleLookIns_WithinOneFrameAndAcrossFrames_NeverTouchTheRootLatch()
{
using var fx = new DispatcherFixture();
var log = new List<string>();
var leaf = new RecordingLeafRenderer(log);
var ctx = new TestContext();
const uint rootCellId = 0xF4180330u;
const uint lookInCellIdA = 0xF4180331u;
const uint lookInCellIdB = 0xF4180332u;
var rootCell = new WalkCell { CellId = rootCellId };
rootCell.PushView();
WalkCopyView.AppendFullViewportQuad(
rootCell.TopView, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
ctx.Cells[rootCellId] = rootCell;
foreach (uint lookInId in new[] { lookInCellIdA, lookInCellIdB })
{
var lookInCell = new WalkCell { CellId = lookInId };
lookInCell.PushView();
WalkCopyView.AppendFullViewportQuad(
lookInCell.TopView, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
ctx.Cells[lookInId] = lookInCell;
}
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, new FakeWorldData());
IWalkEventSink sink = driver;
using DrawScope draw = fx.BeginDraw();
// Arm PortalsDrawnCount with one throwaway ov>0 interior-root flood.
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
sink.Emit(WalkEvent.Landscape(activeViewCount: 1));
var rootViews = new WalkPortalView();
WalkCopyView.AppendFullViewportQuad(
rootViews, ctx.Rays, ctx.WorldViewpoint, ctx.ViewportWidth, ctx.ViewportHeight);
sink.OnLandscapeViews(rootViews);
sink.OnInteriorFloodDrawTurn([rootCellId], outsideViewCount: 1);
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
Assert.Equal(1, driver.PortalsDrawnCount);
log.Clear();
// TWO look-ins in the SAME frame.
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
sink.OnBuildingTurn(new WalkBuilding());
sink.Emit(WalkEvent.DrawCells(outsideViewCount: 0, [lookInCellIdA]));
sink.OnBuildingTurn(new WalkBuilding());
sink.Emit(WalkEvent.DrawCells(outsideViewCount: 0, [lookInCellIdB]));
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
Assert.DoesNotContain("LFLUSH", log);
Assert.DoesNotContain("CLEAR", log);
Assert.DoesNotContain("SEALS", log);
// MUTATION (verified during S4-c1's own implementation, then
// reverted): a bug that only mishandles a REPEAT look-in call within
// one frame (e.g. resetting PortalsDrawnCount on the second
// HandleDrawCellsTurn(LookInStatic) call) leaves this exact
// assertion at 0 instead of 1, while
// LookInDrawCells_NeitherArmsNorConsumesThePortalsDrawnCounter above
// — which calls DrawCells exactly once — stays green throughout;
// that gap is this test's whole reason to exist over the single-
// look-in fact.
Assert.Equal(1, driver.PortalsDrawnCount);
log.Clear();
// A THIRD look-in in a LATER, separate frame.
driver.BeginFrame(ctx, Matrix4x4.Identity, Vector3.Zero);
sink.OnBuildingTurn(new WalkBuilding());
sink.Emit(WalkEvent.DrawCells(outsideViewCount: 0, [lookInCellIdA]));
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
Assert.DoesNotContain("LFLUSH", log);
Assert.DoesNotContain("CLEAR", log);
Assert.DoesNotContain("SEALS", log);
Assert.Equal(1, driver.PortalsDrawnCount);
}
// ── Deliverable: a building turn's alpha barrier precedes its portal // ── Deliverable: a building turn's alpha barrier precedes its portal
// pass (retail RenderDeviceD3D::DrawBuilding @0x0059f2a0: // pass (retail RenderDeviceD3D::DrawBuilding @0x0059f2a0:
// FlushAlphaList(0f) -> CPhysicsPart::Draw(parts,1) [the portal walk] // FlushAlphaList(0f) -> CPhysicsPart::Draw(parts,1) [the portal walk]
@ -812,6 +1032,63 @@ 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
// at the punch-fan producer (WalkFrameDriver.OnPunchGeometry — the
// handler that owns the LOCAL polygon before TransformToWorld). A source
// vertex with local x/y exactly +/-12 drops the WHOLE polygon before any
// transform, event, or counter effect; the same shape at 11.999 is an
// ordinary polygon and punches normally. ─────────────────────────────
[Fact]
public void OnPunchGeometry_RejectsWholePolygonOnExactPlusMinus12LocalVertex_ButPunchesJustInside()
{
using var fx = new DispatcherFixture();
var log = new List<string>();
var leaf = new RecordingLeafRenderer(log);
var driver = new WalkFrameDriver(fx.Dispatcher, leaf, new FakeWorldData());
IWalkEventSink sink = driver;
var building = new WalkBuilding { PositionCellId = 0xA9B40040u };
using DrawScope draw = fx.BeginDraw();
driver.BeginFrame(new TestContext(), Matrix4x4.Identity, Vector3.Zero);
// Degenerate: one vertex sits exactly on the local x == +12 boundary.
// MUTATION: relax the guard's exact equality to a tolerance/
// inequality (e.g. x >= 12f) and this test's second assertion group
// (the admitted 11.999 polygon) starts failing instead — 11.999 is a
// real, non-degenerate local coordinate a fifth of a millimeter
// (retail units) inside the exact boundary.
sink.OnPunchGeometry(
building,
new WalkPolygon
{
Vertices = [new(0f, 0f, 3f), new(12f, 0f, 3f), new(5f, 5f, 3f)],
Plane = new WalkPlane(Vector3.UnitZ, -3f),
},
activeViewIndex: 0);
// Admitted: the nearest-boundary vertex is 11.999, not 12 — an
// ordinary polygon that must punch exactly like any other.
sink.OnPunchGeometry(
building,
new WalkPolygon
{
Vertices = [new(0f, 0f, 3f), new(11.999f, 0f, 3f), new(5f, 5f, 3f)],
Plane = new WalkPlane(Vector3.UnitZ, -3f),
},
activeViewIndex: 0);
driver.EndFrame();
driver.Replay(draw.Frame, draw.Pass);
// Exactly ONE punch reached the leaf — the rejected polygon produced
// no PunchFan event at all (not a punch that draws zero vertices; no
// event, full stop).
WalkPolygon punched = Assert.Single(leaf.Punches);
Assert.Equal(new Vector3(11.999f, 0f, 3f), punched.Vertices[1]);
Assert.Equal(1, log.Count(entry => entry == "PUNCH:3@v0"));
}
[Fact] [Fact]
public void RepeatedFloodTurns_DrawEnvCellShellWholeOncePerRetailFrameStamp() public void RepeatedFloodTurns_DrawEnvCellShellWholeOncePerRetailFrameStamp()
{ {

View file

@ -215,4 +215,84 @@ public sealed class WalkVisibilityMathTests
WalkBoundingType.EntirelyInside, WalkBoundingType.EntirelyInside,
WalkVisibilityMath.ViewconeCheck(new Vector3(3, 5, 0), 1f, Cy, edges)); WalkVisibilityMath.ViewconeCheck(new Vector3(3, 5, 0), 1f, Cy, edges));
} }
// ---- DrawPortalPolyInternal @0x0059bc90's degenerate-input guard
// (S4-c1 C1, T2): ANY source vertex whose LOCAL x or y lands exactly on
// +/-12 rejects the WHOLE polygon. ----
[Theory]
[InlineData(12f, 0f)] // x == +12 exactly
[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
// guard's exact equality to a strict `x > 12f` / `x < -12f` (no
// boundary-inclusive case at all) — the exact +/-12 boundary this
// 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));
}
[Theory]
[InlineData(11.999f, 0f)]
[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
// guard's exact equality to a near-boundary tolerance, e.g.
// `MathF.Abs(x) >= 11.99f` instead of `x == 12f` — a "close enough
// 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)
// does NOT catch this test — 11.999 &lt; 12 either way — which is
// exactly why Boundary_guard_rejects_a_polygon_with_one_vertex_
// exactly_on_plus_minus_12 above exists as the other half of the
// pin: it fails instead if the guard is narrowed to a strict `&gt;`
// that lets the exact +/-12 boundary through.
Vector3[] polygon = [new Vector3(0, 0, 3), new Vector3(x, y, 3), new Vector3(5, 5, 3)];
Assert.False(WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard(polygon));
}
[Fact]
public void Boundary_guard_rejects_the_whole_polygon_even_when_only_one_of_several_vertices_hits_it()
{
// Retail's four var_* flags are OR'd across the WHOLE vertex loop
// before the single post-loop decision (0x59BD42-0x59BD66) — a
// 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
// every vertex — the degenerate vertex here is the LAST of four, so
// the guard would wrongly return false and this fails.
Vector3[] polygon =
[
new Vector3(-3f, -3f, 3f),
new Vector3(3f, -3f, 3f),
new Vector3(3f, 3f, 3f),
new Vector3(12f, 3f, 3f), // the one degenerate vertex
];
Assert.True(WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard(polygon));
}
[Fact]
public void Boundary_guard_ignores_the_vertical_z_component()
{
// Retail's guard reads only the two horizontal FIELDS the decomp
// names (arg1->vertices[i] as the x source, ecx_1[1] as the y
// source) — a vertex whose HEIGHT happens to be +/-12 is an entirely
// ordinary local coordinate and must not trip the guard.
Vector3[] polygon = [new Vector3(0, 0, 12f), new Vector3(1, 1, -12f), new Vector3(2, 0, 0)];
Assert.False(WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard(polygon));
}
[Fact]
public void Boundary_guard_admits_the_empty_polygon()
{
Assert.False(WalkVisibilityMath.IsRejectedByPortalPolygonBoundaryGuard([]));
}
} }