AxlMath — Floating-point Math
AxlMath — Floating-point Math Primitives
Libm-free implementations of floor / ceil / fabs / sqrt /
fmod / sin / cos.
Header: <axl/axl-math.h>
Overview
axl-sdk’s freestanding UEFI build links with -nostdlib and cannot
rely on libm. GCC’s __builtin_floor / __builtin_sin et al. usually
lower to libm calls on baseline targets (the SSE4.1 ROUND instruction
isn’t in the -march=x86-64 baseline, and there’s no hardware sin/cos
on either x64 or AArch64).
AxlMath exposes a small, libm-free implementation of the math primitives downstream consumers (axl-truetype, axl-gfx-path, future AGT widget animation, downstream image codecs needing power-of-two scaling, etc.) actually need. Accuracy is sufficient for UI coordinates and animation easing — not for numerical analysis.
#include <axl.h>
double diag = axl_sqrt(2.0); // 1.414...
int pen_px = axl_floori(p.x); // round-down to int pixel
double t = axl_sin(elapsed * 2.0); // animation easing
All values are double so callers can mix integer and floating-point
inputs without precision surprises; consumers that pin float storage
cast at the boundary.
API Shape
Function |
Notes |
|---|---|
|
|
|
|
|
absolute value |
|
Newton’s method internally; negative input clamps to 0 |
|
|
|
6-term Taylor with constant-time range reduction |
Constants AXL_MATH_PI, AXL_MATH_HALF_PI, AXL_MATH_TWO_PI are exposed as
macros for trig callers that want pinned-precision values.
Accuracy
Function |
Error bound |
Notes |
|---|---|---|
|
exact |
Integer-cast + sign correction |
|
exact |
|
|
~1e-12 |
10 Newton iterations |
|
~1e-12 |
Truncated quotient |
|
~1e-7 |
6-term Taylor through |
Hardware fast paths
Each hardware-pathable primitive (sqrt / floor / ceil / fabs)
has both a __builtin_* hardware path that lowers to a single CPU
instruction AND a libm-free manual fallback that’s correct on any
target. Selection is compile-time per -march via
AXL_MATH_HAS_HW_* flags inside src/math/axl-math.c — no runtime
dispatch, no per-call branch. --gc-sections strips the unused
branch from the link.
|
sqrt |
floor / ceil |
fabs |
sin/cos Horner |
|---|---|---|---|---|
|
|
manual fallback |
|
mul + add |
|
|
|
|
mul + add |
|
|
|
|
|
|
|
|
|
|
sin / cos evaluate a 6-term Horner polynomial; on targets with
hardware FMA the inner loop uses AXL_MATH_FMA for one
fused-multiply-add per term (~30% faster + one extra bit of
precision per term). Without FMA, the same expression compiles to
plain mul + add.
Build with make CFLAGS_EXTRA='-march=x86-64-v3' (or higher) to
opt into the additional fast paths. axl-sdk’s -fno-math-errno
and -fno-trapping-math flags are what let GCC actually inline
__builtin_sqrt etc. instead of emitting libm calls — these are
on by default in our Makefile (the freestanding UEFI build has no
errno + we don’t catch FP exceptions).
QEMU CPU compatibility
For the default -march=x86-64 baseline (the only path tested in
CI) no QEMU configuration changes are needed. The two
hardware paths active at baseline — SQRTSD and ANDPD-as-fabs
— are in the SSE2 ISA which QEMU’s default qemu64 model
exposes, and AArch64’s -cpu cortex-a57 (set by
scripts/axl-common.sh’s build_qemu_base_cmd) covers every
ARMv8-A baseline instruction we emit.
If a consumer bumps to -march=x86-64-v2 or higher, the binary
will contain ROUNDSD (SSE4.1) or VFMADD132SD (FMA3) which
are NOT in qemu64. Two options to test such a build:
Run with
-cpu hostand KVM available.scripts/run-qemu.shalready does this automatically when/dev/kvmis readable + writable. Modern hosts (post-2008 Intel, post-Bulldozer AMD) have all the features axl-sdk can request.Pass an explicit CPU model via
--qemu-arg. Example for a Nehalem+ build:./scripts/run-qemu.sh --qemu-arg -cpu --qemu-arg Nehalem hello.efi ./scripts/run-qemu.sh --qemu-arg -cpu --qemu-arg Haswell hello.efi
QEMU’s CPU model names match the
-marchlevels closely (Nehalem≈x86-64-v2,Haswell≈x86-64-v3).
Without one of these, a bumped--march binary will hit a
#UD (illegal opcode) trap on the first hardware-only
instruction. The symptom is a QEMU EXCEPTION log line and
firmware death, NOT a graceful error — the CPU literally doesn’t
know the instruction.
This isn’t documented elsewhere — flag for AGT / downstream
consumers if they ever bump -march for perf.
UI consumers (widget rendering, layout, animation easing) get more than enough precision. Numerical-analysis consumers should look elsewhere.
Range Reduction (sin/cos)
axl_sin reduces the input to [-π/2, π/2] before evaluating the
Taylor series. The reduction is constant time —
x -= 2π * floor((x + π) / 2π) lands x in one period regardless
of magnitude. The previous file-static duplicates (in axl-truetype
and axl-gfx-path) used while (x > π) x -= 2π; which hung
multi-second on inputs like 1e9. The shared module removes that
footgun.
Behavior on NaN or ±∞ is implementation-defined (the floor cast
hits int64 overflow). UI inputs are bounded well below that.
Linking and Selective Pull-in
Each function is its own translation unit’s .text.axl_* section,
so --gc-sections drops everything you don’t reference. A tool that
only calls axl_floor doesn’t pay for axl_sin.
Substrate Discipline
AxlMath is a foundational primitive. Higher-level modules (axl-gfx,
axl-truetype, axl-gfx-path) dogfood it; toolkits on top (AGT,
downstream codecs) should too. The rule per
CLAUDE.md: no more file-static floor clones.
API Reference
Floating-point math primitives — libm-free.
axl-sdk’s freestanding UEFI build links with -nostdlib and cannot rely on libm. GCC’s __builtin_floor / __builtin_sin et al. usually lower to libm calls on baseline targets (the SSE4.1 ROUND instruction is not in the -march=x86-64 baseline, and there’s no hardware sin/cos on either x64 or AArch64).
This module exposes a small, libm-free implementation of the math primitives downstream consumers (axl-truetype, axl-gfx- path, future AGT widget animations, downstream image-codec integrations) actually need. Accuracy is sufficient for UI coordinates and animation easing — not for numerical analysis.
Module name: AxlMath. All values are double so callers can mix integer and floating-point inputs without precision surprises; consumers that pin float storage cast at the boundary.
double a = axl_sqrt(2.0); // 1.414...
int pen_pixel = axl_floori(p.x); // round-down to int pixel
double phase = axl_sin(t * 2.0); // animation easing
Defines
-
AXL_MATH_PI
-
AXL_MATH_TWO_PI
-
AXL_MATH_HALF_PI
-
AXL_MATH_E
Euler’s number — base of the natural logarithm.
-
AXL_MATH_SQRT_2
Square root of 2 — diagonal of the unit square.
-
AXL_MATH_LOG_2
Natural log of 2 — handy for base-2 ↔ base-e conversions.
-
AXL_MATH_GOLDEN
Golden ratio φ = (1 + √5) / 2 — UI proportions, Fibonacci spirals.
-
AXL_MATH_DEG_TO_RAD
Degrees → radians conversion factor (π / 180).
-
AXL_MATH_RAD_TO_DEG
Radians → degrees conversion factor (180 / π).
Enums
-
enum AxlTransformClass
Classification of a transform, derived from its contents (never stored / consumer-set). Ordered by generality: each kind is a special case of the ones below it, and
axl_transform_classifyreturns the most specific kind that fits. Renderers and the mapping routines key fast paths off this (skip the perspective divide when notPROJECTIVE, use an axis-aligned blit whenSCALE, etc.).Values:
-
enumerator AXL_TRANSFORM_IDENTITY
exactly the identity
-
enumerator AXL_TRANSFORM_TRANSLATE
identity linear part + translation
-
enumerator AXL_TRANSFORM_SCALE
axis-aligned scale (diagonal linear) + translation
-
enumerator AXL_TRANSFORM_AFFINE
general affine (rotation / shear); bottom row [0 0 1]
-
enumerator AXL_TRANSFORM_PROJECTIVE
non-trivial bottom row (perspective)
-
enumerator AXL_TRANSFORM_IDENTITY
Functions
-
double axl_floor(double x)
Floor — largest integer value not greater than x.
axl_floor(3.7) == 3.0,axl_floor(-3.2) == -4.0. Always returns the mathematical floor; no libm linkage.
-
double axl_ceil(double x)
Ceiling — smallest integer value not less than x.
axl_ceil(3.2) == 4.0,axl_ceil(-3.7) == -3.0.
-
int axl_floori(double x)
Floor returned as
intdirectly — convenience for pixel-snap and bbox arithmetic where the next consumer is integer-typed.
-
int axl_ceili(double x)
Ceiling returned as
intdirectly.
-
double axl_fabs(double x)
Absolute value.
axl_fabs(-3.5) == 3.5,axl_fabs(0.0) == 0.0.
-
double axl_sqrt(double x)
Square root — Newton’s method internally.
axl_sqrt(4.0) == 2.0,axl_sqrt(0.0) == 0.0. Accurate to roughly the precision ofdoublefor non-tiny non-huge inputs (UI-scale magnitudes).Warning
Negative input clamps to 0 (NOT NaN, unlike libm). Convention chosen because UI consumers usually mean “absolute distance” — catch sign bugs by checking input rather than result == 0.
-
double axl_fmod(double x, double y)
Floating-point modulo —
x - trunc(x/y) * y.axl_fmod(7.0, 3.0) == 1.0. Sufficient precision for curve flattening and angle wrap; not IEEE-correct for huge magnitudes.
-
double axl_wrap(double x, double n)
Wrap x into the half-open range
[0, n)regardless of sign.Equivalent to
x - n * floor(x / n)— same asaxl_fmodbut usingfloor(toward -∞) instead of truncation (toward zero), so negative inputs wrap cleanly into the positive range.Useful for circular indices (sprite frame % N over floats) and angle normalization:
axl_wrap(angle, AXL_MATH_TWO_PI)brings any angle into[0, 2π).axl_wrap(3.5, 10.0) == 3.5(positive in-range, identity)axl_wrap(12.5, 10.0) == 2.5(positive over-range)axl_wrap(-1.5, 10.0) == 8.5(negative wraps to positive)axl_wrap(10.0, 10.0) == 0.0(right boundary excluded)For
n <= 0the result is implementation-defined; the current implementation returns 0 (safe-default, mirroringaxl_fmod’s zero-divisor convention).- Parameters:
x – value to wrap
n – period (must be
> 0)
- Returns:
Value in
[0, n)forn > 0, else0.
-
double axl_sin(double x)
Sine.
Range-reduces x to
[-π/2, π/2]and evaluates a 6-term Taylor series (throughx¹¹/11!). Accurate to ~1e-7 over the full input range.
-
double axl_cos(double x)
Cosine. Derived from
axl_sinvia the standard identitycos(x) = sin(x + π/2).
-
double axl_ln(double x)
Natural logarithm — base e.
Named
axl_ln(notaxl_log) to avoid clashing with the logging-system function of the same name in<axl/axl-log.h>. The math notationlnis unambiguous for natural log.Range-reduces via the IEEE 754 exponent:
x = m * 2^ewithm ∈ [1, 2), soln(x) = ln(m) + e * AXL_MATH_LOG_2. Mantissa log evaluated via the substitutions = (m-1)/(m+1)and the odd-only seriesln(m) = 2 * (s + s³/3 + s⁵/5 + s⁷/7 + ...). Accurate to ~1e-10 over the normal-double input range.axl_ln(AXL_MATH_E) == 1.0,axl_ln(1.0) == 0.0,axl_ln(2.0) == AXL_MATH_LOG_2.Warning
Non-positive input returns 0 (NOT -∞ or NaN as libm would). Same convention as
axl_sqrt— catch sign bugs by checking the input rather than the result.- Returns:
ln(x)forx > 0, else0.
-
double axl_exp(double x)
Natural exponential —
e^x.Range-reduces via
x = k * log(2) + rwithkinteger andr ∈ [-log(2)/2, log(2)/2], evaluates a 10-term Taylor series onr, then reconstructs2^kby writing the IEEE 754 exponent field directly. Accurate to ~1e-12 for inputs within roughly[-700, 700](the exponent range that fits in a normal double).axl_exp(0.0) == 1.0,axl_exp(1.0) ≈ AXL_MATH_E,axl_exp(AXL_MATH_LOG_2) == 2.0.Large positive inputs saturate at the maximum representable double (~1.8e308) — no FP overflow trap. Large negative inputs underflow cleanly to 0.
- Returns:
e^x, saturated at the double range boundaries.
-
double axl_pow(double base, double exponent)
Power —
baseraised toexponent.Implemented as
exp(exponent * ln(base))forbase > 0. Accuracy compounds thelnandexperrors but stays comfortably under 1e-6 for UI-scale inputs (sRGB↔linear conversion with γ=2.2, easing curves, alpha falloff, etc.).Fast-path edge cases:
axl_pow(x, 0.0) == 1.0for any base, includingaxl_pow(0.0, 0.0)per IEEE 754 §9.2.1.axl_pow(1.0, y) == 1.0for any finite exponent.axl_pow(0.0, y > 0)== 0.
Warning
Negative base returns 0 — we don’t distinguish integer from non-integer exponent (libm would return a real value for integer exponent and NaN for non-integer). Callers that need negative-base power must implement the integer-exponent case themselves.
- Parameters:
base – must be
>= 0exponent – the exponent (any real)
- Returns:
base ^ exponent.
-
double axl_atan(double x)
Arctangent — angle whose tangent is x.
Range-reduces via two identities:
|x| > 1→atan(x) = sgn(x) * π/2 - atan(1/x).|x| > 0.5→atan(x) = π/4 * sgn(x) + atan((|x|-1)/(|x|+1)). After reductions|x| ≤ 0.5(in fact|x| ≤ 1/3if both steps fire); Tayloratan(x) = x - x³/3 + x⁵/5 - ...then converges fast. Accurate to ~1e-9 across the full input range.
axl_atan(0.0) == 0.0,axl_atan(1.0) ≈ π/4,axl_atan(±∞)returns±π/2.- Returns:
Angle in
[-π/2, π/2].
-
double axl_atan2(double y, double x)
Two-argument arctangent — full-circle angle of vector
(x, y).Returns the angle from the positive x-axis to the vector
(x, y), in the range(-π, π]. Standard quadrant handling:x > 0→atan(y/x)in(-π/2, π/2)x < 0,y ≥ 0→atan(y/x) + πin(π/2, π]x < 0,y < 0→atan(y/x) - πin(-π, -π/2)x == 0,y > 0→π/2x == 0,y < 0→-π/2x == 0,y == 0→0(degenerate input, safe-default)
axl_atan2(0.0, 1.0) == 0,axl_atan2(1.0, 0.0) == π/2,axl_atan2(0.0, -1.0) == π.Sign-of-zero is NOT distinguished:
axl_atan2(-0.0, -1.0)returns+π, not-πas IEEE 754 would. axl-sdk has no hard wire-compat bar with libm here; consumers needing IEEE sign-of-zero semantics can wrap with their own signbit check.- Parameters:
y – vertical component
x – horizontal component
- Returns:
Angle in
(-π, π].
-
double axl_asin(double x)
Arcsine — angle whose sine is x.
Computed via the identity
asin(x) = atan(x / √(1 - x²))for|x| < 1, with exact endpoints at±1.axl_asin(0.0) == 0.0,axl_asin(1.0) == π/2,axl_asin(-1.0) == -π/2.Warning
Out-of-domain input (
|x| > 1) returns 0 — same safe-default convention asaxl_sqrtfor negative input. Caller is expected to check the domain.- Returns:
Angle in
[-π/2, π/2].
-
double axl_acos(double x)
Arccosine — angle whose cosine is x.
Implemented as
π/2 - axl_asin(x).axl_acos(1.0) == 0.0,axl_acos(0.0) == π/2,axl_acos(-1.0) == π.Warning
Out-of-domain input (
|x| > 1) returns 0 (same convention asaxl_asin).- Returns:
Angle in
[0, π].
-
double axl_clamp(double x, double lo, double hi)
Clamp x to the closed interval
[lo, hi].axl_clamp(5.0, 0.0, 10.0) == 5.0,axl_clamp(-1.0, 0.0, 10.0) == 0.0,axl_clamp(11.0, 0.0, 10.0) == 10.0.Warning
If
lo > hithe result is implementation-defined; the current implementation returns hi (i.e. the upper clamp wins). Callers are expected to passlo ≤ hi.- Parameters:
x – value to clamp
lo – lower bound (inclusive)
hi – upper bound (inclusive)
- Returns:
x clamped to
[lo, hi].
-
double axl_min(double a, double b)
Smaller of two values.
axl_min(3.0, 5.0) == 3.0,axl_min(-2.0, -5.0) == -5.0.- Returns:
The smaller of a and b (returns b on tie, but either is correct for equal inputs).
-
double axl_max(double a, double b)
Larger of two values.
axl_max(3.0, 5.0) == 5.0,axl_max(-2.0, -5.0) == -2.0.- Returns:
The larger of a and b.
-
double axl_remap(double x, double in_min, double in_max, double out_min, double out_max)
Linear remap — map x from input range
[in_min, in_max]to output range[out_min, out_max].axl_remap(50, 0, 100, 0, 1) == 0.5. Input values outside[in_min, in_max]extrapolate linearly (the formula is not clamped — wrap withaxl_clampat the call site if you want saturation).Degenerate input range (
in_min == in_max) returns out_min to avoid division by zero.- Parameters:
x – input value
in_min – input range lower bound
in_max – input range upper bound
out_min – output range lower bound
out_max – output range upper bound
- Returns:
The remapped value.
-
double axl_step(double edge, double x)
GLSL-style step function.
Returns 0.0 if
x < edge, else 1.0. The boundary valuex == edgereturns 1.0 (matches the GLSL spec).- Returns:
0.0 or 1.0.
-
double axl_smoothstep(double edge0, double edge1, double x)
GLSL-style smoothstep — cubic Hermite interpolation.
Returns 0.0 when
x ≤ edge0, 1.0 whenx ≥ edge1, and a smootht*t*(3 - 2*t)Hermite curve in between, wheret = clamp((x - edge0)/(edge1 - edge0), 0, 1).Useful for anti-aliased edge transitions and easing. The midpoint
(edge0 + edge1) / 2returns exactly 0.5.- Returns:
Smoothly interpolated value in
[0.0, 1.0].
-
double axl_lerp(double a, double b, double t)
Linear interpolation:
a + (b - a) * t.axl_lerp(0, 100, 0.5) == 50. Inputs outside[0, 1]extrapolate linearly (no clamping — wrap withaxl_clampat the call site if you want saturation, same shape asaxl_remap).t == 0returns a exactly,t == 1returns b exactly.
-
double axl_ease_in_cubic(double t)
Cubic ease-in:
t³. Slow start, accelerates to full speed.f(0) = 0,f(1) = 1, f(0) = 0(zero initial velocity). CSScubic-bezier(0.55, 0.055, 0.675, 0.19)` approximation.
-
double axl_ease_out_cubic(double t)
Cubic ease-out:
1 - (1-t)³. Fast start, decelerates.f(0) = 0,f(1) = 1, f(1) = 0` (zero ending velocity).
-
double axl_ease_in_out_cubic(double t)
Cubic ease-in-out:
t < 0.5 ? 4t³ : 1 - (-2t+2)³/2.Slow at both ends, full speed at midpoint. Most-used default for UI panel slide / fade transitions.
-
double axl_ease_in_quint(double t)
Quintic ease-in:
t⁵. More aggressive slow-start than cubic.
-
double axl_ease_out_quint(double t)
Quintic ease-out:
1 - (1-t)⁵. Dramatic deceleration — item snapping into place from a flick.
-
double axl_ease_in_out_quint(double t)
Quintic ease-in-out:
t < 0.5 ? 16t⁵ : 1 - (-2t+2)⁵/2.
-
double axl_ease_in_sine(double t)
Sinusoidal ease-in:
1 - cos(t * π/2). Subtle slow-start.Clamped to
[0, 1]at the endpoints — does NOT extrapolate fort < 0ort > 1(unlike the cubic/quint variants). The short-circuit guarantees exactf(0) == 0andf(1) == 1so animation keyframes don’t drift.
-
double axl_ease_out_sine(double t)
Sinusoidal ease-out:
sin(t * π/2). Subtle slow-finish.Clamped to
[0, 1]; seeaxl_ease_in_sinefor rationale.
-
double axl_ease_in_out_sine(double t)
Sinusoidal ease-in-out:
(1 - cos(t * π)) / 2.Symmetric S-curve; less aggressive midpoint slope than the cubic/quint variants. Clamped to
[0, 1].
-
int axl_clz(uint64_t x)
Count leading zero bits in x.
Returns 64 for input 0 (
__builtin_clzll(0)is undefined; we substitute a defined value).axl_clz(1) == 63,axl_clz(1ULL << 63) == 0.- Returns:
Number of leading zero bits, in
[0, 64].
-
int axl_ctz(uint64_t x)
Count trailing zero bits in x.
Returns 64 for input 0 (same UB-substitution as
axl_clz).axl_ctz(1) == 0,axl_ctz(1ULL << 63) == 63,axl_ctz(6) == 1.- Returns:
Number of trailing zero bits, in
[0, 64].
-
int axl_popcount(uint64_t x)
Count set bits (population count) in x.
axl_popcount(0) == 0,axl_popcount(7) == 3,axl_popcount(~0ULL) == 64.- Returns:
Number of set bits, in
[0, 64].
-
int axl_log2i(uint64_t x)
Integer floor-log₂: largest
isuch that2^i ≤ x.axl_log2i(1) == 0,axl_log2i(2) == 1,axl_log2i(255) == 7,axl_log2i(256) == 8.Warning
axl_log2i(0)returns 0 (safe-default; mathematically log₂(0) is -∞).- Returns:
floor(log2(x))forx > 0, else0.
-
uint64_t axl_round_up_pow2(uint64_t x)
Round x up to the nearest power of two.
axl_round_up_pow2(0) == 1,axl_round_up_pow2(1) == 1,axl_round_up_pow2(5) == 8,axl_round_up_pow2(256) == 256(powers of two are fixed points).Warning
Inputs greater than
1ULL << 63would overflow; returns 0 as the safe-default in that case.- Returns:
The smallest power of two
≥ x, or 0 on overflow.
-
uint8_t axl_sat_add_u8(uint8_t a, uint8_t b)
Saturating add:
min(a + b, 0xFF).axl_sat_add_u8(200, 100) == 255(instead of wrapping to 44).
-
uint8_t axl_sat_sub_u8(uint8_t a, uint8_t b)
Saturating subtract:
max(a - b, 0).axl_sat_sub_u8(50, 100) == 0(instead of wrapping to 206).
-
uint16_t axl_sat_mul_u16(uint16_t a, uint16_t b)
Saturating multiply:
min(a * b, 0xFFFF).axl_sat_mul_u16(1000, 1000) == 0xFFFF(instead of wrapping).
-
double axl_vec2_dot(AxlVec2 a, AxlVec2 b)
Dot product:
a.x*b.x + a.y*b.y.Equals
|a| * |b| * cos(θ)where θ is the angle between them. Negative iff the angle is obtuse, zero iff perpendicular.
-
AxlVec2 axl_vec2_normalize(AxlVec2 v)
Normalize v to unit length.
axl_vec2_normalize((3, 4))returns(0.6, 0.8).Warning
Zero-length input returns
(0, 0)— same safe-default convention asaxl_sqrt(negative → 0). Catch the length-zero bug at the call site by checking the input.
-
AxlVec2 axl_vec2_lerp(AxlVec2 a, AxlVec2 b, double t)
Linear interpolation:
a + (b - a) * t.t = 0→ a,t = 1→ b.toutside[0, 1]extrapolates (no clamp).
-
AxlVec2 axl_vec2_perp(AxlVec2 v)
Left perpendicular:
(-v.y, v.x)— v turned 90° (toward +y from +x).axl_vec2_perp((1, 0)) == (0, 1). Negate for the right perpendicular.
-
double axl_vec2_cross(AxlVec2 a, AxlVec2 b)
2D cross product (scalar z-component):
a.x*b.y - a.y*b.x.Equals
|a| * |b| * sin(θ): positive iff b is counter-clockwise from a, zero iff parallel. The sign is the standard orientation test (twice the signed area of the triangle0, a, b).
-
AxlVec2 axl_vec2_rotate(AxlVec2 v, double radians)
Rotate v by radians about the origin (column-vector
[c -s; s c], same convention asaxl_transform_rotate).
-
double axl_vec2_angle(AxlVec2 v)
Angle of v from the +x axis, in radians
(-π, π]—atan2(v.y, v.x).axl_vec2_angle((0, 1)) == π/2.(0, 0)returns 0.
-
AxlVec2 axl_vec2_reflect(AxlVec2 v, AxlVec2 n)
Reflect v across the line through the origin with unit normal n:
v - 2*(v·n)*n. n MUST be unit length (normalize first); a non-unit normal scales the result.
-
AxlVec2 axl_vec2_project(AxlVec2 a, AxlVec2 b)
Vector projection of a onto b:
(a·b / b·b) * b— the component of a parallel to b. Zero-length b returns(0, 0)(safe default, matchingaxl_vec2_normalize).
-
AxlTransform axl_transform_identity(void)
Identity matrix — leaves any transformed point unchanged.
[1 0 0; 0 1 0; 0 0 1].
-
AxlTransform axl_transform_translate(double tx, double ty)
Translation matrix.
axl_transform_map_point(axl_transform_translate(3, 4), p)equals(p.x + 3, p.y + 4).
-
AxlTransform axl_transform_scale(double sx, double sy)
Non-uniform scale matrix.
sx == sy == 1is the identity scale.
-
AxlTransform axl_transform_rotate(double radians)
Rotation matrix. Angle in radians.
Always computes
[c -s; s c](the standard column-vector rotation matrix) —axl_transform_rotate(AXL_MATH_HALF_PI)applied to(1, 0)returns(0, 1)regardless of any downstream y-axis convention. Whether that visually reads as CCW or CW depends on whether the framebuffer is y-up (math/SVG convention) or y-down (axl-gfx and stb-style rasterizers).
-
AxlTransform axl_transform_shear(double sx, double sy)
Skew matrix.
sxshears in the x direction proportional to y;syshears in y proportional to x. Both arguments are the tangent of the shear angle (matches the CSSskew()convention: passaxl_sin(angle)/axl_cos(angle)for the angle form).axl_transform_map_point(axl_transform_shear(0.5, 0), (0, 1))returns(0.5, 1)— y stays the same, x shifts by0.5 * y.
-
AxlTransform axl_transform_multiply(AxlTransform a, AxlTransform b)
Compose two transforms: the result applies a first, then b.
cairo
cairo_matrix_multiplyoperand order — for column-vector points it is the matrix productb · a. So:NOTE: the operand order is cairo a-first as of v0.22.0 (pre-release); the predecessorM = axl_transform_multiply(axl_transform_rotate(AXL_MATH_HALF_PI), axl_transform_translate(10, 0)); // M rotates first, THEN translates by (10, 0)
axl_mat3_mul(a, b)appliedbfirst.
-
AxlVec2 axl_transform_map_point(AxlTransform m, AxlVec2 p)
Apply matrix m to point p, with perspective divide.
Treats
pas the column vector[p.x p.y 1]ᵀ, computes [x y’ w]ᵀ = m·p, and returns(x’/w, y’/w). For an affine matrix the bottom row is[0 0 1]sow` is exactly 1 and the divide is a no-op (results are bit-exact); a non-trivial bottom row (perspective) makes the divide meaningful.axl_transform_map_point(axl_transform_translate(3, 4), (1, 1))returns(4, 5).
-
AxlVec2 axl_transform_map_vector(AxlTransform m, AxlVec2 v)
Apply only the linear part of m to v — the upper-left 2×2, ignoring translation. Use for directions / deltas / sizes (a drag vector, a surface normal) that should rotate and scale but not shift.
(m0*x + m1*y, m3*x + m4*y).
-
double axl_transform_determinant(AxlTransform m)
Determinant of m (full 3×3). For an affine matrix (
[0 0 1]bottom row) this reduces tom0*m4 - m1*m3, the signed area scale of the linear part; zero iff m is singular (non-invertible / collapses to a line or point).
-
bool axl_transform_invert(AxlTransform m, AxlTransform *out)
Invert m. Writes the inverse to out and returns true; returns false (leaving out unmodified) if m is singular (≈ zero determinant).
The inverse maps results back to inputs — e.g. converting a screen point to local coordinates by inverting the transform it was drawn with (hit-testing). Computed by the adjugate / determinant for the general 3×3 case.
- Parameters:
m – [in] matrix to invert
out – [out] receives the inverse (untouched if singular)
-
AxlTransform axl_transform_perspective(double px, double py)
A perspective transform with bottom row
[px py 1].Maps
(x, y)to(x, y) / (px*x + py*y + 1)— the points where the denominator stays positive are foreshortened toward the origin aspx*x + py*ygrows.axl_transform_perspective(0, 0)is the identity. Compose with the affine builders viaaxl_transform_multiplyto build a general projective map, or useaxl_transform_quad_to_quadto derive one from corner correspondences.- Parameters:
px – x-weight of the perspective denominator (m[6])
py – y-weight of the perspective denominator (m[7])
-
bool axl_transform_quad_to_quad(const AxlVec2 src[4], const AxlVec2 dst[4], AxlTransform *out)
Build the transform mapping one quad onto another (4 point correspondences) — the general projective map, exact for any non-degenerate simple quad (convex or concave; no general solver, closed form via the unit square).
Corners are matched by index in the order top-left, top-right, bottom-right, bottom-left (consistent winding for both quads):
map_point(result, src[i]) == dst[i]for eachi. Use it to warp a source rectangle onto an arbitrary on-screen quadrilateral.- Parameters:
src – [in] source corners (TL, TR, BR, BL)
dst – [in] destination corners (same order)
out – [out] receives src→dst transform
- Returns:
true on success; false (leaving out untouched) if src is degenerate (collinear / zero-area — not invertible).
-
AxlRect axl_transform_map_rect(AxlTransform m, AxlRect r)
Map the four corners of axis-aligned rect r through m and return the axis-aligned bounding box of the result.
Exact when m is axis-aligned (
axl_transform_is_axis_aligned); otherwise it is the tight AABB enclosing the (rotated / sheared / projected) image — a conservative cover, the usual input to a clip or dirty-region test. A normalized rect (non-negative w/h) is returned.Defined when r does not cross m’s horizon — i.e. all four corners map with the same sign of
w(always true for affine maps and for the rect→on-screen-quad warpsaxl_transform_quad_to_quadproduces). A projective m whose horizon line passes through r has no finite enclosing box, and the returned rect is meaningless; clip r to the front of the horizon first.
-
void axl_transform_map_quad(AxlTransform m, const AxlVec2 in[4], AxlVec2 out[4])
Map four points through m (full perspective divide each), writing the images to out. in and out may alias. For clip-region and quad-corner work where the bounding box of
map_rectis too loose.- Parameters:
m – transform to apply
in – [in] four source points
out – [out] four mapped points
-
AxlTransformClass axl_transform_classify(AxlTransform m)
Classify m by its contents (see
AxlTransformClass). Pure function of the matrix; uses a small tolerance so composed transforms classify as expected despite floating-point drift.
-
bool axl_transform_is_identity(AxlTransform m)
True iff m is (within tolerance) the identity.
-
bool axl_transform_is_axis_aligned(AxlTransform m)
True iff m maps every axis-aligned rectangle to an axis-aligned rectangle — i.e. non-perspective with a diagonal or anti-diagonal linear part (axis scales, 90° rotations, flips). The condition under which
axl_transform_map_rectis exact.
-
bool axl_transform_is_affine(AxlTransform m)
True iff m is affine (bottom row
[0 0 1], no perspective) — equivalentlyaxl_transform_classify(m) != AXL_TRANSFORM_PROJECTIVE.
-
bool axl_point_in_rect(AxlVec2 p, AxlRect r)
Half-open hit test: point is “inside”
riffr.x ≤ p.x < r.x + r.wand the same for y. Right and bottom edges are EXCLUDED (the standard convention so adjacent rects don’t both claim a shared edge).
-
AxlRect axl_rect_intersect(AxlRect a, AxlRect b)
Intersection of two rects. Returns the empty rect
{0, 0, 0, 0}if they don’t overlap.
-
AxlRect axl_rect_union(AxlRect a, AxlRect b)
Smallest rect containing both inputs. If either is empty (
w <= 0orh <= 0), returns the other unchanged.
-
bool axl_segment_intersect(AxlVec2 a1, AxlVec2 a2, AxlVec2 b1, AxlVec2 b2, AxlVec2 *out)
Segment-segment intersection test. Returns
trueiff the segments cross at a single point on both segments (parameterst, s ∈ [0, 1]). Ifoutis non-NULL the intersection coordinates are written there.Warning
Parallel segments — including collinear overlap that shares infinitely many points — return
false. The algorithm tests for a unique intersection only, not for any-shared-point. Document at the caller when collinear-overlap matters.- Parameters:
a1 – first segment, endpoint 1
a2 – first segment, endpoint 2
b1 – second segment, endpoint 1
b2 – second segment, endpoint 2
out – optional, may be NULL
-
double axl_distance_point_to_segment(AxlVec2 p, AxlVec2 a, AxlVec2 b)
Shortest distance from point p to the closed segment from a to b. Projects p onto the segment line, clamps the parameter to
[0, 1], then measures Euclidean distance from p to the clamped point.Degenerate segment (
a == b) returns|p - a|.
-
struct AxlVec2
- #include <axl-math.h>
2D vector / point — double-precision components.
Used for both directional vectors and positional points; math operations don’t distinguish between the two. Doubles match the rest of AxlMath; gfx consumers that store coordinates as
floatconvert at the call boundary.
-
struct AxlTransform
- #include <axl-math.h>
2D transform — 3×3 homography over
[x y 1]ᵀcolumn vectors, row-major. The single transform type for the library (the gfx CTM, the matrix toolkits hand to the transform-aware primitives, etc.).Stored as a flat 9-element array indexed:
An affine transform has bottom row[ m[0] m[1] m[2] ] [ m[3] m[4] m[5] ] [ m[6] m[7] m[8] ]
[0 0 1]and decodes as[a b tx; c d ty; 0 0 1](scale/rotate in the 2×2 sub-matrix, translation in the last column); a non-trivial bottom row encodes perspective.Public Members
-
double m[9]
-
double m[9]
-
struct AxlRect
- #include <axl-math.h>
Axis-aligned rectangle, defined by its top-left corner and width/height. Negative
worhare treated as empty rects by all the helpers below — the canonical form normalizes to non-negative extents.Edge semantics are HALF-OPEN: top/left included, bottom/right excluded (so adjacent rects don’t both claim a shared edge). This is intentionally asymmetric with
AxlCircle, which uses CLOSED intersection — rects are a tiling primitive, circles aren’t, and each convention matches its respective domain.
-
struct AxlCircle
- #include <axl-math.h>
Circle, defined by center and radius. Negative radius is treated as a degenerate (always-false-intersect) circle.
Intersection semantics are CLOSED — a tangent contact counts as an intersection. See
AxlRectfor the rationale on why rects and circles use different boundary conventions.