CNA-BUG-028: Curve::ComputeTangent Smooth tangents test the key spacing against two different epsilons, where XNA tests the value difference against one

CNA snapshot 009d40f5  ·  Known Issues › Current bugs  ·  source links pinned to 009d40f5

✓

Evidence basis: source-verified at the pinned commit; executed for this entry (the Evidence section names exactly what was run); tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Nothing on this page was executed unless the Evidence section says so.

For CurveTangent::Smooth, ComputeTangent zeroes the in tangent when the neighbour span is below 2^-24 but the out tangent only below the smallest denormal, while XNA zeroes both when the neighbours' value difference is below FLT_EPSILON.

Identifier
CNA-BUG-028
Category
Bug
Subsystem
Math & geometry
Status
Open
Verified against
CNA 009d40f5 (009d40f5dd085c4e674d3479675fac84b12b3e0a)
Severity
Low (a triage suggestion, not a project priority)
Evidence basis
Reproduced: executed for this entry (the Evidence section names exactly what was run)
Tests touching this area
Yes: see Current tests
Affected contract
Curve::ComputeTangent(int, CurveTangent, CurveTangent), ComputeTangents (all overloads) with CurveTangent::Smooth; the C ABI cna_curve_compute_tangent* routes

Expected behaviour

XNA 4.0 (read from the IL of the genuine XNA 4.0 Microsoft.Xna.Framework assembly; not executed), for a Smooth key with neighbours (p0, v0) and (p1, v1): the in tangent is 0 when |v1 - v0| < 1.1920929E-07 (FLT_EPSILON), otherwise (v1 - v0) * |p0 - p| / (p1 - p0); the out tangent uses the same test and (v1 - v0) * |p1 - p| / (p1 - p0). Both tangents share one test on the value difference.

Actual behaviour at TARGET

  • Curve.cpp computes pn = p1 - p0 and tests the spacing, not the values, with two different thresholds:
  • in tangent: MathHelper::WithinEpsilon(pn, 0.0f), i.e. |pn| < MachineEpsilonFloat (2^-24, about 5.96e-8), else (v1 - v0) * ((p - p0) / pn);
  • out tangent: std::fabs(pn) < std::numeric_limits<float>::denorm_min() (about 1.4e-45, so in practice only pn == 0), else (v1 - v0) * ((p1 - p) / pn).
  • Consequences: neighbours whose values differ by less than FLT_EPSILON get a tiny non-zero tangent instead of XNA's 0; for a neighbour span between 1.4e-45 and 5.96e-8 the in tangent is zeroed while the out tangent is still computed, (v1 - v0) * (p1 - p) / pn, which for sorted keys is at most |v1 - v0| in size, so the two tangents of one key follow different rules and the in tangent loses the value XNA gives it; and because the operations are ordered differently, ordinary tangents may differ from XNA's in the last bit (not measured). CNA's form is FNA's (MathHelper.WithinEpsilon for in, float.Epsilon for out); the ratio between the two thresholds is about 4e37.

Source locations

Evidence

Checked by reading at 009d40f5; the audit's own executed comparison follows below. Both thresholds and formulas are in the function body. The XNA algorithm was read from its IL; an earlier reading that treated the out-tangent test as the correct one compared against FNA only, and against XNA both branches deviate. Ordinary inputs do differ in the last bit; the executed comparison below measures how often.

Independent re-verification: Checked by reading at 009d40f5; nothing was built or executed for this entry. Both thresholds and formulas are in the function body. The XNA algorithm was read from its IL; an earlier reading that treated the out-tangent test as the correct one compared against FNA only, and against XNA both branches deviate. The C ABI curve smoke test does run a Smooth out tangent (compute_tangents_in_out with Flat in and Smooth out over keys at 0, 1, 2 with values 0, 2, 4 expects 2.0), but that input has evenly spaced keys on which CNA and XNA agree, so it cannot tell the rules apart; the earlier statement that it exercises Linear tangents only is wrong. Whether ordinary inputs differ in the last bit was not measured.

Independent re-verification: Reproduced by the audit at 009d40f5. The TARGET Curve.cpp was built with g++ -O0 and compared with a float32 model of the decompiled XNA 4.0 IL. The entry's reproduction gave CNA tangents of 5e-09 against XNA's 0. Keys at 0, 1e-9 and 2e-9 with values 0, 3 and 6 gave CNA in=0 and out=3 against XNA's 3 and 3. On 1,000,000 random sorted key triples CNA's tangents differed from the model's in about 34% of cases, by at most 2 ulp, because the operations are ordered differently (real XNA on an x87 JIT may keep wider intermediates, so the last-bit figure is against a strict float32 model). The XNA algorithm was read from its IL; an earlier reading that treated the out-tangent test as the correct one compared against FNA only, and against XNA both branches deviate. The C ABI curve smoke test does run a Smooth out tangent (compute_tangents_in_out with Flat in and Smooth out over keys at 0, 1, 2 with values 0, 2, 4 expects 2.0), but that input has evenly spaced keys on which CNA and XNA agree, so it cannot tell the rules apart.

Focused reproduction

Illustrative; the audit's executed probe is described under Evidence.

Curve c;
c.getKeysProperty().Add(CurveKey(0.0f, 0.0f));
c.getKeysProperty().Add(CurveKey(1.0f, 0.0f));
c.getKeysProperty().Add(CurveKey(2.0f, 1.0e-8f));   // neighbour values differ by less than FLT_EPSILON
c.ComputeTangent(1, CurveTangent::Smooth);
// XNA: TangentIn == TangentOut == 0     TARGET code: both about 5e-9

Current tests

CurveTest.ComputeTangentSmoothMiddleKey in CurveTests.cpp uses three keys with the value 0, so every formula gives 0 and the two thresholds cannot be told apart; the other tangent tests use Flat and Linear. The C ABI curve smoke test runs one Smooth out tangent (Flat in, Smooth out, keys at 0, 1 and 2 with values 0, 2 and 4, expecting 2.0), but on evenly spaced keys, where CNA and XNA agree, so it cannot tell the rules apart. No XNA oracle covers Curve.

Regression test

Smooth tangents for unequally spaced keys with unequal values (for example positions 0, 1, 3 and values 0, 1, 5, expecting 5/3 and 10/3), for neighbour values closer than FLT_EPSILON (expecting 0 for both), and for a neighbour span below 2^-24; ideally the expected values come from running the genuine XNA runtime through CNA's existing framework-oracle tooling.

Blast radius

Curves whose tangents are computed at run time with CurveTangent::Smooth (directly or through the C ABI). Curves loaded from content keep the tangents stored in the file and are unaffected unless the game recomputes them.

Workaround

Set the tangents explicitly with CurveKey::setTangentInProperty and setTangentOutProperty instead of computing Smooth tangents.

The same subject is explained at several altitudes. These are the neighbouring pages at each one.

Known issues
Bug index