Chapter 8 Geometry, Color, Curves, and C++ Value Layout
Chapter 7 gives the method-level reference for CNA’s math types. This chapter takes the complementary view: which geometric conventions cross subsystem boundaries, which C++ object layouts differ from XNA value types, and which apparently harmless casts or degeneracies become observable defects.
8.1 The value-type resemblance has a hard limit
XNA presents vectors, colours, planes, bounds, and curve keys as compact managed value types. CNA preserves their names and most operations, but it cannot preserve that representation merely by spelling the same fields in C++. Several public types inherit polymorphic interfaces:
| Type family | Why it is polymorphic | Practical consequence |
|---|---|---|
| Color |
Implements IPackedVectorT<
UInt32>, whose base has virtual operations and a virtual destructor. |
The object has a vptr; it is not four raw RGBA bytes. |
| Built-in vertex types | Implement IVertexType. | A public vertex object cannot be uploaded by copying sizeof(T) bytes. |
|
BoundingBox, BoundingSphere,
BoundingFrustum, CurveKey |
Implement IEquatable<T>. | Their field list is not their complete object representation. |
| Plain math types | Vector2, Vector3, Vector4, Matrix, Quaternion, Plane, Ray, Point, and Rectangle have no base class. | Their layout is simpler, but padding and C++ object rules still belong to the ABI, not to the XNA contract. |
At the pinned revision, sizeof(Color) == 24. The single packed value is still AABBGGRR on a little-endian machine, yet the object begins with a vptr. Reinterpreting a Color* as pixel bytes would overwrite or read the virtual-table pointer. CNA’s own GetBackBufferData<Color> path therefore reads native bytes into a temporary byte vector and constructs each result as Color(r,g,b,a). Texture3D and TextureCube use the same conversion rule.
This is not theoretical portability advice. A regression test explicitly rejects the old raw cast, and the graphics module documents the exact 24-byte size where it converts texture data. Application code should use component access, PackedValue, or the typed texture APIs; it should never serialize a public math object with memcpy(sizeof(T)) unless the file format is deliberately CNA-ABI-specific.
8.2 GPU streams are a second, plain-data representation
The graphics core solves the polymorphic-vertex problem with a parallel internal stream layer in modules/graphics/include/CNA/Internal/Graphics/BuiltInVertexStreams.hpp. These structs have no virtual base, and both their total sizes and member offsets are compile-time assertions:
| Stream representation | Bytes |
|---|---|
| PositionColorStream | 16 |
| PositionTextureStream | 20 |
| PositionColorTextureStream | 24 |
| PositionNormalTextureStream | 32 |
| PositionNormalTangentTextureStream | 48 |
| PositionNormalTextureSkinnedStream | 52 |
| PositionNormalTangentTextureSkinnedStream | 68 |
Every built-in VertexDeclaration takes its stride from the matching stream struct, not from the public vertex class. Conversion is therefore an explicit semantic operation: positions and texture coordinates are copied as floats, while Color contributes only its packed four-byte payload. Chapter 18 follows these bytes through binding, multi-stream normalization, and renderer capability gates.
8.3 Planes and half-spaces
CNA uses the plane equation
The three-point constructor is the only constructor that normalizes automatically and computes . DotCoordinate returns the signed expression above. Operationally, PlaneIntersectionType::Front is the positive-normal side and Back is the negative side; the enum’s own prose comments reverse those meanings, while Plane, BoundingBox, and BoundingSphere consistently implement the positive-side interpretation.
8.3.1 The inverse-transpose path is presently wrong
Transforming a plane should multiply it by the inverse transpose of the point transform. CNA’s Plane::Transform intends to do that, but calls the out-parameter transpose as Matrix::Transpose(m, m). That overload writes fields while it is still reading the same object, so it is not alias-safe. For a non-symmetric inverse it progressively mixes old and new fields and produces a symmetrization rather than a transpose.
A minimal counterexample is the plane transformed by a pure translation along X. The correct plane is unchanged. The aliased implementation instead produces a normal proportional to , silently rotating it. Both existing transform tests use the identity matrix, whose inverse is symmetric, so they cannot expose the error. This is recorded as CNA-BUG-001; portable application code should avoid Plane::Transform for nontrivial matrices until the source uses a separate transpose destination.
8.4 Bounds encode several non-obvious conventions
BoundingFrustum independently confirms CNA’s Direct3D clip volume. Its extracted planes describe and , and their normals point outward. Consequently a point on the positive side of any plane is outside. This agrees with the projection builders in Chapter 7; it is a second derivation, not a repeated assertion.
The three bounding types do not share one quality level:
-
•
BoundingBox::GetCorners preserves XNA’s fixed eight-corner order. Its containment tests use half-space and min/max logic, but Contains(BoundingFrustum) retains legacy reversed-question behavior.
-
•
BoundingSphere builds point sets with CreateFromPoints, using a Ritter-style expansion. Under a non-uniform transform, the radius is multiplied by the largest row-vector scale. Its frustum Contains path never returns Disjoint because the required minimum-distance accumulator is never populated.
-
•
BoundingFrustum caches six planes and eight corners. Its Intersects(Ray) main case throws NotImplementedException; source and test absence must not be mistaken for a completed geometric query.
Those gaps contain no TODO marker. They are a useful reminder that marker searches find intent labels, not semantic incompleteness.
8.5 Rectangle, Point, and edge ownership
Rectangle::Contains is half-open: the left and top edges belong to the rectangle, while the right and bottom edges do not. Intersects requires positive overlap, so rectangles that merely touch at an edge are not intersecting. Center uses integer arithmetic, and IsEmpty is true only when all four fields are zero; a zero-width rectangle at a nonzero position is not the singleton Empty value.
Point is simply two 32-bit integers. It deliberately has no implicit Vector2 bridge. Make rounding policy visible when converting a continuous coordinate to a pixel or cell: truncation, floor, and nearest are different behaviors for negative values.
8.6 Color is packed, named, and polymorphic
The public surface contains 141 named colours: 140 opaque values plus Transparent. There are 139 distinct packed values because Aqua/Cyan and Fuchsia/Magenta are aliases. Three counts can therefore all appear plausible; only a count that names declarations, opacity, or distinct packed values is meaningful.
Construction from float vectors clamps components to , scales to , and packs the four bytes. Integer constructors clamp to . Multiplication scales alpha as well as RGB. These behavioral facts are independent of the 24-byte C++ object layout: packing defines the value, not permission to treat the entire object as packed storage.
8.7 Curves: ordered keys and explicit extrapolation
Curve evaluates cubic Hermite segments between sorted CurveKey entries. Duplicate positions are allowed; insertion and setItemProperty preserve sorted order and can reposition a key. CurveContinuity::Step selects the left value until the segment endpoint, while smooth keys use their incoming and outgoing tangents.
Five loop modes govern positions outside the key range. Constant and Linear hold or extrapolate an endpoint; Cycle, CycleOffset, and Oscillate repeat a span. The repeating modes depend on a stable cycle count and on a nonzero range. CNA guards some degenerate spans, but its tangent computation uses different near-zero tests for incoming and outgoing sides. Treat a duplicate-position curve as an authored edge case and test it with the same evaluator that will ship.
Curves also cross the content boundary. CNJ can carry a self-contained Curve payload, and the XNB reader has a concrete CurveReader; a successful parse proves field recovery, while the runtime evaluator and loop behavior remain separate claims.
8.8 Exceptions are part of the port
Sibling operations do not consistently choose the same exception family. With empty input, CreateFromPoints throws different exceptions. BoundingBox uses ArgumentException; BoundingSphere uses the standard invalid-argument exception.
An undersized corner destination throws ArgumentOutOfRangeException for the box and std::out_of_range for the frustum. Existing tests encode these differences, so a catch block written for one type is not a generic geometry policy.
Degenerate numeric inputs are usually quieter: normalizing a zero vector or quaternion and inverting a singular matrix can yield NaN or infinity. The module’s strong argument checks are concentrated in projection factories. Validate user-authored axes, scales, planes, and key spans at the application boundary rather than expecting every math primitive to reject them.
8.9 What the math tests prove
The pinned math module contains 818 statically counted GoogleTest definitions. They use EXPECT_FLOAT_EQ or explicit tolerances of , , and ; none uses captured XNA or MonoGame numeric oracle values. The suite is strong on API wiring and many analytic identities, but it does not independently pin matrix/quaternion composition order, the depth mapping, corner order, singular inputs, or the non-identity plane transform.
For a port, pair these types with tests derived from the original game’s observable behavior: screen-space points, collision classifications, curve samples, or captured reference values. A familiar type name establishes lineage. It does not erase C++ layout, exception, and evidence boundaries.