Crafter.Math/AGENTS.consumer.md
2026-10-31 06:52:22 +01:00

8.7 KiB

Crafter.Math

The Crafter engine's math library: a C++ module with no implementation units and no dependencies, so everything is a template or an inline body compiled into your translation units with your build's --target and --march. Source and issues: https://forgejo.catcrafts.net/Catcrafts/Crafter.Math. Namespace Crafter. The README there documents the whole surface; these notes are what an agent working in a consumer needs.

Which type

Vector<T, Len, Alignment = 0> is plain data, for vertex and uniform structs, events, anything stored or passed around. .x .y .z .w (and .r .g .b .a) alias v[]; constructors exist for Len 2, 3 and 4 (Vector<float, 2>{x, y}); + - * / with vectors and scalars, += and friends, ==, unary -; statics Dot(a, b), Cross(a, b) (Len == 3), Normalize(a); members Length(), LengthSq(), Normalize(), Invert(). std::format("{}", v) prints {x,y,z}.

  • Alignment is the number of stored elements. 0 means Len, naturally aligned: Vector<float, 3> is 12 bytes aligned 4. Any N >= Len stores T v[N] and marks the struct __attribute__((packed)), alignment 1: Vector<float, 3, 3> is 12 bytes aligned 1 (the vertex type Crafter.Graphics uses), Vector<float, 3, 4> is 16 bytes, a vec4 slot. Conversion between alignments is implicit.
  • Arithmetic between two lengths covers the shorter one and leaves the other lanes of the result uninitialised, and == compares the shorter. The member Normalize() divides without a zero check; the static Normalize(a) checks.

VectorF32<Len, Packing> is one SIMD register holding Packing vectors of Len floats, for math-heavy loops. Construct from a const float* or broadcast one float; Load(ptr), Store(ptr), and Store<float>() returning a std::array. Operators as above; statics Cross (Len == 3), MulitplyAdd(a, b, c) (a*b + c), MulitplySub(a, b, c) (a*b - c), Rotate(v, q) and RotatePivot(v, q, pivot) by a VectorF32<4, Packing> quaternion, QuanternionFromEuler(halfAngles), Blend<{{bools}}>(a, b); members Sin(), Cos(), SinCos(), Shuffle<{{i, j, k}}>(), Negate<{{bools}}>(), ExtractLo<N>(). Inside a template write v.template Store<float>().

  • Load, Store and the pointer constructor move the whole register, AlignmentElement floats (4, 8 or 16), not Len * Packing. Give them a buffer that big (alignas(16) float buf[4] = { x, y, z, 0 }; is the idiom in the tests), zero the padding lanes on the way in and ignore them on the way out. The pointer need not be aligned.
  • Dot, Length, LengthSq and Normalize are batched with the arity fixed at compile time: Normalize, Length and LengthSq take exactly BatchSize vectors, Dot takes 2 * BatchSize (a0, b0, a1, b1, ...). They return VectorF32<1, Packing * BatchSize> with one lane per input vector (Normalize returns std::array<VectorF32<Len, Packing>, BatchSize>). There is no two-argument Dot: for one product pass the same pair BatchSize times and read lane 0, as MatrixRowMajor::operator* does, or use Vector<float, 3>::Dot.
  • BatchSize is AlignmentElement / Packing, 4 for every VectorF32<Len, 1>. OptimalPacking is how many Len-vectors fill one register (table below). Shapes convert implicitly, copying the lanes they share.
  • The x86 backend lacks const on the scalar operators, Sin, Cos, SinCos and Shuffle: call them on non-const objects.

VectorF16<Len, Packing> is the same surface for _Float16 where the target has native half arithmetic: x86 with AVX512-FP16, AArch64 with ARMv8.2-FP16. Everywhere else, including every x86 tier below sapphirerapids and all of RISC-V, it is using VectorF16 = VectorF32, converting on Load and Store from _Float16*. Never overload a function on VectorF16 and VectorF32 both, and never assume its lanes are halves.

MatrixRowMajor<float, 4, RowSize, 1> is four-column and row-major, each row a VectorF32<4, 1> in the public rows[]; constructors take the 12 or 16 floats row by row. Only T = float, CollumSize = 4, Repeats = 1 exist.

  • <float, 4, 3, 1> (48 bytes) is the column-vector shape: M * VectorF32<3, 1> applies it with an implicit w = 1, TransformNormal(v) with w = 0; translation sits in column 3. Store(float*) writes the 12 floats row-major, which is VkTransformMatrixKHR: m.Store(reinterpret_cast<float*>(instance.transform.matrix)).
  • <float, 4, 4, 1> (64 bytes) is the row-vector shape for the GPU side (v * M, translation in row 3). There is no matrix-times-vector operator for it in C++. operator* between two 4x4s, Transpose(), LookTo(eye, direction, up) and LookAt(eye, focus, up) are 4x4 only.
  • Both shapes have Identity(), Scaling(x, y, z), Translation(x, y, z) and Rotation(pitch, yaw, roll) in radians (each also from a VectorF32<3, 1>), Rotation(Vector<float, 4> quaternion), TRS(t, q, s) (scale, then rotate, then translate: a glTF node), InverseAffine() (any rotation, scale and shear plus a translation, never a projection) and Decompose() returning (t, q, s), with a mirroring expressed as a negative X scale. Each means the same transform in both shapes. std::format prints both.

Quaternions are Vector<T, 4> in (x, y, z, w) order, unit, w the scalar part, applied by Vector<T, 3>::Rotate(v, q) and RotatePivot(v, q, pivot); identity is (0, 0, 0, 1). The functions are statics of Vector<T, 4> and are spelled Quanternion: QuanternionMultiply(a, b) is the Hamilton product, b first then a (Rotate(v, a*b) == Rotate(Rotate(v, b), a)); QuanternionConjugate(q) is the inverse rotation; QuanternionFromAxisAngle(unitAxis, radians) is right-handed (+90° about Z takes X to Y); QuanternionFromUnitVectors(from, to) is the shortest arc, defined for opposite directions too; QuanternionFromBasis(right, up, forward); QuanternionFromEuler(rollHalf, pitchHalf, yawHalf) takes half angles; QuanternionNlerp(a, b, t) and QuanternionSlerp(a, b, t) take the shorter arc and return unit quaternions.

Intersection tests (IntersectionTestRayTriangle, IntersectionTestRaySphere, IntersectionTestRayOrientedBox, IntersectionTestSphereOrientedBox, IntersectionTestOrientedBoxOrientedBox, with PackedOBBs<Packing> and GetOBBCorners) are batched over Packing * BatchSize primitives per call; Packing defaults to VectorF32<3, 1>::OptimalPacking (the ray-box test to the smaller of the 3- and 4-vector packings). A vertex argument is std::array<VectorF32<3, Packing>, BatchSize>: element b, lanes s*3 .. s*3+2, holds primitive b * Packing + s. The result is a VectorF32<1, Total> with lane i for primitive i: the ray parameter t, or std::numeric_limits<float>::max() for a miss (the two box-overlap tests return 0 for a hit). The triangle test culls back faces; the sphere test returns the near root, so a ray starting inside misses. tests/Intersection/main.cpp in the Crafter.Math repository has PackVec3, PackVec4 and PackScalars helpers worth copying.

Targets and SIMD

Backend Register MaxElement (floats) VectorF32<3,1>::OptimalPacking BatchSize of VectorF32<3, Opt>
x86 AVX-512 (x86-64-v4, sapphirerapids) 64 B 16 5 3 (15 lanes)
x86 AVX2 (x86-64-v3), and the scalar fallback 32 B 8 2 4 (8 lanes)
AArch64 NEON, WebAssembly SIMD128 16 B 4 1 4
RISC-V RVV 16, 32 or 64 B from the Zvl* in the march 4, 8 or 16
  • The backend and register width are fixed when the module is compiled, from predefined macros, so from the --march your build uses. crafter-build's default is native: fine for a developer build, wrong for a binary that ships. --march=x86-64-v3 is the lowest x86 tier the library is tested on (its SSE paths use FMA); the sapphirerapids tier runs only on CPUs with AVX512-FP16.
  • Tested as of 2026-10-31: all three x86 tiers on Linux and on Windows (x86_64-w64-mingw32 under wine); AArch64 armv8-a and armv8.2-a+fp16 under qemu, except Crafter.Math's own Matrix test, which does not compile for AArch64 with clang 23 (arm_neon.h wrapper functions defined twice across module units; it bites a unit that emits VectorF32<3, 1> arithmetic before a VectorF32<3, 1> Sin, Cos or SinCos, as that test does through MatrixRowMajor::Rotation(pitch, yaw, roll)). If an AArch64 build of yours fails with definition with same mangled name in arm_neon.h, it is that issue. RISC-V has never been built. WebAssembly builds (crafter-build adds -msimd128; flags you add to your own units must match) and has no tests.

Spellings

  • Quanternion*, MulitplyAdd, MulitplySub, CollumSize, Aligment are the real identifiers. Don't correct them in a consumer; a rename is an upstream change that updates every consumer at once.