CNA-BUG-028: Curve::ComputeTangent Smooth tangents test the key spacing against two different epsilons, where XNA tests the value difference against one
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.cppcomputes 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_EPSILONget 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.WithinEpsilonfor in, float.Epsilon for out); the ratio between the two thresholds is about 4e37.
Source locations
modules/math/src/Curve.cpp— Curve::ComputeTangent(int, CurveTangent, CurveTangent): the two Smooth branchesmodules/math/src/MathHelper.cpp— MathHelper::WithinEpsilon and GetMachineEpsilonFloat (2^-24)modules/math/tests/Microsoft/Xna/Framework/CurveTests.cpp— CurveTest.ComputeTangentSmoothMiddleKey uses three equal valuesmodules/c-api/src/CnaCApiCurve.cpp— the C ABI tangent routes call ComputeTangent and ComputeTangents
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.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- Math types: curve types
- Maintainer workflow
- Add a regression test: the math case
- Known issues
- Bug index