Math module internals
Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page); oracle-compared (recorded by CNA, not re-run here). 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. Checked by reading the TARGET sources, tests and recorded oracle corpora; no build or test was executed. The oracle corpora (matrix, bounding sphere, framework packing) are CNA's recorded outputs of the genuine XNA framework and cover only the functions named on the page. Everything else is unit-test or source-read evidence; deeper per-type ABI and oracle coverage, hash codes and structure size beyond Color remain unverified, and the C ABI library was not built.
modules/math/ owns CNA's XNA-shaped value layer: vectors, matrices, quaternions, colour, points and rectangles, bounding volumes, planes and rays, and curves. Its results flow into graphics transforms, effects, packed vertices, content serialization and the C ABI's value routes. It has no device, platform or lifecycle to initialise, so the maintenance risk is different from most modules: preserving evaluation order, packing and failure behaviour while changing arithmetic that looks harmless. This page is for maintainers who touch those numbers; the type-by-type reference is Math types, whose numerical-behaviour table summarises the same evidence at user level. Everything below was checked by reading the TARGET snapshot, its tests and its recorded oracle corpora; nothing was built or run for this page.
Physical module and API boundary
math/CMakeLists.txt builds cna_math (alias CNA::Math) through cna_add_module from a CONFIGURE_DEPENDS glob of src/*.cpp, seventeen files at TARGET, so it is a compiled static archive and not a header-only library. It links Sharp Runtime's Core.Base component PUBLIC (the exception types, the SharpRuntime integer aliases such as bytecs, intcs and UInt32, and the System::IEquatable interface base) and cna_core_headers PUBLIC. The core edge is headers only: the public headers use the CNAEXT marker, and Color.cpp includes core's inline Internal/PackedRounding.hpp; no core symbol is referenced, so the core archive stays off the link line (Core: boundary). The closure is a checked contract: ModuleProbes.cmake registers probe_math with a link-closure check, and the probe's link line may contain no other libcna_ archive, no SDL3, enet, FFmpeg or renderer library. A renderer may consume these values, but the module never selects a renderer or opens a window; a backend translates coordinates and precision at its own boundary rather than by changing math semantics to suit one API.
Public types live under modules/math/include/Microsoft/Xna/Framework. Most are held or returned by value; the only types that own mutable state are Curve and CurveKeyCollection, and there is no allocator, no explicit dispose and no resource to release, so the ownership questions are value copies, the mutable key collection and layout or serialized structure (ownership and lifetime master map). The families, and where each is implemented and tested:
| Family | Implementation | Main tests (static definitions) |
|---|---|---|
Vector2, Vector3, Vector4 | Vector3.cpp, Vector2.cpp, Vector4.cpp | Vector3Tests (84), Vector2Tests (85), Vector4Tests (79), Vector3CompoundAssignmentTests (5) |
Matrix | Matrix.cpp | MatrixTests.cpp (85), MatrixOracleTests.cpp (3, over a 444-case corpus) |
Quaternion, Plane, Ray | Quaternion.cpp, Plane.cpp, Ray.cpp | QuaternionTests (57), PlaneTests (32), RayTests (19) |
BoundingBox, BoundingSphere, BoundingFrustum | BoundingBox.cpp, BoundingSphere.cpp, BoundingFrustum.cpp | BoundingBoxTests (51), BoundingSphereTests (52), BoundingSphereOracleTests (4, over a 445-set corpus), BoundingFrustumTests (40) |
Point, Rectangle, MathHelper | Point.cpp, Rectangle.cpp, MathHelper.cpp | PointTests (18), RectangleTests (35), MathHelperTests (55) |
Color | Color.cpp | ColorTests.cpp (61); the packing corpus is checked from the graphics tree |
Curve, CurveKey, CurveKeyCollection and the enums | Curve.cpp, CurveKey.cpp, CurveKeyCollection.cpp | CurveTests (28), CurveKeyCollectionTests (21), CurveKeyTests (17), five small enum-identity files |
| Cross-type contracts | headers | ObjectEqualityContractTests (15: Equals(std::any) for the fourteen boxed-equality types), XmlSerializationEXTTests (4) |
The test directory holds 27 files with 873 static GoogleTest definitions at TARGET (counted by macro; that is a definition count, not a count of executed cases, and nothing was executed). XmlSerializationEXT.hpp is a header-only, opt-in CNAEXT file that states each value type's member list once for System::Xml::Serialization::XmlSerializer (element names are the field names, in XNA's order, and Matrix writes M11 to M44 row by row). Including it is what pulls in Sharp Runtime's Xml.Serialization component, so the math archive itself does not depend on it. That component is not configured on a Windows target, and UnitTests.cmake then removes XmlSerializationEXTTests.cpp from the inventory as well. The text-form counterpart is the opt-in Framework.Design converters, a second public surface over the same types.
Layout and object shape
Which types are plain data and which carry a vtable matters to anything that copies bytes:
Matrixdeclares sixteen publicfloatfieldsM11,M12, …,M44in row order and no virtual members;Vector2/3/4,Quaternion,Plane,Ray,PointandRectangleare likewise plain field structs with no base class. A new field in any of them changes layout for every consumer that copies these types, including the C ABI adapters and serialized data.Colorderives fromIPackedVectorT<UInt32>andSystem::IEquatable<Color>, so it holds a vtable pointer before its packed word;BoundingBox,BoundingSphere,BoundingFrustumandCurveKeyoverrideEqualsthroughIEquatableand are polymorphic as well. OnlyColorhas a test that pins this (ColorTest.SizeIsLargerThanFourBytesVtablePresent), because casting aColor*to bytes for pixel I/O once wrote into the vtable; pixel buffers are plain byte arrays that construct aColorper pixel.- The C ABI never exposes the C++ layout.
math_values.hdefines row-majorCNA_Matrix(m11…m44) and other plain structs, andcore.hdefinesCNA_Coloras fouruint8_tchannelsr,g,b,a; the packed word is a separate route,cna_color_get_packed_value. The C header repeats the enum identities as the same numbers (CNA_CONTAINMENT_*,CNA_PLANE_INTERSECTION_*,CNA_CURVE_*), and the enum-identity tests pin the C++ side. The C routes live inCnaCApiMatrix.cpp,CnaCApiColor.cpp,CnaCApiGeometry.cpp,CnaCApiCurve.cpp,CnaCApiVectors.cppandCnaCApiMath.cpp, and are exercised by theCApi_MatrixSmoke,CApi_ColorSmoke,CApi_GeometrySmoke,CApi_CurveSmoke,CApi_VectorSmokeandCApi_MathValuesSmoketests when the experimental C API is built (see route families; that library was not built for this page).
Under the default-OFF CNA_ENABLE_UNITY_BUILD pilot, BuildPerformance.cmake compiles the math sources in six groups of at most three files, because six files deliberately use the same helper names in anonymous namespaces and would collide in one unity translation unit; test objects are batched by eight, and the seven math test files that use the name kEps get a per-file unique spelling. A new math source with anonymous-namespace helpers must be placed in a group where its names do not clash. See Core: boundary for the core half of that pilot.
Matrix convention, numerical order and graphics consequences
Row vectors, axes and the view and projection terms
The API uses the row-vector convention: a position is a row multiplied on the left, and translation lives in M41 to M43. Vector3::Transform(position, matrix) computes X from position.X*M11 + position.Y*M21 + position.Z*M31 + M41, and the other components from the next columns; TransformNormal is the same without the M4x term. Matrix::Multiply(a, b) is the ordinary row-by-column product, so v * (a * b) applies a first. The named-axis accessors in Matrix.cpp read rows of a world matrix: Right is (M11, M12, M13), Up is (M21, M22, M23), Backward is (M31, M32, M33), Forward, Down and Left are the negated rows, and Translation is row four. Matrix::CreateWorld(position, forward, up) fills those rows from the normalised axes and the position.
The view and projection builders read as follows at TARGET:
| Builder | What the source does |
|---|---|
CreateLookAt(position, target, up) | The backward axis is normalize(position - target), the vector from the target back to the camera (not camera-to-target); the right axis is normalize(cross(up, backward)) and the camera up is cross(backward, right). The three axes fill the first three columns (M11, M21, M31 is right, M12, M22, M32 is up, M13, M23, M33 is backward) and M41 to M43 hold the negated dot products of each axis with the position, so the camera looks down −Z. It validates nothing: coincident position and target, or an up vector parallel to the view direction, normalise a zero vector and produce non-finite entries without an exception. |
CreatePerspectiveFieldOfView(fov, aspect, near, far) | Throws std::invalid_argument when fov <= 0 or fov >= 3.141593f, when either distance is not positive, or when near >= far. M22 is 1/tan(fov/2) and M11 is that divided by the aspect ratio; M33 is far/(near-far), M34 is −1, M43 is near*far/(near-far) and M44 is 0, which maps view-space depth −near to 0 and −far to 1. The aspect ratio is not validated. |
CreatePerspective, CreatePerspectiveOffCenter | The same near and far validation and depth terms; width, height and the off-centre bounds are not validated. |
CreateOrthographic, CreateOrthographicOffCenter | No validation at all; M33 is 1/(near-far) and M43 is near/(near-far), the same 0-to-1 depth convention. |
Do not transpose a matrix, or swap the near and far terms, inside shared math to make one shader look right. GL-style consumers read a matrix as column-major storage, so the CNAEXT helper Matrix::ToColumnMajor(float out[16]) writes the fields in their declared order M11, M12, …, M44 and the graphics effects (BasicEffect.cpp is one caller) use it at the upload boundary. When an on-screen result looks transposed or depth looks inverted, inspect that renderer's upload and clip-space translation first, then the math.
Which arithmetic is deliberately wider than float
Several results are bit-for-bit reproductions of the XNA 4.0 runtime, and the comments and tests state where each choice was measured. XNA ships as a 32-bit assembly whose float arithmetic runs on the x87 unit, so an expression is accumulated at extended precision and rounded to float only when it is stored. CNA reproduces that with double intermediates in specific places:
| Function | Evaluation at TARGET | Recorded measurement (from the source comments) |
|---|---|---|
Matrix::Multiply(matrix, matrix) | Both operands widened to double; each entry is ((a0*b0)+(a1*b1)) + ((a2*b2)+(a3*b3)) in double, narrowed once when stored | 168 products: a float accumulation reproduces 48, the wide form all 168 |
Matrix::Invert | The 2×2 minors and the four cofactors are float locals (narrowed); the determinant and its reciprocal stay double; every entry is one wide expression narrowed once | 52 inversions: all 52 reproduced; also narrowing the reciprocal reproduces 8, plain float throughout reproduces 3 |
Vector3::Transform(position, matrix) | Each component is ((px*M1c) + (py*M2c)) + ((pz*M3c) + M4c) in double, narrowed once | 60 cases: float reproduces 8, the wide form all 60 |
Vector3::TransformNormal | The same shape without the translation term | 60 further cases, measured with the position transform |
BoundingSphere::CreateFromPoints | Private helpers SquaredLength, WideLength and WideDistance accumulate in double; the seed midpoint and the growth share are computed wide and stored as float | 445 point sets, see the bounding sphere |
Three neighbouring choices are deliberately not wide, and the tests pin them from the other side: CreateRotationX/Y/Z narrow the sine and cosine to float before they reach the matrix; MathHelper::ToRadians is one single-precision multiplication by the single-precision constant 0.017453292519943295f; and ToDegrees multiplies by 180.0f / Pi in single precision rather than by the exactly rounded 57.29578. Everything else is plain float: Matrix::Multiply(matrix, scalar), Add, Subtract, Lerp, Vector3::Transform(vector, quaternion) and, importantly, Vector3::Length, LengthSquared, Distance and DistanceSquared, which sum their squares in float. The oracle-test comments record that XNA's own Length, Distance and DistanceSquared accumulate wide; only the private helpers inside BoundingSphere.cpp follow that rule, and no oracle covers the public Vector3 functions (see the evidence section). The MathHelper interpolators (Hermite, CatmullRom) also use double intermediates, for a different reason: their comments cite lost precision (and, for Hermite, a NaN in place of infinity for large amount), and Vector3::CatmullRom delegates to MathHelper::CatmullRom per component.
An optimisation that changes association, contracts a multiply-add (std::fma, compiler contraction), vectorises a dot product, or substitutes an algebraically equal formula can change the last bit and, downstream, rendered pixels. Read MatrixOracleTests.cpp and the corpus tests/reference/xna40/framework/matrix-oracle.txt before approving such a change. The corpus has 444 cases, and the test asserts the family counts: 168 multiplications (120 random and 48 of the cancel shape, a rotation against its own inverse and the conjugation A-1 T A that a scene transform performs), 42 rotations, 52 inversions, 60 position and 60 normal transforms, and 31 conversions each way. EveryMeasuredCaseAnswersTheSameBits compares raw IEEE-754 words, and ANarrowAccumulationWouldNotHavePassed is a negative control that fails unless a float accumulation disagrees with the corpus on more than 100 entries, so the cases cannot silently stop discriminating. CNA's corpus README records the files as output of the genuine XNA Game Studio 4.0 framework run under Wine, produced by run-matrix-oracle.sh; the values are CNA's recorded oracle and were not re-run for this page. A match on these cases is an empirical match, not a proof for every input, and where a function is not bit-exact by design a test should state its tolerance rather than assume none is needed.
Worked case: Decompose and the sign of a zero
This is a fixed, historical defect that shows how small a numeric detail can be. Matrix::Decompose takes each row's sign from the product of the row's four elements, tested with < 0.0f exactly as FNA's Math.Sign(x) < 0 does (Math.Sign is 0, not −1, for negative zero). An earlier version used std::signbit, which is true for −0.0. Every axis-aligned quarter turn has exact zeros in each row and (-1) * 0 is −0.0, so the sign flipped, the normalised 3×3 became a reflection, and Quaternion::CreateFromRotationMatrix of a reflection returned the identity: a 90-degree node silently lost its orientation. MatrixTest.DecomposeAxisAlignedQuarterTurnRecoversTheRotationRatherThanIdentity now asserts unit, positive scale and the rotation for all three axes. Its documented boundary is MatrixTest.DecomposeReportsPositiveScaleForAMirroredAffineTransform: the fourth-column element (M14, M24, M34) is part of each product and is zero for every affine transform, so the sign is always +1 and a mirrored transform decomposes to positive scales. A reflection has no rotation, so the quaternion that comes back is not a unit rotation (CreateScale(-1, 1, 1) gives (0, 0, 0, 0.7071) and true), and a sheared matrix also reports success. That is FNA's algorithm, not XNA 4.0's: the XNA IL negates the largest axis's scale when the basis determinant is negative and returns false for a shear, and the test's own comment attributes the FNA heuristic to XNA (CNA-BUG-259). Decompose is therefore not a mirroring test; use the sign of Determinant() of the 3×3 for handedness. It returns false with an identity rotation when any scale is within about 1.2e-7 of zero (the file-local epsilon described below).
Other numeric details that are easy to disturb
- There are two epsilons.
MathHelper::MachineEpsilonFloatis computed at static-initialisation time by halving until1 + ecompares equal to 1 infloat, which gives 2−24 (about 5.96e-8) when float arithmetic is evaluated at float precision;MathHelper::WithinEpsilontests|a - b| < MachineEpsilonFloatand is used byCurve::ComputeTangent,CurveKeyCollection::setItemProperty, theHermiteshortcuts andRay.Matrix::Decomposeinstead calls a file-localWithinEpsiloninMatrix.cppwith the literalFLT_EPSILON(1.192092896e-07) and a<=test, twice as large; the source comment calls the two values “typically identical”, which they are not. The C ABI publishesCNA_MATH_MACHINE_EPSILON_FLOATas the literal5.96046448E-8Finmath.h, andCApi_MathValuesSmokecompares the macro with that literal; the C++ tests only assert thatMachineEpsilonFloatis positive and below 1e-4. Matrix::ToStringreproduces XNA's exact outer format with the extra spaces after the first and before the last brace, measured on the genuine importer; text that other layers compare or parse depends on it.- Hash codes are not XNA's integers.
Matrix::GetHashCodesums per-float bit hashes, andBoundingBoxcombinesMinandMaxhashes in a platform-sized integer with a boost-style mix; treat them as consistent withEqualsand nothing more. CheckForNaNsthrowsstd::logic_erroronVector2,Vector3andVector4in every build, but onMatrixandQuaterniononly whenNDEBUGis not defined. No code in the snapshot, tests included, calls it.
Color packing and exceptional floating point
Color presents R, G, B and A but stores one 32-bit word in AABBGGRR order: R in bits 0 to 7, G in 8 to 15, B in 16 to 23 and A in 24 to 31. A renderer, image path or binding that treats that word as AARRGGBB swaps red and blue even though every getter looks right; ColorTest.RedPackedValueIsAabbggrr, BluePackedValueIsAabbggrr and CornflowerBluePackedValue pin the order. The routes that make a byte from something else deliberately use different rounding, and consolidating them behind one generic float-to-byte helper is the mistake to avoid:
| Route | Rule at TARGET |
|---|---|
Color(float r, g, b[, a]), Color(Vector3), Color(Vector4), PackFromVector4 | Multiply the unit value by 255, saturate to 0–255, round to nearest with ties to even through CNA::Internal::ClampAndRound (core's PackedRounding.hpp), and map NaN to 0. Infinities saturate. The Vector3 form sets alpha to 255. |
Color(int r, g, b[, a]) | Clamps each argument to 0–255; no rounding is involved. |
Color(byte …), setPackedValueProperty | Stored as given. |
Lerp(a, b, amount) | Clamps amount to 0–1 with MathHelper::Clamp, interpolates each channel in float over byte units, then truncates toward zero after a guarded narrowing (a non-finite value becomes 0 and the rest is clamped to ±1,000,000) and the integer constructor clamps to 0–255. The midpoint of black and white is therefore 127, not 128. A NaN amount is defined and yields 0 in every channel, alpha included (ColorTest.LerpWithNaNAmountIsDefinedNotUndefinedBehavior). |
Multiply, operator* | Scales each channel in float with the same guarded truncation; a negative scale clamps to 0. |
FromNonPremultiplied(Vector4) and (int r, g, b, a) | The float overload premultiplies RGB by W and then uses the rounding float constructor; the integer overload divides r*a by 255 with integer division. ColorTest.FromNonPremultipliedOverloadsAgreeWithXna shows both give (128, 0, 32, 128) for the same input. |
ToVector3, ToVector4 | Each byte divided by 255. |
The source comments contrast these rules with FNA: FNA's float constructor truncates (Color(new Vector4(0.25f, 0.5f, 0.75f, 1)) is {63, 127, 191, 255} there and {64, 128, 191, 255} in the measured XNA runtime), FNA's PackFromVector4 neither clamps nor rounds, and CNA follows the measured XNA runtime. The measurements are the 28 color/* cases of the 74-case framework-packing-oracle.json (its other cases are the packed-vector types and six bounding-sphere ties); XnaFrameworkPackingTests.cpp in the graphics tree reproduces every case and its CoversEveryMeasuredCase fails if a case is added without a reproduction. The same core helpers (RoundHalfToEven, ClampAndRound, UnpackUNorm, UnpackSNorm) serve fourteen integer-packed graphics types, so a change to them is a graphics change as well: RoundHalfToEven is written out with floor and a tie correction so it does not depend on the current floating-point rounding mode, and it casts through long long, which is why ClampAndRound screens NaN and out-of-range values first (casting NaN or an out-of-range float to an integer is undefined in C++). ClassicTextureFormatTests.cpp is a broader cross-layer check on texture formats that draw through Color. ColorTests, that corpus and the C ABI's CApi_ColorSmoke together are the evidence for this section; the ties-to-even claim rests on the recorded XNA measurements and unit tests, not on the site having run them.
Geometry and curve failure boundaries
The bounding volumes, Plane, Ray and Curve answer containment, intersection and interpolation questions independently of any renderer. ContainmentType (Disjoint 0, Contains 1, Intersects 2), PlaneIntersectionType (Front 0, Back 1, Intersecting 2) and the curve enums (CurveLoopType 0–4, CurveTangent 0–2, CurveContinuity 0–1) are public behaviour with fixed numeric identities that the C ABI repeats, not helper return codes. Error behaviour is specific to each function, and no single policy applies:
Failure map
| Where | Behaviour at TARGET |
|---|---|
Matrix::CreatePerspective* | std::invalid_argument for a non-positive near or far distance, near >= far, and, for the field-of-view form, a field of view that is not strictly between 0 and 3.141593f. |
Other Matrix builders, Invert, Plane::Normalize, Vector3::Normalize | No validation: singular or zero-length input divides by zero and returns non-finite values. |
BoundingSphere::CreateFromPoints | std::invalid_argument for an empty list. |
BoundingBox::CreateFromPoints | System::ArgumentException for an empty list — a different exception family from the sphere's. |
BoundingBox::GetCorners(vector&) and BoundingFrustum::GetCorners(vector&) | A destination smaller than eight elements throws System::ArgumentOutOfRangeException for the box and std::out_of_range for the frustum. |
Array overloads of Vector2/3/4::Transform and TransformNormal | std::out_of_range for a negative index or length or a range beyond either array. |
Curve::ComputeTangent(index, …), CurveKeyCollection index, RemoveAt, CopyTo | std::out_of_range; the C++ code adds negative-index guards that the FNA reference does not have. |
BoundingFrustum::Intersects(Ray) | Partial: System::NotImplementedException when the ray origin lies exactly on a frustum plane (see below). |
Color | No exceptions: NaN, infinities and out-of-range inputs are defined (see above). |
The C ABI's exception barrier turns these families into result codes (std::invalid_argument and std::out_of_range into invalid-argument results, std::logic_error into an invalid-state result, NotImplementedException into not-supported), so changing the exception type of a math function changes what a binding sees; the header of cna_bounding_frustum_intersects_ray documents the not-supported case. Inspect the target function and its tests before deciding what an invalid input should do.
BoundingSphere::CreateFromPoints
The growth algorithm is XNA's own and is the most heavily measured geometry code in the module. The seed pair is chosen by the distance between each axis's extreme points, not by the square of it (they differ where two squared spans round apart but their single-precision roots agree); ties are resolved with >=, so the later of two equally wide axes seeds the sphere (FNA uses >, and a right triangle's sphere comes out mirrored under that rule); the seed radius is half the pair's distance, not the distance from the midpoint to an end; and each point that lies beyond the radius sets the new radius to the mean of the old radius and the distance and slides the centre by 1 - r'/d of the difference. Every intermediate is stored as float and evaluated wide between stores. BoundingSphereOracleTests.cpp compares bits against tests/reference/xna40/framework/bounding-sphere-oracle.txt, 445 point sets in five families (200 seed, 60 grow, 150 step, 21 box, 14 span), with two negative controls (ChoosingTheAxisBySquaredSpanWouldNotHavePassed, TheFormerGrowthWouldNotHavePassed). The box family models a mesh's repeated control points, where whether a point that lies exactly on the sphere grows it is decided in the last bit. CreateFromFrustum builds the sphere from GetCorners().
BoundingFrustum
BoundingFrustum.cpp stores its matrix and derives six normalised planes in the order Near, Far, Left, Right, Top, Bottom plus eight corners, and recomputes both whenever setMatrixProperty runs. The near plane comes from the third column alone, which is consistent with the 0-to-1 depth convention above. The plane normals point outward, so a Front classification against any plane means “outside” and yields Disjoint. Three behaviours to keep in mind:
Contains(point)ends its plane loop at the first plane the point lies exactly on and reportsIntersects, even if a later plane would have rejected it.Intersects(Ray)answers no hit for any ray whose origin is outside the frustum, whatever its direction, a distance of 0 for an origin inside, and throwsNotImplementedExceptionfor an origin exactly on a plane; a real entry distance is never computed. The three tests cover the inside and outside origins and the output-parameter form, not the boundary case. The user-level statement is on Math types.- Equality and hashing compare and hash only the stored matrix.
Curve, CurveKey and CurveKeyCollection
CurveKeyCollection::Add inserts a key in ascending position order (a key equal to an existing position goes after it, and duplicates are accepted), Clone re-adds every key, and setItemProperty replaces a key in place when the new position is within the machine epsilon of the old one and otherwise erases it and re-inserts it, so the index of the new key can change. CurveKey has no position setter, so a position changes only through the collection. The non-const indexer returns a mutable CurveKey&; Curve::ComputeTangent mutates keys through it, and any code that edits keys changes the interpolation path without touching Curve::Evaluate. Curve owns its collection by value.
Case in Curve::Evaluate | Result |
|---|---|
| No keys / one key | 0 / that key's value |
| Before the first key | Pre-loop: Constant the first value; Linear first.Value - first.TangentIn * (first.Position - position); Cycle, CycleOffset (shifted by cycle * (last.Value - first.Value)) and Oscillate map the position back into the key range |
| After the last key | Post-loop with the same modes; Linear uses last.Value + first.TangentOut * (position - last.Position) — the first key's out tangent, which CurveTest.EvaluatePostLoopLinear pins with its own comment |
| Inside the range | Cubic Hermite between the neighbouring keys using the previous key's TangentOut and the next key's TangentIn; if the previous key has Step continuity the previous key's value is held, subject to the first caveat below |
Four details in the source deserve scoped caution; each was found by reading and none is exercised by a test. First, the Step branch returns the next key's value when the evaluated position is at least 1.0f and the previous key's value otherwise: the comparison is against the absolute constant 1, not against the segment's end, and the only Step test evaluates at 0.5 in a segment from 0 to 1. That branch follows FNA's Curve.cs and diverges from XNA 4.0, whose IL (Curve.FindSegment, Curve.Hermite) compares the segment-normalised parameter with 1, so XNA keeps the previous key's value until the segment's end; this was established by reading the IL, and no runtime oracle has compared the two. Second, ComputeTangent zeroes a Smooth tangent when the neighbour position span is near zero, with two different tests for the two tangents: the in tangent tests WithinEpsilon(pn, 0) (span below about 6e-8) and the out tangent tests |pn| < denorm_min. XNA 4.0's IL tests the neighbour value difference against 1.1920929E-07f for both, so this is FNA's code and diverges from XNA as well. The only smooth-tangent test uses equal values, so it cannot tell the formulas apart. Third, the cyclic loop modes divide by last.Position - first.Position with no guard, so a collection whose first and last key share a position turns an out-of-range evaluation into an infinity cast to int; XNA's IL caches an inverse time range that stays 0 unless the range exceeds float.Epsilon. Fourth, duplicate positions are accepted by Add and unguarded in Evaluate, which divides by the segment width; XNA's FindSegment yields parameter 0 for a segment narrower than 1e-10. A related divergence sits in the table above: the post-loop Linear case uses the first key's out tangent where XNA's IL uses the last key's, again FNA's behaviour. Treat all of these as behaviour to preserve or fix deliberately, with a test.
The module adds no locks (see the thread and callback map). Independent values may be computed on different threads; concurrent mutation of the same value, Curve or CurveKeyCollection is not a guarantee anywhere in the source. C ABI math routes marshal copies across the boundary, so a change to a field, an enum value, packing or an exceptional input needs the C header, its smoke test and binding review as well (C API internals).
How to modify it safely
- A transform bug. Write the smallest failing
MatrixTestorVector3Testcase that names the row or column convention it relies on, then runMatrixOracleTests. Remember the limits of that oracle: it coversMultiply,Invert, the three rotation builders, position and normal transforms and the two angle conversions. It does not coverCreateLookAt, the perspective builders orQuaternion:MatrixTest.CreateLookAtM44IsOnechecks onlyM44, the perspective tests check the thrown cases andM34, and the view and projection terms are exercised only indirectly by consumers in the content and graphics-ext test trees. For those, add a value-level test first, then a renderer pixel check. - A colour change. Include 0, 1, halves and exact ties, NaN, infinities and a packed-word round trip; run
ColorTestsand, from the graphics tree,XnaFrameworkPackingand the texture-format tests. Keep the AABBGGRR order and the separate rounding of the constructors,LerpandMultiply. - A geometry change. Test inside, boundary or tangent, and outside cases plus degenerate arguments in the owning suite, and say in the test which exception family is expected.
- A curve change. Cover keys before and after the range, each loop mode and each tangent and continuity mode, including a case whose values are not all equal.
- A public type change. Add C ABI value tests and check the external binding marshalling, and consider the converters in Framework.Design: a property name, field or constructor change there is a second public surface. Also check
XmlSerializationEXT.hpp.
Build and test routes: the unit-math build preset builds the focused CnaMathTests executable after cmake --preset unit (STUB renderer, Debug); it is EXCLUDE_FROM_ALL and is not a separate CTest registration, while the aggregate CnaTests discovers each case as its own CTest entry. The unit-core-math-unity preset builds core and math tests with the unity pilot. When outputs leave the module, run the aggregate and the consumer or renderer tests as well (Building: tests, Test architecture, I need to change public XNA behavior).
Self-review questions: did the change alter arithmetic association, contract a multiply-add or change a float conversion? Does Color's packed word stay AABBGGRR? Did a new enumerator or reordering change the C++ and C numeric identities? Can a serialized or foreign caller still read the same layout? Are the new tests true oracles or round trips, or do they compare a function with another implementation that changed with it?
What the evidence covers, and what it does not
Source-read at 009d40f5 and, where stated, recorded in CNA's own corpora: the row-vector convention, the view and projection terms, the wide-accumulation table, the Decompose case, the colour rules, the failure map and the frustum and curve behaviour. CNA's corpora record the genuine XNA framework's answers for the 444-case matrix corpus, the 445-set bounding-sphere corpus and the 28 colour cases, and CNA's tests compare CNA's output with them bit for bit; those are the only oracle-compared parts of this module, and whether the tests pass at TARGET was not executed here. The following carry no oracle at TARGET, and the deeper per-type ABI and oracle coverage remains open:
CreateLookAt, the perspective and orthographic builders,Quaternion,Plane,Ray,BoundingBox,BoundingFrustumandCurve: unit tests only.- The public
Vector3::Length,LengthSquared,DistanceandDistanceSquared, which accumulate infloatalthough the recorded measurement says XNA accumulates wider. - Hash codes,
ToStringforms other thanMatrix's, and exception types (the failure map records what the code does, not that XNA does the same). - Structure size and alignment beyond the
Colorvtable test, and the C ABI value routes, which were not built or run for this page.
Curated source and test route
math/CMakeLists.txt, thenMatrix.hpp: the target boundary, the public fields and the CNAEXT-marked members.Matrix.cpp(CreateLookAt, the perspective builders,Multiply,Invert,Decompose) andVector3.cpp(Transform), thenMatrixOracleTests.cppandmatrix-oracle.txtfor the measured expectations.Color.hpp,Color.cpp,ColorTests.cppand core'sPackedRounding.hpp: packed layout, constructor rounding and the truncating interpolation.BoundingSphere.cppwith its oracle test, thenBoundingFrustum.cppandCurve.cppwithCurveKeyCollection.cpp: measured growth, stateful geometry and key interpolation; read the matching geometry and curve tests before generalising an edge case.math_values.handCnaCApiGeometry.cpp: how values and failures cross the C boundary.
For where the module sits among the others see the module index; for task routes, the Maintainer Handbook.
Deep dives on this topic
Long-form pages that explain the exact semantics, invariants and evidence behind this subject.
- Coordinate and composition conventions: handedness, row vectors, depth and clip space — CNA's right-handed basis, row-vector matrices applied in reading order, quaternion products that run the other way, the [0,1] depth range derived three times, clip-space W and the ToColumnMajor bridge.
- Curve evaluation: keys, tangents, loop types and the XNA reference — How CNA's Curve evaluates: sorted keys, the per-segment Hermite basis, Step continuity, the five loop types, smooth tangents and degenerate curves, each compared with the XNA 4.0 algorithm.
- Math value types in C++: object layout, equality, hashing and API shape — Which CNA math types carry a vtable, why Color is 24 bytes on 64-bit hosts, the internal vertex stream structs, output-reference aliasing, exact equality, hash and ToString differences, and the split argument exceptions.
- Planes, rays and bounding volumes: exact containment and intersection semantics — Half-space conventions, plane transforms, ray tolerances, box corner order, sphere and frustum containment rules in CNA, compared function by function with XNA 4.0, with workarounds for every mismatch.
- Rectangle, Point and Color: integer geometry and packed colour semantics — Rectangle's half-open edges, touching and empty rules, Point's missing Vector2 bridge and rounding, Color's AABBGGRR word, the 141/140/139 named-colour counts, construction rounding and premultiplied alpha.
- Vector, Matrix and MathHelper numerics: interpolation, clamping and degenerate inputs — What CNA's vector, matrix and MathHelper functions return for out-of-range amounts, inverted clamps, NaN, zero vectors, singular matrices and bad camera input, compared with XNA 4.0, plus the precision and test evidence.
Known issues in this area
Current defects, gaps and limitations at this snapshot that touch this subject.
- CNA-BUG-001: Plane::Transform(plane, matrix) transposes the inverse in place through a non-aliasing-safe Matrix::Transpose — Plane::Transform with a Matrix passes one object as both source and destination of Matrix::Transpose, which is not aliasing-safe, so any transform whose inverse is not symmetric (a translation, most rotations) yields a w
- CNA-BUG-002: BoundingSphere::Contains(const BoundingFrustum&) never returns Disjoint — When any frustum corner lies outside the sphere, BoundingSphere::Contains(frustum) always answers Intersects, because its distance test compares a constant zero, so a frustum nowhere near the sphere is reported as overla
- CNA-BUG-003: BoundingBox::Contains(const BoundingFrustum&) never returns Disjoint and answers Contains when only the first frustum corner is outside — BoundingBox::Contains(frustum) classifies the frustum from its corners only, so a frustum wholly separate from the box is reported as Intersects, and one whose first corner alone lies outside is reported as Contains.
- CNA-BUG-004: BoundingFrustum::Intersects(Ray) never computes an entry distance: rays starting outside always miss, and an origin on a plane throws — BoundingFrustum::Intersects(Ray) and Ray::Intersects(BoundingFrustum) report no hit for every ray whose origin is outside the frustum, whatever its direction, return 0 for an origin inside, and throw NotImplementedExcept
- CNA-BUG-025: PlaneIntersectionType's Doxygen swaps the meaning of Front and Back — PlaneIntersectionType.hpp documents Front as the negative half-space and Back as the positive one, the opposite of what every CNA classification returns and of XNA's meaning.
- CNA-BUG-026: Matrix::CreatePerspectiveFieldOfView accepts a field of view of exactly MathHelper::Pi — The upper guard compares against the literal 3.141593f, which rounds to the float one step above MathHelper::Pi, so a field of view of exactly Pi passes validation and builds a degenerate projection instead of throwing a
- CNA-BUG-027: Vector Clamp, Min and Max resolve inverted ranges and NaN operands differently from XNA — Vector2/3/4::Clamp use std::min(std::max(v, min), max), so max wins when min > max, whereas XNA's vector Clamp and CNA's MathHelper::Clamp let min win; vector and scalar Min/Max also return a different operand from XNA f
- CNA-BUG-028: Curve::ComputeTangent Smooth tangents test the key spacing against two different epsilons, where XNA tests the value difference against one — 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 b
- CNA-BUG-063: Vector3::Length, LengthSquared, Distance and DistanceSquared accumulate in float, not at the width CNA measured XNA to use — CNA's own measurement of the XNA 4.0 runtime found Vector3.Distance summed at extended precision (a float sum matched none of 200 pairs), and BoundingSphere.cpp follows that rule, but the public Vector3 length and distan
- CNA-BUG-064: Curve::Evaluate reads the wrong key for Step continuity away from position 1 and for a Linear post-loop — 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 inst
- CNA-BUG-065: Curve::Evaluate divides by zero for coincident key positions, returning NaN and converting infinity to int in the cyclic loop modes — GetNumberOfCycle and GetCurvePosition divide by the key span and the segment width without a guard, so curves with coincident keys yield NaN or undefined behaviour where XNA returns the first key's value.
- CNA-BUG-066: BoundingFrustum::Contains(Vector3) answers Intersects for a point exactly on a plane and skips the remaining planes — BoundingFrustum::Contains(point) returns Disjoint for any positive plane distance and, for a distance of exactly zero, Intersects without testing the remaining planes; XNA never answers Intersects for a point and uses a
- CNA-BUG-067: Math argument failures throw a mix of Sharp Runtime and std:: exception types that differs between near-identical functions — BoundingBox throws Sharp Runtime ArgumentException types as XNA does, but BoundingSphere::CreateFromPoints, BoundingFrustum::GetCorners and the perspective builders throw std::invalid_argument or std::out_of_range.
- CNA-BUG-068: Vector2/3/4 and Matrix division by a scalar divide every component where XNA 4.0 multiplies by one reciprocal — Divide(v, s), operator/(v, s) and Matrix::Divide(m, s) divide per component where XNA multiplies by 1/s, so results can differ in the last bit; Vector3::operator/= alone uses the reciprocal.
- CNA-BUG-069: Ray::Intersects(BoundingBox) and Ray::Intersects(BoundingSphere) differ from XNA 4.0 on degenerate and boundary rays — A zero-direction ray inside a box returns no hit (XNA: 0), axis parallelism uses a 2^-24 threshold where XNA uses 1e-6, and a ray starting exactly on a sphere and pointing outward returns no hit (XNA: 0, because XNA's in
- CNA-BUG-070: BoundingSphere::Contains(Vector3) and Contains(BoundingSphere) return different ContainmentType values from XNA 4.0 — A point exactly on the surface is Intersects in CNA (XNA: Disjoint), and Contains(BoundingSphere) tests d^2 <= (R - r)^2, losing the sign of R - r, so a small sphere reports that it Contains a larger concentric sphere (X
- CNA-BUG-071: BoundingFrustum::Intersects differs from XNA 4.0: box and sphere overloads use a conservative plane test, and the Plane overload counts an on-plane corner as Intersecting — The box and sphere overloads report true for volumes just outside an edge or corner where XNA's exact GJK query reports false, and the Plane overload treats a zero corner distance as Intersecting (XNA: Back).
- CNA-BUG-251: BoundingSphere(Vector3 center, float radius) accepts a negative radius, where XNA 4.0 throws ArgumentException — CNA's BoundingSphere(Vector3, float) constructor stores any radius, while XNA 4.0's throws ArgumentException for radius < 0, so a negative radius is silently accepted.
- CNA-BUG-252: Vector3, Vector4, Quaternion and Matrix GetHashCode add raw int bit patterns with signed arithmetic, which overflows (undefined behaviour) for ordinary values — The four GetHashCode functions sum FloatHash results as int with plain +; three 1.0f components already exceed INT_MAX, so the sum is signed-overflow undefined behaviour, while Vector2::GetHashCode already sums as unsign
- CNA-BUG-259: Matrix::Decompose follows FNA's algorithm, not XNA 4.0's: a mirrored matrix decomposes to positive scales and a non-unit quaternion, and a sheared matrix reports success — Matrix::Decompose never checks the determinant: a mirrored matrix returns positive scales, a non-unit quaternion and true where XNA 4.0 negates the largest scale, and a sheared matrix returns true where XNA returns false
- CNA-VGAP-001: Matrix camera builders have no XNA oracle and no full-matrix value test — CreateLookAt and the perspective and orthographic builders are asserted only on single terms and covered indirectly by consumer tests; CNA's XNA matrix oracle does not include them.
- CNA-VGAP-002: BoundingSphere::CreateFromBoundingBox and the SmoothStep/Hermite/CatmullRom helpers use different arithmetic from XNA 4.0, and no oracle measures last-bit agreement — CreateFromBoundingBox and the SmoothStep/Hermite/CatmullRom helpers compute with formulas or precision that differ from XNA's, and only CreateFromPoints has an XNA oracle.
Related pages
The same subsystem is explained at four altitudes. These are the neighbouring pages at each one.
- User guide
- Math types guide · Math types: numerical behaviour · Math types: bounding types · Math types: curve types · PackedVector types · Verification: oracle corpus · C API route families
- Architecture
- Architecture overview · Graphics architecture
- Maintainer workflow
- Using the XNA oracle as evidence · I need to change public XNA behavior · Thread and callback map · Maintainer Handbook
- Tests and validation
- Test architecture and change recipes
- Reference
- Module index · Test target index · Public header index