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

axl_floor / axl_ceil

double double, libm-shape

axl_floori / axl_ceili

double int, pixel-snap convenience

axl_fabs

absolute value

axl_sqrt

Newton’s method internally; negative input clamps to 0

axl_fmod

x - trunc(x/y) * y; zero-divisor returns 0

axl_sin / axl_cos

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

floor, ceil, floori, ceili

exact

Integer-cast + sign correction

fabs

exact

sqrt

~1e-12

10 Newton iterations

fmod

~1e-12

Truncated quotient

sin, cos

~1e-7

6-term Taylor through x¹¹/11!

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.

-march baseline

sqrt

floor / ceil

fabs

sin/cos Horner

x86-64 (default)

SQRTSD (SSE2)

manual fallback

ANDPD-mask (SSE2)

mul + add

x86-64-v2 (Nehalem 2008+)

SQRTSD

ROUNDSD (SSE4.1)

ANDPD

mul + add

x86-64-v3 (Haswell 2013+)

SQRTSD

ROUNDSD

ANDPD

VFMADD

armv8-a (any AArch64)

FSQRT

FRINTM / FRINTP

FABS

FMADD

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:

  1. Run with -cpu host and KVM available. scripts/run-qemu.sh already does this automatically when /dev/kvm is readable + writable. Modern hosts (post-2008 Intel, post-Bulldozer AMD) have all the features axl-sdk can request.

  2. 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 -march levels closely (Nehalemx86-64-v2, Haswellx86-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 timex -= * 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_classify returns the most specific kind that fits. Renderers and the mapping routines key fast paths off this (skip the perspective divide when not PROJECTIVE, use an axis-aligned blit when SCALE, 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)

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 int directly — convenience for pixel-snap and bbox arithmetic where the next consumer is integer-typed.

int axl_ceili(double x)

Ceiling returned as int directly.

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 of double for 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 as axl_fmod but using floor (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 <= 0 the result is implementation-defined; the current implementation returns 0 (safe-default, mirroring axl_fmod’s zero-divisor convention).

Parameters:
  • x – value to wrap

  • n – period (must be > 0)

Returns:

Value in [0, n) for n > 0, else 0.

double axl_sin(double x)

Sine.

Range-reduces x to [-π/2, π/2] and evaluates a 6-term Taylor series (through x¹¹/11!). Accurate to ~1e-7 over the full input range.

double axl_cos(double x)

Cosine. Derived from axl_sin via the standard identity cos(x) = sin(x + π/2).

double axl_ln(double x)

Natural logarithm — base e.

Named axl_ln (not axl_log) to avoid clashing with the logging-system function of the same name in <axl/axl-log.h>. The math notation ln is unambiguous for natural log.

Range-reduces via the IEEE 754 exponent: x = m * 2^e with m [1, 2), so ln(x) = ln(m) + e * AXL_MATH_LOG_2. Mantissa log evaluated via the substitution s = (m-1)/(m+1) and the odd-only series ln(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) for x > 0, else 0.

double axl_exp(double x)

Natural exponential — e^x.

Range-reduces via x = k * log(2) + r with k integer and r [-log(2)/2, log(2)/2], evaluates a 10-term Taylor series on r, then reconstructs 2^k by 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 — base raised to exponent.

Implemented as exp(exponent * ln(base)) for base > 0. Accuracy compounds the ln and exp errors 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.0 for any base, including axl_pow(0.0, 0.0) per IEEE 754 §9.2.1.

  • axl_pow(1.0, y) == 1.0 for 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 >= 0

  • exponent – the exponent (any real)

Returns:

base ^ exponent.

double axl_atan(double x)

Arctangent — angle whose tangent is x.

Range-reduces via two identities:

  • |x| > 1atan(x) = sgn(x) * π/2 - atan(1/x).

  • |x| > 0.5atan(x) = π/4 * sgn(x) + atan((|x|-1)/(|x|+1)). After reductions |x| 0.5 (in fact |x| 1/3 if both steps fire); Taylor atan(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 > 0atan(y/x) in (-π/2, π/2)

  • x < 0, y 0atan(y/x) + π in (π/2, π]

  • x < 0, y < 0atan(y/x) - π in (-π, -π/2)

  • x == 0, y > 0π/2

  • x == 0, y < 0-π/2

  • x == 0, y == 00 (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 as axl_sqrt for 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 as axl_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 > hi the result is implementation-defined; the current implementation returns hi (i.e. the upper clamp wins). Callers are expected to pass lo 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 with axl_clamp at 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 value x == edge returns 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 when x edge1, and a smooth t*t*(3 - 2*t) Hermite curve in between, where t = clamp((x - edge0)/(edge1 - edge0), 0, 1).

Useful for anti-aliased edge transitions and easing. The midpoint (edge0 + edge1) / 2 returns 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 with axl_clamp at the call site if you want saturation, same shape as axl_remap).

t == 0 returns a exactly, t == 1 returns b exactly.

double axl_ease_in_cubic(double t)

Cubic ease-in: . 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 for t < 0 or t > 1 (unlike the cubic/quint variants). The short-circuit guarantees exact f(0) == 0 and f(1) == 1 so 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]; see axl_ease_in_sine for 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 i such that 2^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)) for x > 0, else 0.

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 << 63 would 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).

AxlVec2 axl_vec2(double x, double y)

Construct a 2D vector / point from components.

AxlVec2 axl_vec2_add(AxlVec2 a, AxlVec2 b)

Component-wise addition: (ax+bx, ay+by).

AxlVec2 axl_vec2_sub(AxlVec2 a, AxlVec2 b)

Component-wise subtraction: (ax-bx, ay-by).

AxlVec2 axl_vec2_scale(AxlVec2 v, double k)

Scalar multiplication: (v.x * k, v.y * k).

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.

double axl_vec2_length(AxlVec2 v)

Euclidean length: √(v.x² + v.y²). Uses axl_sqrt.

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 as axl_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 = 0a, t = 1b. t outside [0, 1] extrapolates (no clamp).

double axl_vec2_distance(AxlVec2 a, AxlVec2 b)

Euclidean distance between a and b|a - b|.

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 triangle 0, 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 as axl_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, matching axl_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 == 1 is 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.

sx shears in the x direction proportional to y; sy shears in y proportional to x. Both arguments are the tangent of the shear angle (matches the CSS skew() convention: pass axl_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 by 0.5 * y.

AxlTransform axl_transform_multiply(AxlTransform a, AxlTransform b)

Compose two transforms: the result applies a first, then b.

cairo cairo_matrix_multiply operand order — for column-vector points it is the matrix product b · a. So:

M = axl_transform_multiply(axl_transform_rotate(AXL_MATH_HALF_PI),
                           axl_transform_translate(10, 0));
// M rotates first, THEN translates by (10, 0)
NOTE: the operand order is cairo a-first as of v0.22.0 (pre-release); the predecessor axl_mat3_mul(a, b) applied b first.

AxlVec2 axl_transform_map_point(AxlTransform m, AxlVec2 p)

Apply matrix m to point p, with perspective divide.

Treats p as 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 to m0*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 as px*x + py*y grows. axl_transform_perspective(0, 0) is the identity. Compose with the affine builders via axl_transform_multiply to build a general projective map, or use axl_transform_quad_to_quad to 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 each i. 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 warps axl_transform_quad_to_quad produces). 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_rect is 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_rect is exact.

bool axl_transform_is_affine(AxlTransform m)

True iff m is affine (bottom row [0 0 1], no perspective) — equivalently axl_transform_classify(m) != AXL_TRANSFORM_PROJECTIVE.

bool axl_point_in_rect(AxlVec2 p, AxlRect r)

Half-open hit test: point is “inside” r iff r.x p.x < r.x + r.w and 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 <= 0 or h <= 0), returns the other unchanged.

bool axl_segment_intersect(AxlVec2 a1, AxlVec2 a2, AxlVec2 b1, AxlVec2 b2, AxlVec2 *out)

Segment-segment intersection test. Returns true iff the segments cross at a single point on both segments (parameters t, s [0, 1]). If out is 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|.

bool axl_circle_circle_intersect(AxlCircle a, AxlCircle b)

Circle-circle overlap test. Returns true iff the two closed disks share at least one point — i.e., the distance between centers is a.radius + b.radius.

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 float convert at the call boundary.

Public Members

double x
double y
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:

[ m[0] m[1] m[2] ]
[ m[3] m[4] m[5] ]
[ m[6] m[7] m[8] ]
An affine transform has bottom row [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]
struct AxlRect
#include <axl-math.h>

Axis-aligned rectangle, defined by its top-left corner and width/height. Negative w or h are 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.

Public Members

double x
double y
double w
double h
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 AxlRect for the rationale on why rects and circles use different boundary conventions.

Public Members

AxlVec2 center
double radius