fixarray
fixarray — with its stamp (commit, host, harness) and the commands that reproduce it — is on the fixarray benchmarks page.Fixed-extent vectors and matrices — the same mathematics as an ndarray::NDArray, with the shape moved into the type, for the small-and-hot regime (a renderer's transforms, a physics solver's contact frames, a filter's small state). Header-only; import fixarray (it auto-links ndarray for its element concepts). The heavy shape-generic numerics on the dynamic NDArray (LU/QR/SVD/eigen) live in the sibling linalg module.
The type
An NDArray carries its shape at runtime and its elements on the heap. That is what makes it general — and, for a 3-D direction or a 4×4 transform, it is the whole cost: an allocation and an indirection to move sixteen floats.
fixarray::Fixed<T, Dims...> is the same idea with the shape in the type. The elements live inline, so the value is trivially copyable and nothing allocates; the loops have compile-time trip counts and auto-vectorize. These are the types for high-performance work — transforms, contact frames, small filter state — where the same matrix is built and consumed millions of times a second. Everything else matches NDArray: the element types, the (row, column) index, numpy's vocabulary, and the answers.
using namespace cheatah::fixarray;
vec3f up{0.0F, 1.0F, 0.0F}; // 12 bytes, no allocation
mat4f mvp = projection * view; // 64 bytes — exactly a push constant
vec3f n = normalize(cross(up, dir));Aliases: vec2f…vec4f, vec2d…vec4d, mat2f…mat4f, mat2d…mat4d; Vec<T,N> and Mat<T,R,C> for anything else. Rank 1 (vector) and rank 2 (matrix).
From cheatah
import fixarray and use it from a .purr program — construct with the call form and operate with the free functions:
import io
import fixarray
fn length2(v: fixarray.Fixed<f32, 3>) -> f32 { # module-qualified param/return
return fixarray.dot(v, v)
}
fn main() {
let a = fixarray.vec3f(1.0, 2.0, 3.0) # construct (deduced type)
let b: fixarray.vec3f = fixarray.vec3f(4.0, 5.0, 6.0) # or annotate the type
io.print(fixarray.dot(a, b)) # 32
let bytes: fixarray.Vec<u8, 3> = fixarray.Vec<u8, 3>(1, 2, 3) # narrow element (1 byte each)
io.print(length2(a)) # 14
}
main()Aliases (vec3f, mat4f, …) and the explicit fixarray.Fixed<T, Dims…> / fixarray.Vec<T, N> spellings all work as let/field/parameter/return annotations. A parameter of a fixarray type binds by reference, so pass a named vector (an lvalue) to your own functions. import fixarray alone pulls in ndarray transitively.
A matrix is stored column-major, unlike NDArray. The indexing you write does not change, but data() hands back columns. That is deliberate and measured: m * v becomes a sum of scaled columns — contiguous and vertical — instead of four horizontal dot products behind a shuffle network, and the buffer is already in the order GPU APIs expect, so uploading a transform is a copy rather than a transpose.
Speed
Benchmarked against GLM across the complete overlap of the two APIs — 160 pairs: every vector and matrix operation (arithmetic, dot, cross, length, normalize, distance, reflect, min/max/clamp, mix, step/smoothstep, matmul, transpose, determinant, inverse, matrixCompMult, outerProduct, inverseTranspose), at sizes 2/3/4 — the GLSL common functions and the extra matrix builtins at 3 and 4 — in both float and double (fixed_glm_bench.cpp). Both sides compile in the same translation unit with the same flags, so neither gets an instruction set the other lacks — and the benchmark verifies the two produce identical results before it times either, so a fast wrong answer can never masquerade as a win.
Fixed is faster than or at parity with GLM across the whole overlap, and slower on none. The generated tables that carry the exact tally — a representative sample, the complete comparison, and the reading of the parity floor that decides each verdict — are on the fixarray benchmarks page, and the bench_gate.sh hard gate keeps the claim true: it fails the build if any pair regresses past GLM (tolerant by ratio and an absolute floor, with a confirmation re-run so sub-nanosecond noise never flakes it).
Choices that earn it, each a comment in fixarray.hpp: a matrix is stored column-major, so m * v is a sum of contiguous columns rather than horizontal dot products behind a shuffle network; dot sums pairwise and, at width ≥ 4, packs the products into one SIMD multiply (also lowering the rounding error to O(log n)); normalize takes one reciprocal then multiplies; min/max/abs/clamp are branchless always-writes that lower to minps/maxps; and every elementwise op builds its result in one pass through from_indices (no default-zero then overwrite). No intrinsics — the code is shaped so the compiler vectorizes it, no library to link at all.
Classes
Fixed— A fixed-extent, inline-stored array — an cheatah::ndarray::NDArray whose shape lives in the type.
Functions
Inner product of two vectors — Σ aᵢbᵢ, the same quantity dot(const NDArray&, const NDArray&) computes, without the allocation.
Summed pairwise, so it is both faster and more accurate than a left-to-right accumulation.
T | the element type. |
N | the length. |
a, b | the vectors. |
the inner product.
O(N).
none.
Fixarray.DotAndCrossCross product of two 3-vectors — the vector perpendicular to both, right-handed.
T | the element type. |
a, b | the vectors. |
a × b.
O(1).
none.
Fixarray.DotAndCrossThe squared Euclidean length of a vector — dot(v, v).
Prefer it to norm when only comparing lengths: it avoids the square root.
T | the element type. |
N | the length. |
v | the vector. |
Σ vᵢ².
O(N).
none.
for a complex element this is the bilinear Σ vᵢ² (matching dot), not the Hermitian Σ |vᵢ|² — it is the squared Euclidean LENGTH only for real elements.
Fixarray.NormAndNormalizeThe Euclidean length of a vector.
T | the element type; floating-point, since the result is a root. |
N | the length. |
v | the vector. |
sqrt(Σ vᵢ²).
O(N).
none.
Fixarray.NormAndNormalizeThe unit vector pointing the same way as v.
T | the element type; floating-point. |
N | the length. |
v | the vector; must not be the zero vector. |
v / norm(v).
std::domain_error | when |
O(N).
none.
Fixarray.NormAndNormalizeThe transpose of a matrix — rows become columns.
T | the element type. |
R | the rows. |
C | the columns. |
m | the matrix. |
the C×R transpose.
O(R·C).
none.
Fixarray.TransposeAndTraceThe trace of a square matrix — the sum of its diagonal.
T | the element type. |
N | the dimension. |
m | the matrix. |
Σ mᵢᵢ.
O(N).
none.
Fixarray.TransposeAndTraceMatrix product — the same A·B matmul(const NDArray&, const NDArray&) computes, with the shapes checked by the compiler rather than at runtime.
T | the element type. |
R | the rows of |
K | the shared dimension. |
C | the columns of |
a, b | the matrices. |
the R×C product.
O(R·K·C).
none.
Fixarray.Matmuloperator* · 2 overloads
Matrix product, spelled a * b.
T | the element type. |
R | the rows of |
K | the shared dimension. |
C | the columns of |
a, b | the matrices. |
matmul(a, b).
O(R·K·C).
none.
Fixarray.Matmuldeterminant · 3 overloads
The determinant of a 2×2 matrix, in closed form.
T | the element type. |
m | the matrix. |
det(m).
O(1).
none.
Fixarray.DeterminantAndInverseinverse · 3 overloads
The inverse of a 2×2 matrix, in closed form.
T | the element type; floating-point, since the inverse divides. |
m | the matrix; must be non-singular. |
m⁻¹.
std::domain_error | when |
O(1).
none.
Fixarray.DeterminantAndInverseThe Euclidean distance between two points — norm(a - b).
T | the element type; floating-point, since the result is a root. |
N | the dimension. |
a, b | the points. |
‖a − b‖.
O(N).
none.
Fixarray.GeometryThe squared distance between two points — squared_norm(a - b).
Prefer it to distance when only comparing distances: it skips the square root.
T | the element type. |
N | the dimension. |
a, b | the points. |
‖a − b‖².
O(N).
none.
Fixarray.GeometryReflect an incident vector about a surface normal — I − 2 (N·I) N, the GLSL reflect.
normal is assumed unit length, as GLSL requires.
T | the element type. |
N | the dimension. |
incident | the incoming vector. |
normal | the unit surface normal. |
the reflected vector.
O(N).
none.
Fixarray.GeometryRefract an incident vector through a surface — the GLSL refract.
incident and normal are assumed unit length. On total internal reflection (a negative radicand) the result is the zero vector, exactly as GLSL specifies.
T | the element type; floating-point. |
N | the dimension. |
incident | the unit incoming vector. |
normal | the unit surface normal. |
eta | the ratio of indices of refraction (source over destination). |
the refracted vector, or the zero vector under total internal reflection.
O(N).
none.
Fixarray.GeometryVec< T, N > faceforward(const Vec< T, N > &n, const Vec< T, N > &incident, const Vec< T, N > &reference)
source#
Orient a normal to face a viewer — the GLSL faceforward: return n when reference points against the incident direction (dot(reference, incident) < 0), -n otherwise.
Used to keep a surface normal on the camera's side.
T | the element type. |
N | the dimension. |
n | the normal to orient. |
incident | the incident vector. |
reference | the reference normal the result is oriented against. |
n or -n.
O(N).
none.
Fixarray.GeometryThe absolute value of every element.
T | the element type; a real number, so |
Dims | the extents. |
x | the array. |
|x| elementwise.
O(size).
none.
Fixarray.CommonUnaryThe sign of every element: -1, 0, or +1.
T | the element type; a real number. |
Dims | the extents. |
x | the array. |
the elementwise sign.
O(size).
none.
Fixarray.CommonUnarymin · 2 overloads
The smaller of each corresponding pair of elements.
T | the element type; a real number. |
Dims | the extents. |
a, b | the arrays. |
min(aᵢ, bᵢ) elementwise.
O(size).
none.
Fixarray.MinMaxClampmax · 2 overloads
The larger of each corresponding pair of elements.
T | the element type; a real number. |
Dims | the extents. |
a, b | the arrays. |
max(aᵢ, bᵢ) elementwise.
O(size).
none.
Fixarray.MinMaxClampclamp · 2 overloads
Constrain every element to [lo, hi] — the GLSL clamp with scalar bounds, the common case of pinning a colour to [0, 1].
T | the element type; a real number. |
Dims | the extents. |
x | the array. |
lo | the lower bound. |
hi | the upper bound. |
O(size).
none.
Fixarray.MinMaxClampmix · 2 overloads
Linear interpolation — the GLSL mix: a (1 − t) + b t, with a scalar blend t (0 gives a, 1 gives b).
Composed from the operators, so it inherits their vectorization.
T | the element type; floating-point. |
Dims | the extents. |
a, b | the endpoints. |
t | the blend factor. |
the interpolated array.
O(size).
none.
Fixarray.MixStepA step at edge — the GLSL step: 0 where an element is below edge, 1 at or above.
T | the element type; a real number. |
Dims | the extents. |
edge | the threshold. |
x | the array. |
xᵢ < edge ? 0 : 1 elementwise.
O(size).
none.
Fixarray.MixStepA smooth Hermite transition from 0 to 1 across [edge0, edge1] — the GLSL smoothstep, with everything below edge0 giving 0 and everything above edge1 giving 1.
T | the element type; floating-point. |
Dims | the extents. |
edge0 | the lower edge. |
edge1 | the upper edge. |
x | the array. |
the smoothstepped array.
O(size).
none.
Fixarray.MixStepThe elementwise (Hadamard) product — the GLSL matrixCompMult.
Named apart from operator* precisely because * is the matrix product; this multiplies corresponding entries.
T | the element type. |
R | the rows. |
C | the columns. |
a, b | the matrices. |
the elementwise product.
O(R·C).
none.
Fixarray.MatrixExtrasThe outer product of a column and a row — the GLSL outerProduct: an R×C matrix whose (i, j) entry is c[i] · r[j].
A rank-one update, the workhorse of a covariance accumulation.
T | the element type. |
R | the length of |
C | the length of |
c | the column vector. |
r | the row vector. |
the R×C outer product.
O(R·C).
none.
Fixarray.MatrixExtrasThe inverse transpose of a matrix — transpose(inverse(m)), the GLSL inverseTranspose.
This is the matrix that carries normals correctly under a non-uniform transform, so lighting stays right.
T | the element type; floating-point. |
N | the dimension. |
m | the matrix; must be non-singular. |
(m⁻¹)ᵀ.
std::domain_error | when |
O(1) at the fixed sizes.
none.
Fixarray.MatrixExtrasExtract one row of a matrix as a vector.
T | the element type. |
R | the rows. |
C | the columns. |
Ix | the index type: an integer, or a scoped |
m | the matrix. |
i | the row, |
the C-vector of that row.
O(C).
none.
Fixarray.NamedRowsAndColumnsExtract one column of a matrix as a vector — a basis vector of the transform.
This is the axis a scoped enum was made to name: column(view, Axis::Right).
T | the element type. |
R | the rows. |
C | the columns. |
Ix | the index type: an integer, or a scoped |
m | the matrix. |
j | the column, |
the R-vector of that column.
O(R).
none.
Fixarray.NamedRowsAndColumnsRender v the way an NDArray renders — numpy-style nested brackets, each element through the SHARED scalar formatter (so i8/u8 elements print as NUMBERS, f32/f64 plainly, and a complex as a+bj).
A vector is [a, b, c]; a matrix is [[…], […]] in reading (row, column) order — regardless of the column-major storage. This is what io.print/io.str/str() show.
v | the value to format. |
the bracketed text.
O(Fixed::size).
allocates the result string and a formatting stream per element.
Stream v (the nested-bracket to_string form), so a Fixed is directly Streamable — a cheatah io.print(v) / io.str(v) finds this by ADL, exactly as it does for an NDArray or a primitive.
os | the stream. |
v | the value. |
os.
O(Fixed::size).
allocates the intermediate string and a formatting stream per element.
Constants & variables
The product of a pack of extents — a fixed array's element count, 1 for an empty pack.
Dims | the extents. |
Types
A fixed-extent vector of N elements.
T | the element type. |
N | the length. |
A fixed-extent matrix of R rows and C columns — column-major storage, mathematical (row, col) indexing (see the file doc).
T | the element type. |
R | the rows. |
C | the columns. |
A 2-D vector of float.
A 3-D vector of float — a direction, a position, a colour.
A 4-D vector of float — a homogeneous point, an RGBA colour.
A 2-D vector of double.
A 3-D vector of double.
A 4-D vector of double.
A 2×2 matrix of float.
A 3×3 matrix of float — a rotation, or a normal matrix.
A 4×4 matrix of float — a transform; exactly the 64 bytes of a push constant.
A 2×2 matrix of double.
A 3×3 matrix of double.
A 4×4 matrix of double.
