The user's "doors/doorways leak through terrain and houses over a
landblock" is the #117 mark-pass bias evaluated in the wrong space.
Mechanism (confirmed analytically, Issue129PunchBiasTests): the punch's
pass-A stencil mark biased the aperture fan toward the viewer by a
CONSTANT 0.0005 NDC. NDC depth is non-linear - a constant NDC bias b
spans ~= b*d^2*(f-n)/(f*n) meters of eye depth at eye distance d. With
retail's znear 0.1 (d4b5c71) that is 0.125 m at 5 m but ~190 m at one
landblock: every hill/house in front of a distant aperture passed the
LEQUAL mark and was far-Z punched -> door-shaped leak through the
occluder. This is exactly the risk AD-18's register row recorded
("an occluder within ~bias in front of a distant aperture gets punched
through") - the symptom-scan rule found it before instrumentation.
Fix: cap the bias's EYE-SPACE span at 0.5 m -
biasNdc(d) = min(0.0005, capMeters * near / d^2)
in the mark-pass vertex shader (clipPos.w = eye depth), CPU-mirrored as
PortalDepthMaskRenderer.MarkBiasNdc for tests. Below the ~10 m
crossover the constant-NDC term is smaller and wins - bit-identical to
the T5-validated close-range behavior, so the #108 grass coverage that
justified the bias is untouched. Beyond it the punch can never reach an
occluder more than 0.5 m in front of the aperture plane.
Pins (Issue129PunchBiasTests): the old form spans >100 m of eye depth
at a landblock (the leak, kept as documentation of the refuted shape);
the capped form stays <= 0.5 m at every distance 1-400 m and matches
the validated constant bit-for-bit below 10 m.
AD-18 register row updated in the same commit (bias description + the
#129 closure + the residual risk note: door-hugging geometry beyond the
0.5 m cap at >10 m viewing range re-occludes - the cap constant is the
tuning knob if the gate shows residue).
Suites: App 256+1skip / Core 1439+2skip / UI 420 / Net 294 green.
Awaiting the user visual gate at the original spot (+ #108 cellar
re-check up close).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
68 lines
3 KiB
C#
68 lines
3 KiB
C#
using System;
|
||
using AcDream.App.Rendering;
|
||
using Xunit;
|
||
|
||
namespace AcDream.App.Tests.Rendering;
|
||
|
||
/// <summary>
|
||
/// #129 — doors/doorways leak through terrain and houses from over a landblock
|
||
/// away. The punch's mark pass (#117, AD-18) biased the aperture fan toward
|
||
/// the viewer by a CONSTANT 0.0005 NDC. NDC depth is non-linear: a constant
|
||
/// NDC bias b spans ≈ b·d²·(f−n)/(f·n) meters of eye depth at eye distance d
|
||
/// — 0.125 m at 5 m but ~190 m at a landblock (znear 0.1), so distant
|
||
/// occluders in front of an aperture passed the mark and were far-Z punched:
|
||
/// the door-shaped leak. The fix caps the bias's eye-space span
|
||
/// (PortalDepthMaskRenderer.MarkBiasNdc): identical to the validated constant
|
||
/// below the ~10 m crossover, never more than the cap beyond it.
|
||
/// </summary>
|
||
public class Issue129PunchBiasTests
|
||
{
|
||
private const float Near = PortalDepthMaskRenderer.CameraNearPlaneMeters; // 0.1 (retail znear)
|
||
private const float Far = 5000f;
|
||
|
||
/// <summary>Eye-depth span (meters) covered by an NDC depth bias b at eye
|
||
/// distance d: ndc(d) = f(d−n)/((f−n)d) ⇒ d(ndc) inverse ⇒
|
||
/// span = b·d²·(f−n)/(f·n) (exact for small b via the derivative).</summary>
|
||
private static float EyeSpanMeters(float biasNdc, float d) =>
|
||
biasNdc * d * d * (Far - Near) / (Far * Near);
|
||
|
||
[Fact]
|
||
public void OldConstantBias_SpansMetersAtALandblock_TheLeak()
|
||
{
|
||
// The refuted form (documentation of WHY the constant was wrong):
|
||
// 0.0005 NDC at ~one landblock spans far more eye depth than any
|
||
// occluder separation — everything in front got punched.
|
||
Assert.True(EyeSpanMeters(0.0005f, 192f) > 100f);
|
||
// ...while at close range it was a sane sliver:
|
||
Assert.InRange(EyeSpanMeters(0.0005f, 5f), 0.05f, 0.30f);
|
||
}
|
||
|
||
[Fact]
|
||
public void CappedBias_MatchesValidatedConstant_AtCloseRange()
|
||
{
|
||
// Below the crossover the T5-validated constant must win unchanged —
|
||
// this preserves the #108 grass coverage bit-for-bit.
|
||
foreach (float d in new[] { 0.5f, 1f, 3f, 5f, 8f, 9.9f })
|
||
Assert.Equal(0.0005f, PortalDepthMaskRenderer.MarkBiasNdc(d), 6);
|
||
}
|
||
|
||
[Fact]
|
||
public void CappedBias_EyeSpanNeverExceedsCap_AtAnyDistance()
|
||
{
|
||
for (float d = 1f; d <= 400f; d += 1f)
|
||
{
|
||
float span = EyeSpanMeters(PortalDepthMaskRenderer.MarkBiasNdc(d), d);
|
||
Assert.True(span <= PortalDepthMaskRenderer.PunchMarkBiasEyeCapMeters * 1.02f,
|
||
FormattableString.Invariant($"bias spans {span:F2} m of eye depth at d={d} m"));
|
||
}
|
||
}
|
||
|
||
[Fact]
|
||
public void CappedBias_At200m_CannotReachOccluders()
|
||
{
|
||
// The reported #129 distance: occluder separations are tens of
|
||
// meters; the punch reach must stay under the 0.5 m cap.
|
||
float span = EyeSpanMeters(PortalDepthMaskRenderer.MarkBiasNdc(200f), 200f);
|
||
Assert.True(span <= 0.51f, FormattableString.Invariant($"span {span:F3} m at 200 m"));
|
||
}
|
||
}
|