# tenvex A header-only C++20 SIMD math library for 4D vector arithmetic. tenvex uses **expression templates** — a C++ template metaprogramming technique that allows chained vector operations to be evaluated lazily. A compound expression such as `norm3(a + b * 2.0f) * dot3(b, c) + c * 3.0f` is evaluated, at the point of assignment, as a single sequence of SSE4.1 intrinsics without intermediate temporaries. ## License BSD 3-Clause License. Copyright (c) 2026 Artem Zyktin. See [LICENSE.txt](LICENSE.txt). ## Status - **x86-64 / SSE4.1** - implemented and tested. - **ARM64 / NEON** - planned, not yet implemented. ## Features - Header-only - just include `tenvex.h`. - Expression-template engine: nodes are lazy and evaluated on assignment. - C++20 concepts (`expression`, `vec_expr`, `scalar_expr`) enforce type safety at compile time. - Storage policy: small trivially-copyable nodes are stored by value, larger ones by `const&`, which also avoids dangling references to temporary sub-expressions. - Operations: `+`, `-`, `*` (scalar), `/` (by scalar), `dot3`, `cross3`, `norm3`, `magnitude3`, `==`, `approx_eq`, and component accessors `x()`, `y()`, `z()`, `w()`. ## Requirements - A C++20 compiler (MSVC, GCC, or Clang). - An x64 CPU with **SSE4.1** support. - Premake for generating the test build (binaries are bundled for Windows, Linux, and macOS). ## Getting Started tenvex is header-only. Copy `src/tenvex/` into your project and include the single public header: ```cpp #include "tenvex.h" ``` ### Basic Usage ```cpp #include "tenvex.h" using namespace tnvx; vec4 a = { 1.0f, 2.0f, 3.0f, 0.0f }; vec4 b = { 4.0f, 5.0f, 6.0f, 0.0f }; vec4 c = { 1.0f, 0.0f, 1.0f, 0.0f }; // Arithmetic vec4 sum = a + b; vec4 diff = a - b; vec4 scaled = a * 2.0f; // vector * scalar vec4 scaled2 = 3.0f * b; // scalar * vector vec4 halved = a / 2.0f; // vector / scalar // Vector operations (3-component; the w / homogeneous lane is handled explicitly) float d = dot3(a, b); // 3D dot product (ignores w) vec4 cr = cross3(a, b); // 3D cross product, result w = 0 vec4 n = norm3(a); // normalize xyz, w preserved float mag = magnitude3(a); // 3D length (ignores w) // Compound expression - evaluated lazily, no temporaries, fused on assignment vec4 result = norm3(a + b * 2.0f) * dot3(b, c) + c * 3.0f; ``` `dot3` and `magnitude3` return scalar-expression nodes (`Dot3`, `Magn3`) that are implicitly convertible to `float` (the scalar is taken from lane 0), so `float d = dot3(a, b);` works, and they can also be used as operands inside a larger lazy expression. ### `vec4` `vec4` is a 16-byte, `alignas(16)` value type. It can be default-constructed, built from components (with `w` defaulting to `0`), or constructed from any vector expression (which triggers evaluation): ```cpp vec4 zero; // default vec4 v = { 1.0f, 2.0f, 3.0f }; // w defaults to 0 vec4 p = { 1.0f, 2.0f, 3.0f, 1.0f }; // explicit w vec4 r = a + b * 2.0f; // built from an expression (evaluates here) vf4 raw = v.eval(); // underlying __m128 ``` Individual components are read on demand from the stored register; `vec4` keeps no separate float copy: ```cpp float cx = v.x(); // lane 0 float cy = v.y(); // lane 1 float cz = v.z(); // lane 2 float cw = v.w(); // lane 3 ``` ### Comparison ```cpp vec4 a = { 1.0f, 2.0f, 3.0f, 4.0f }; vec4 b = { 1.0f, 2.0f, 3.0f, 4.0f }; bool exact = (a == b); // exact, all-lane equality bool approx1 = approx_eq(a, b); // default epsilon = 1e-6f bool approx2 = approx_eq(a, b, 1e-3f); // custom epsilon ``` ## Architecture ### Two layers - **Eager kernel layer** (`expressions/core.h`, namespace `tnvx::detail`): `TNVX_INLINE` functions over `vf4` (`add`, `sub`, `mul`, `div`, `dot3`, `magnitude3`, `norm3`, `cross3`, `scalar`, `get_lane`, `eq`, `approx_eq`). This is the only place intrinsics live, and the only place the SIMD backend is selected. - **Expression layer** (everything else in namespace `tnvx`): lazy nodes that compose and delegate down to the kernels. This layer operates only on `vf4` and contains no intrinsics. ### Expression templates Every operation returns a lazy node rather than evaluating immediately. Nodes inherit from `Expr` via CRTP (which provides `self()`) and expose a `vf4 eval() const` method. Evaluation happens only when the result is assigned to a `vec4`, or - for scalar nodes - when converted to `float`. ``` vec4 result = norm3(a + b * 2.0f) * dot3(b, c) + c * 3.0f; // Add< Mul< Norm3< Add> >, Scalar >, Mul > // evaluated as one SSE4.1 sequence at assignment ``` ### C++20 concepts | Concept | Meaning | | ---------------- | ----------------------------------------------------------------------- | | `expression` | Derives from `Expr` and has `eval() -> vf4` and `self() -> const T&` | | `vec_expr` | An `expression` marked as a vector expression (`is_vec_expr`) | | `scalar_expr` | An `expression` marked as a scalar expression (`is_scalar_expr`) | ### Storage policy Each node's operands are stored by value or by `const&` depending on size and triviality. This avoids dangling references to temporary sub-expressions: ```cpp // trivially copyable && trivially destructible && sizeof(T) <= 16 -> stored by value // otherwise -> stored as const T& template using tnvx_ref_or_value_t = std::conditional_t, T, const T&>; ``` ### Operation nodes | Node | Concept | Intrinsics | Notes | | ------------- | ------------- | ----------------------------------------------------------- | --------------------------------------------------------------------------- | | `Scalar` | `scalar_expr` | `_mm_set_ps1` | Broadcasts a `float` to all 4 lanes | | `Add` | `vec_expr` | `_mm_add_ps` | | | `Sub` | `vec_expr` | `_mm_sub_ps` | | | `Mul` | vec / scalar | `_mm_mul_ps` | Scalar multiply only (no component-wise `vec * vec`). Operands may be vector×scalar, scalar×vector, or scalar×scalar; a scalar operand is a `float` or a scalar expression | | `Div` | `vec_expr` | `_mm_div_ps` | Vector divided by a scalar (`float`) | | `Dot3` | `scalar_expr` | `_mm_dp_ps` (mask `0x7F`) | 3-component dot (ignores w), broadcast to all lanes; convertible to `float` | | `Cross3` | `vec_expr` | `_mm_shuffle_ps` + `_mm_mul_ps` + `_mm_sub_ps` | 3-component cross product; result `w = 0` | | `Norm3` | `vec_expr` | `_mm_dp_ps` + `_mm_sqrt_ps` + `_mm_div_ps` + `_mm_blend_ps` | Normalizes xyz by the 3D magnitude; preserves w | | `Magn3` | `scalar_expr` | `_mm_sqrt_ps(_mm_dp_ps(..., 0x7F))` | 3-component length (ignores w), broadcast; convertible to `float` | Comparison helpers (free functions in `tnvx`, backed by `tnvx::detail`): | Function | Intrinsics | Notes | | ---------------------------- | --------------------------------------- | ------------------------------ | | `operator==` | `_mm_cmpeq_ps` + `_mm_movemask_ps` | Exact, all-lane equality | | `approx_eq(a, b, eps=1e-6f)` | `_mm_andnot_ps` + `_mm_cmple_ps` + mask | Per-lane abs difference <= eps | ## Building The build uses [Premake5](https://premake.github.io/); binaries are bundled in `premake/bin/` for all platforms. The workspace is x64-only and uses C++20, with `debug` and `release` configurations and `tenvex_tests` as the start project. On GCC/Clang the `tenvex_tests` project is built with `-msse4.1`; projects that consume the headers must enable SSE4.1 in their own build. ### Windows (Visual Studio 2022) ```bat scripts\config-vs2022.bat ``` Then open the generated solution under `build/` and build the `tenvex_tests` project. ### Linux / macOS ```sh # Linux ./premake/bin/linux/premake5 gmake2 # macOS ./premake/bin/macos/premake5 gmake2 # then make config=debug # or: make config=release ``` Build output is written to `bin//x64//output/`. ## Running Tests The test suite uses Google Test (vendored in `thirdparty/gtest/`) and currently covers 27 cases: | Category | Tests | | -------------------- | ---------------------------------------------------------------------------------------------------------- | | Basic operations | `add`, `sub`, `mult_a`, `mult_b`, `div`, `dot3`, `cross`, `norm_a`, `norm_b`, `magnitude_a`, `magnitude_b` | | Compound expressions | `mixed_type_add`, `expression_a`, `expression_b`, `expression_c`, `expression_d`, `expression_from_temporaries` | | Math properties | `cross_anticommutative`, `dot_commutative`, `norm_magnitude`, `dot_perpendicular`, `cross_parallel`, `w_is_zero_with_nonzero_input_w`, `preserves_nontrivial_w`, `zero_vector_is_nan` | | Type system | `dot3_times_literal_collapses`, `magnitude3_times_literal_collapses` | Build and run `tenvex_tests` from the generated project or makefile.