CNA-BUG-064: Curve::Evaluate reads the wrong key for Step continuity away from position 1 and for a Linear post-loop
Evidence basis: source-verified at the pinned commit; 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.
A Step segment returns the next key's value whenever the evaluated position is at least 1.0 (an absolute constant) instead of at the segment's end, and a Linear post-loop extrapolates with the first key's TangentOut instead of the last key's; XNA does neither.
- Identifier
CNA-BUG-064- Category
- Bug
- Subsystem
- Math & geometry
- Status
- Open
- Verified against
- CNA
009d40f5(009d40f5dd085c4e674d3479675fac84b12b3e0a) - Severity
- Medium (a triage suggestion, not a project priority)
- Evidence basis
- Source-verified: read at TARGET, not executed
- Tests touching this area
- Yes: see Current tests
- Affected contract
- float Curve::Evaluate(float position) const with CurveContinuity::Step keys or CurveLoopType::Linear post-loop; C ABI cna_curve_evaluate
Expected behaviour
XNA 4.0 (read from the IL of the genuine XNA 4.0 Microsoft.Xna.Framework assembly; not executed): FindSegment computes the normalised parameter t in [0, 1] of the segment (in double; a width of at most 1e-10 gives t = 0), and Hermite returns the next key's value for a Step key only when t >= 1, i.e. at the segment's end, and the previous key's value everywhere else in the segment. After the last key, CurveLoopType.Linear returns last.Value - last.TangentOut * (last.Position - position), the last key's own slope.
Actual behaviour at TARGET
- Step:
Curve::GetCurvePositioninCurve.cppreturnsnext.getValueProperty()whenposition >= 1.0fand the previous value otherwise, comparing the absolute position with the constant 1. For keys (0, 0, Step), (1, 5, Step), (2, 9),Evaluate(1.5f)returns 9 where XNA returns 5; for Step keys at 0 and 0.5,Evaluate(0.5f)returns the first key's value where XNA returns the second's. - Linear post-loop:
Curve::Evaluatereturnslast.Value + first.TangentOut * (position - last.Position). For keys (0, 0, TangentOut 2) and (1, 1, tangents 0),Evaluate(2.0f)returns 3 where XNA returns 1.
Both lines are ported unchanged from FNA's C# implementation (read at FNA commit 1358793, 2026-08-01); its comment on the Linear line even says "with a tangent of last point". The pre-loop Linear case (first key's TangentIn) matches XNA.
Source locations
modules/math/src/Curve.cpp— Curve::GetCurvePosition (the Step branch) and Curve::Evaluate (the post-loop Linear case)modules/math/tests/Microsoft/Xna/Framework/CurveTests.cpp— CurveTest.EvaluatePostLoopLinear pins first.TangentOut; StepContinuityReturnsCurrentSegmentValue evaluates only at 0.5 in a 0 to 1 segmentmodules/c-api/tests/pure_c/CurveSmoke.c— validate_curve_loops uses keys with identical tangents, so Linear post-loop cannot tell first from lastmodules/c-api/src/CnaCApiCurve.cpp— cna_curve_evaluatemodules/content/src/Xnb/CurveContentTypeReader.cpp— curves loaded from XNB are evaluated by the same code
Evidence
Checked by reading at 009d40f5; nothing was built or executed for this entry. Both branches were read in Curve.cpp; XNA's segment search, Hermite and loop handling were read from its IL, which settles the Step question that earlier documentation left open. The worked values were derived by hand from the two formulas.
Focused reproduction
Illustrative; not compiled or run for this entry.
Curve step;
step.getKeysProperty().Add(CurveKey(0.0f, 0.0f, 0.0f, 0.0f, CurveContinuity::Step));
step.getKeysProperty().Add(CurveKey(1.0f, 5.0f, 0.0f, 0.0f, CurveContinuity::Step));
step.getKeysProperty().Add(CurveKey(2.0f, 9.0f));
float a = step.Evaluate(1.5f); // XNA: 5 TARGET code: 9
Curve lin;
lin.getKeysProperty().Add(CurveKey(0.0f, 0.0f, 0.0f, 2.0f));
lin.getKeysProperty().Add(CurveKey(1.0f, 1.0f));
lin.setPostLoopProperty(CurveLoopType::Linear);
float b = lin.Evaluate(2.0f); // XNA: 1 TARGET code: 3
Current tests
CurveTest.EvaluatePostLoopLinear in CurveTests.cpp builds exactly the Linear case above and asserts 3, with a comment deriving it from first.TangentOut: it pins the defect and must change with the fix. CurveTest.StepContinuityReturnsCurrentSegmentValue evaluates at 0.5 in a segment from 0 to 1, where the absolute and normalised tests agree. The C ABI CurveSmoke.c loop checks use keys whose tangents are all 2, so they pass either way. No XNA oracle covers Curve.
Regression test
The two cases above with XNA's answers (5 and 1), a Step segment ending before 1.0 evaluated exactly at its end, and a correction of EvaluatePostLoopLinear's expectation; ideally cross-checked by running the genuine runtime through CNA's framework-oracle tooling.
Blast radius
Any curve with Step keys whose segments do not happen to span 0 to 1, and any curve with a Linear post-loop whose first and last out tangents differ, evaluated in C++, through cna_curve_evaluate or after loading from XNB. Smooth continuity inside the range, the other loop types and the pre-loop are unaffected by these two lines.
Workaround
For Step curves, look up the key values directly; for a Linear post-loop, set the first key's TangentOut equal to the last key's or extrapolate outside the curve.
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 · I need to change public XNA behaviour
- Known issues
- Bug index