Vecmat
A simple math and linear algebra library in C for 2D/3D graphics, machine learning, physics, and science.
Vecmat is a heartfelt ❤ love letter to the C programming language — with emphasis on the elegance, simplicity and readability of the language, even for scenarios where other languages might seem more suited. Performance is important but second to usability and elegance.
Philosophy
Elegance, simplicity, and readability matter more than squeezing every cycle.
Goals
- One common, easy-to-read API that is self-explanatory.
- Put usability first, then performance. Default functions take and return values by copy so call sites stay simple.
- Keep the public API stable. Speedups live behind the same names.
- Work well in graphics engines, simulations, and games, not only tiny demos.
- Stay portable C11, easy to pull in with CMake (
FetchContentorfind_package). - Grow SIMD and MMA without forcing apps to pass ISA flags.
Features
- Default interfaces use value types and obvious names (
vector3,matrix4,quaternion). - Angles are radians on unsuffixed APIs. Write
VM_or call theDEG(90) _degsuffix at the human/config edge;VM_documents an already-radian literal.RAD(M_ PI_ 2) - The real work lives in
_ptrfunctions (pointers in, pointers out). Those are what SIMD/MMA backends implement. - You can access components as
.x/.y/.zor asm11,m21, ... or as a flat.v[]array. - Performance is not ignored; it is layered under a stable, comfortable API.
- BSD 3-Clause License — great for individuals, organizations, and companies.
- Includes a unit testing and benchmarking framework
unitest.h - Exceptions in tests are handled using a custom handler
except.h; it is only 24 lines and you can reuse it.
Precision chosen at build time
- Default:
floatandint32_t. - Optional:
double(VECMAT_USE_F64), and int width 8 / 16 / 32.
Math types
- Float vectors: 2D, 3D, 4D (
vector2/vector3/vector4). - Integer vectors: same sizes (
vector2i/vector3i/vector4i). - Float and integer matrices: 2x2, 3x3, 4x4.
- Quaternions for rotation.
- Easing functions for animation-style interpolation.
- Clip-space presets for OpenGL (
RH_NO), Vulkan (RH_ZO) and Direct3D (LH_ZO). - Dense packed
vm_(gemm C = α op(A) op(B) + β C), batched GEMM, and heapvm_with LU / QR / SVD / Cholesky (solve, det, inverse, least squares).mat - Sparse CSR (
vm_) with CG / BiCGSTAB and Jacobi / SSOR / IC(0) preconditioners.spmat - Time integrators (semi-implicit Euler, velocity Verlet, RK2 / RK4), CFL helper, and
vm_.rigid_ step - Regular-grid / MAC operators and an assembled 5-/7-point Laplacian for Poisson projection.
Features to Avoid
- No SSE and no NEON on purpose. The library jumps to AVX / AVX2 / AVX-512 and ARM SVE / SVE2.
Two ways to call everything
- By-value helpers for everyday code.
_ptrkernels for hot paths and SIMD.
Extended Features
Computer Graphics
Clip-space helpers
Build projection and view matrices for different graphics APIs and depth conventions.
Perspective projections — camera frustum matrices (radians; _deg if FOV is in degrees). The unsuffixed mat4_ / mat4_ / mat4_ helpers also take radians. Use mat4_ (and friends) for degrees:
mat4_/perspective_ clip mat4_perspective_ clip_ deg mat4_/perspective_ rh_ no mat4_perspective_ rh_ no_ deg mat4_/perspective_ rh_ zo mat4_perspective_ rh_ zo_ deg mat4_/perspective_ lh_ zo mat4_perspective_ lh_ zo_ deg mat4_/perspective_ lh_ no mat4_perspective_ lh_ no_ deg
Orthographic projections — parallel projection matrices from frustum bounds:
Look-at view matrices — world-to-view transforms from eye, target, and up:
Look-from-direction view matrices — same basis as look-at, but the camera aims along a direction (FPS / fly camera, no target point):
mat4_/look_ from_ dir mat4_look_ from_ dir_ clip mat4_/look_ from_ dir_ rh mat4_look_ from_ dir_ lh quat_/look quat_— orientation whose local −Z (RH) or +Z (LH) aims along the directionlook_ clip quat_— shortest rotation taking one vector onto anotherfrom_ to
Infinite / reverse-Z projections — infinite far plane, optionally with reversed depth (near → 1, infinity → 0 on ZO):
mat4_stays historic OpenGLperspective_ infinite RH_NOmat4_— infinite + any clip convention (perspective_ infinite_ clip *_ZOis infinite + zero-to-one)mat4_— modern-engine preset: infinite + RH + ZO + reversed depthinfinite_ reverse_ z mat4_— same mapping for the other clip conventionsinfinite_ reverse_ z_ clip
Viewport, world ↔ window — NDC to a pixel box and back. Geometric vec3_ (onto a direction) is unchanged:
mat4_/viewport mat4_viewport_ depth vec3_/world_ to_ window vec3_window_ to_ world vec3_/world_ to_ window_ clip vec3_window_ to_ world_ clip
Affine inverse and normal matrices — skip the 4×4 adjugate when the transform is [A t; 0 1]:
mat4_— invert the 3×3 linear part and apply it to the translationinverse_ affine mat3_/normal mat4_— inverse-transpose of the 3×3 for transforming normalsnormal
Clip conventions — handedness + depth range selectors used by the *_clip helpers:
VM_— right-handed, clip z inCLIP_ RH_ NO [-1, 1](OpenGL-style)VM_— right-handed, clip z inCLIP_ RH_ ZO [0, 1](Vulkan-style)VM_— left-handed, clip z inCLIP_ LH_ ZO [0, 1](Direct3D-style)VM_— left-handed, clip z inCLIP_ LH_ NO [-1, 1]
Rotation helpers
Build 4×4 rotation matrices from axis angles in radians (mat4_ / mat4_ / mat4_ / mat4_). Use *_deg or VM_ when the angle is in degrees:
mat4_/rotation_ x mat4_rotation_ x_ deg mat4_/rotation_ y mat4_rotation_ y_ deg mat4_/rotation_ z mat4_rotation_ z_ deg mat4_/rotation mat4_rotation_ deg
Point Clouds, 3D Reconstruction, DNNs, LLMs
GEMM (General Matrix–Matrix Multiplication — BLAS Standard)
BLAS-style dense multiply:
C = alpha * op(A) * op(B) + beta * C
where op(X) is X or X transposed. Row-major and column-major layouts are supported.
vm_— Main routine for ordinary dense panels.gemm vm_— Simple triple-loop reference (tests / fallback).gemm_ ref vm_— Same asgemm_ ex vm_, plus optional bias (gemm C(i,j) += bias[j]) and/or ReLU.vm_/gemm_ batch vm_— Many same-shaped problems at once (pointer list, or fixed strides in one buffer).gemm_ strided_ batch
If every problem shares the same B (identical pointers, or strideB == 0), that matrix is packed once and reused — the usual “shared weights, many inputs” case.
Large batches can use a small worker pool (not OpenMP). Cap or disable it with vm_gemm_set_threads(n) or VECMAT_GEMM_THREADS (1 = serial, 0 = auto). Tiny jobs stay serial so thread setup does not dominate; workers are reused across calls.
vm_ unfolds an NCHW image into a GEMM-ready panel for convolution.
Internally, large multiplies use blocking/packing; with runtime dispatch the inner kernel may use AVX / AVX2 / AVX-512 / SVE / SVE2, otherwise scalar. fp16 / bf16 are not in this release.
Dense Linear Algebra
- Heap
vm_(M×N, column-major) for general dense work beyond the fixed 2×2 / 3×3 / 4×4 types.mat - LU —
vm_/lu_ factor vm_with partial pivoting;lu_ solve vm_andmat_ det vm_are thin wrappers on the same path (square systems).mat_ inverse - QR — Householder
vm_/qr_ factor vm_;qr_ unpack vm_for least-squaresqr_ solve min ||Ax − b||whenm ≥ n. - SVD — thin one-sided Jacobi
vm_(svd_ factor A = U diag(s) Vᵀ, singular values descending) for rank, conditioning, and reconstruction-style work. - Cholesky — in-place
vm_/chol_ factor vm_for dense SPD systems (tiny Poisson, covariance, SPD least squares).chol_ solve
Physics and Simulations
Vecmat is still a math library: it does not ship a fluid solver, an SPH engine, or a constraint island. It supplies the primitives those codes call every substep.
Precision. Graphics can stay float. Scientific time integration and Poisson solves should configure -DVECMAT_USE_F64=ON so vm_ is double. The same relative-tolerance style used by LU / QR (tol ~ n ε max|A|) is reused by CG / BiCGSTAB as ||r|| / max(||b||, ε).
Sparse systems
vm_— square CSR, built from triplets (spmat vm_sorts and sums duplicates)spmat_ from_ triplets vm_—spmv y = A xvm_— conjugate gradient for SPD systems (pressure Poisson, implicit diffusion, linear elasticity)cg vm_— nonsymmetric Krylov (advection–diffusion)bicgstab - Left preconditioners: Jacobi, SSOR (ω = 1), IC(0). IC(0) falls back to Jacobi if a pivot breaks down.
vm_reportsksp_ info iters,rel_res,ok
A 2-D Poisson problem on an N×N grid is N² unknowns with about five non-zeros per row. Dense LU is already the wrong tool at N = 64. CG + Jacobi is enough for a teaching projection step; IC(0)+CG is what a small research code can ship.
Time integration
vm_—euler_ semi v += a dt,x += v dt(particles, games)vm_— velocity Verlet with anverlet acc(x)callback (MD / SPH / Hamiltonians)vm_/rk2 vm_— explicit Runge–Kutta on a flat state vectorrk4 vm_cfl_dt(cfl, dx, speed)—dt = cfl * dx / (|u|+ε)vm_— symplectic Euler onrigid_ step (x, v, q, ω)with body-frame torque andI⁻¹(τ − ω×Iω)
quat_ is the orientation exponential map used inside vm_.
Rigid algebra
mat3_/chol mat3_— 3×3 SPD solve without LU pivotingspd_ solve vm_—inertia_ world I_w = R I_b Rᵀvm_omega_from_L— recoverωfromL = Iωvm_—rigid_ energy ½ m |v|² + ½ ω·(Iω)vm_— one-normal positional / velocity correctionbaumgarte_ correct mat3_— principal axes of an inertia tensor (setup / analysis)sym_ eigen
x, v, F are world-frame; ω and τ are body-frame.
Grid operators
vm_— uniform Cartesian metadata (grid3 nz == 1is 2-D)- MAC index helpers:
vm_/mac_ u vm_/mac_ v vm_and countsmac_ w vm_,mac_ div vm_,mac_ grad vm_mac_ curl_ z vm_— assemble the SPD operatorgrid_ laplacian −∇²(5-point / 7-point) with Dirichlet or Neumann rows
Documentation
API pages use the m.css Doxygen theme with a custom Dark Fire palette (doc/m-theme-dark-fire.css, orange/red embers, spark yellow, steel-blue info). doc/conf.py and doc/Doxyfile-mcss drive that pipeline. The stock Doxygen HTML theme is still available from the same Doxyfile.
Generate local docs with m.css
python3 -m venv .venv && source .venv/bin/activate python3 -m pip install jinja2 Pygments git clone --depth 1 https://github.com/mosra/m.css /tmp/m.css python3 /tmp/m.css/documentation/doxygen.py doc/conf.py
HTML lands in doc/html/. Doxygen writes XML to doc/xml/ first; both directories are git-ignored.
Stock Doxygen HTML
cd doc && doxygen Doxyfile
SIMD and MMA
Selection order: SVE2 -> SVE -> AVX-512F -> AVX2 -> AVX -> Scalar
| CMake flag | Default | Effect |
|---|---|---|
-DVECMAT_RUNTIME_DISPATCH=ON | ON for x86-64 and AArch64 | Build extra ISA TUs and bind public names at runtime |
-DVECMAT_ENABLE_AVX=ON | ON on x86-64 | Compile AVX kernels (-mavx / /arch:AVX) |
-DVECMAT_ENABLE_AVX2=ON | ON on x86-64 | Compile AVX2 kernels (-mavx2 / /arch:AVX2) |
-DVECMAT_ENABLE_AVX512=ON | ON on x86-64 | Compile AVX-512F kernels (-mavx512f / /arch:AVX512) |
-DVECMAT_ENABLE_SVE=ON | ON on AArch64 | Compile SVE kernels (-march=armv8-a+sve) |
-DVECMAT_ENABLE_SVE2=ON | ON on AArch64 | Compile SVE2 kernels (-march=armv8-a+sve2) |
vm_ is thread-safe (C11 atomics, double-checked locking) and idempotent. Concurrent first-use of dispatched kernels is safe.
How to check for features:
vm_cpu_init(); printf("compiled=%s runtime=%s selected=%s\n", vm_cpu_name(vm_cpu_compiled_features()), vm_cpu_name(vm_cpu_runtime_features()), vm_cpu_name(vm_cpu_selected_features()));
CPU Feature Support
- AVX supported
- AVX2 (FMA3) supported
- AVX-512F (AVX-512 FMA) supported
- AVX10 (FMA3) work in progress
- AVX10.1 (Xeon 6) coming in 2027
- AVX10.2 (Xeon 7) tbd
- SVE (ARMv8.2-A+) supported
- SVE2 (ARMv9) supported
MMA Support
- WMMA / MMA (NVIDIA/CUDA) work in progress
- MFMA / WMMA (AMD/ROCm) work in progress
- AMX (4th-7th generation Intel Xeon) coming in 2027
- SME / SME2 (ARMv9.2-A+) tbd
At this moment we have no plans to support NEON.
Relevant Resources
CMake Integration
Source using
if(NOT TARGET vecmat::vecmat) include(FetchContent) FetchContent_Declare(vecmat GIT_REPOSITORY https://github.com/alkavan/vecmat.git GIT_TAG v0.2.6 ) FetchContent_MakeAvailable(vecmat) endif() target_link_libraries(my_app PRIVATE vecmat::vecmat)
Installed Package
find_package(vecmat 0.2 CONFIG REQUIRED) target_link_libraries(my_app PRIVATE vecmat::vecmat)
System integration / Out-of-source build and installation
cmake -S . -B build -DCMAKE_BUILD_TYPE=Debug \ -DVECMAT_BUILD_TESTS=ON \ -DCMAKE_INSTALL_PREFIX="$HOME/.local" cmake --build build -j cmake --install build
Note: Use -DVECMAT_INSTALL=ON only when install rules were turned off or vecmat isn't top-level — and you still want cmake --install to install it.
Scalar precision flags
vm_ and vm_ are selected at compile time. Pass the matching CMake options when configuring Vecmat. The options become public compile definitions on vecmat::vecmat and vecmat::vecmat_static, so anything that links the library sees the same typedefs.
Defaults (no flags): vm_ is float, vm_ is int32_t.
| CMake flag | Header macro | Effect |
|---|---|---|
-DVECMAT_USE_F64=ON | VECMAT_USE_F64 | vm_ is double |
-DVECMAT_USE_INT8=ON | VECMAT_USE_INT8 | vm_ is int8_t |
-DVECMAT_USE_INT16=ON | VECMAT_USE_INT16 | vm_ is int16_t |
-DVECMAT_USE_INT32=ON | VECMAT_USE_INT32 | vm_ is int32_t |
The integer flags are mutually exclusive. CMake will error if more than one is ON. VECMAT_USE_F64 can be combined with any one integer flag.
Configure from the command line:
cmake -S . -B build \ -DVECMAT_USE_F64=ON \ -DVECMAT_USE_INT16=ON \ -DVECMAT_BUILD_TESTS=ON
With FetchContent, set the cache variables before FetchContent_MakeAvailable:
set(VECMAT_USE_F64 ON CACHE BOOL "" FORCE) set(VECMAT_USE_INT16 ON CACHE BOOL "" FORCE) FetchContent_MakeAvailable(vecmat)
Without CMake, define the same macros yourself (compiler flag or before the library include):
cc -DVECMAT_USE_F64 -DVECMAT_USE_INT16 ...
#define VECMAT_USE_F64 #define VECMAT_USE_INT16 #include <vecmat.h>
The library and every translation unit that includes vecmat.h must use the same set of macros, or the types will not match at link time.
Contributing
We don't have any complicated rules for contributing (for now), we only expect people to comply with the project Philosophy and Goals.
Artificial Intelligence Guidelines and Transparency
- AI use: Use of AI is neither prohibited nor encouraged. You may use AI only if you follow all the guidelines in this section.
- Disclosure: If you add AI-generated material to a contribution or derivative work, say so clearly — for example in the pull request, commit message, or nearby comments. Note which parts were AI-generated or heavily AI-assisted. Everyday autocomplete or small wording help does not need a notice.
- Responsibility: When you contribute or share a derivative, you take responsibility that the work has enough original human authorship, and that any AI-generated parts don't violate someone else's terms or the project [LICENSE](LICENSE).
- AI training: If you train an AI system on this code, it is recommended to give it the whole project, including in-code comments and any generated documentation that exists.
Usage and Examples
Vectors and matrices are plain C structs. Components are available as named fields (.x / .y / .z / .w, or m11, m21, …) and as a flat .v[] array. Prefer the value constructors for everyday code.
Individual element access
vector3 p; p.x = 1.0f; // same as p.v[0] p.v[1] = 2.0f; // same as p.y printf("%f\n", p.z);
matrix3 mat; mat.v[0] = 1.0f; // same as mat.m11 (column-major) printf("%f\n", mat.m21); // same as mat.v[1]
Initializing a vector
vector3 p = vec3(1.0f, 2.0f, 3.0f); vector2 q = vec2(4.0f, 5.0f); vector3i grid = vec3i(8, 16, 24); vector3 origin = vec3_zero(); vector3 ones = vec3_one(); vector3 fill = vec3_splat(0.5f); vector3 named = { .x = 1.0f, .y = 0.0f, .z = 0.0f }; vector4 homog = { .v = {1.0f, 2.0f, 3.0f, 1.0f} }; vec3_assign_xyz(&p, 0.0f, 1.0f, 0.0f); vector3 lifted = vec3_from_vec2(q, 0.0f);
The same pattern exists for vector2 / vector4 and the integer types (vecN_zero, vecN_one, vecN_splat, plus vec2i / vec3i).
Initializing a matrix
matrix3 ident = { .m11 = 1.0f, .m21 = 0.0f, .m31 = 0.0f, .m12 = 0.0f, .m22 = 1.0f, .m32 = 0.0f, .m13 = 0.0f, .m23 = 0.0f, .m33 = 1.0f }; matrix3 also = { .v = {1,0,0, 0,1,0, 0,0,1} };
Accessing matrix elements
Accessing elements by name
float determinant(const matrix3 *mat) { float det = mat->m11 * (mat->m22 * mat->m33 - mat->m23 * mat->m32) // First term - mat->m12 * (mat->m21 * mat->m33 - mat->m23 * mat->m31) // Second term (negative) + mat->m13 * (mat->m21 * mat->m32 - mat->m22 * mat->m31); // Third term return det; }
Accessing elements by index
matrix3 mat; for (int i = 0; i < 9; i++) { mat.v[i] *= 2.0f; // Scale all elements by 2 }
Implementing Common Vector And Matrix Operations
Vector Operations Examples
A function for general linear transformation to the vector:
void transform(vector3 *out, const matrix3 *mat, const vector3 *vec) { out->x = mat->m11 * vec->x + mat->m12 * vec->y + mat->m13 * vec->z; out->y = mat->m21 * vec->x + mat->m22 * vec->y + mat->m23 * vec->z; out->z = mat->m31 * vec->x + mat->m32 * vec->y + mat->m33 * vec->z; }
A function to translate a vector by adding a translation offset:
void translate(vector3 *out, const vector3 *vec, const vector3 *translation) { out->x = vec->x + translation->x; out->y = vec->y + translation->y; out->z = vec->z + translation->z; }
Matrix Operations Examples
You can write a function to multiply two matrix3 instances.
Using the array access makes it easier to implement with nested loops:
void multiply(matrix3 *result, const matrix3 *a, const matrix3 *b) { for (int c = 0; c < 3; c++) { /* columns of result / of B */ for (int r = 0; r < 3; r++) { /* rows of result / of A */ float sum = 0.0f; for (int k = 0; k < 3; k++) { sum += a->v[k * 3 + r] * b->v[c * 3 + k]; /* column-major */ } result->v[c * 3 + r] = sum; } } }
This creates a matrix4 that can apply rotation/scaling (from matrix3) followed by translation:
void affine_matrix(matrix4 *out, const matrix3 *linear, const vector3 *translation) { // Copy the 3x3 linear part (columns 1-3) out->m11 = linear->m11; out->m21 = linear->m21; out->m31 = linear->m31; out->m41 = 0.0f; out->m12 = linear->m12; out->m22 = linear->m22; out->m32 = linear->m32; out->m42 = 0.0f; out->m13 = linear->m13; out->m23 = linear->m23; out->m33 = linear->m33; out->m43 = 0.0f; // Set translation in the fourth column out->m14 = translation->x; out->m24 = translation->y; out->m34 = translation->z; out->m44 = 1.0f; }