fixed 0.1.0
Signed Q32.32 fixed-point scalar for provable game math (shared by glam, nalgebra and rapier ports)
Signed Q32.32 fixed-point scalar for provable game math: Fixed { raw: i64 }, fused kernels that
rescale once per output (wide), and loop-free transcendental functions (trig, exp).
Shared by the Cairo ports of glam, nalgebra and rapier. No dependencies.
Design, rounding and overflow policy: docs/DESIGN.md.
Compatible with Cairo 2.19.4.
fixed::fixed)use fixed::{Fixed, FixedTrait, HALF, ONE, PI};
let a: Fixed = 3_i32.into(); // exact, 100 gas
let b = FixedTrait::from_ratio(1, 3); // 1/3, rounded toward zero
let c = a * b + HALF; // `*` floors, `+` is a native i64 add
assert!(c.abs_diff_eq(ONE + HALF, FixedTrait::from_raw(4)));
let d = (PI / a).sqrt().lerp(ONE, HALF);
value = raw / 2^32, range [-2^31, 2^31), resolution 2^-32 (EPSILON).f32 and glam's FloatExt: abs signum copysign min max clamp floor ceil round trunc fract fract_gl recip sqrt div_euclid rem_euclid mul_add powi lerp inverse_lerp remap step saturate smoothstep move_towards abs_diff_eq, the operators + - * / % -x, their
*Assign forms, PartialOrd, Zero, One, Bounded, and the f32::consts constants.*, mul_add, lerp and every fused kernel
round toward negative infinity; /, %, recip, from_ratio round toward zero.'Fixed: overflow', 'Fixed: division by zero', 'Fixed: sqrt negative'; the native +, -
and unary - keep the corelib messages, e.g. 'i64_add Overflow'). Nothing wraps or saturates.fixed::wide)A raw product Fixed * Fixed costs one step; its rescale costs a range check and a division.
Every kernel sums exact raw products and rescales once per output scalar:
use fixed::wide::{RecipTrait, WideAdd, WideMul, WideNarrow, WideSub, det3, dot3, normalize3, wide_mul};
let d = dot3(ax, bx, ay, by, az, bz); // 1 rescale instead of 3
let cross_x = wide_mul(ay, bz).sub(wide_mul(by, az)).narrow(); // a*b - c*d
let triple = wide_mul(a, b).sub(wide_mul(c, d)).mul(e).narrow(); // (a*b - c*d) * e, exact
let (nx, ny, nz) = normalize3(x, y, z); // 1 sqrt + 1 division
let r = RecipTrait::new(det); // divide many values by `det`
let m00 = r.mul(adj00);
W1..W16: exact sums of up to 16 products (Q64.64). T1..T16: exact sums of up to 16 triple
products (Q96.96), built with Wn.mul(Fixed). The index is tracked by the type system:
Wn + Wm -> W(n+m). Additions, subtractions and negations cost one step and cannot overflow;
only narrow() is range-checked.dot2/3/4, dot2_add, dot3_add, mul_add, mul_sub, det3,
norm2/3/4 (length as the integer square root of the raw sum of squares: no rescale, no
underflow), norm*_squared, distance2/3/4 (exact differences), distance*_squared,
normalize2/3/4, and the Norm / Recip types behind them.fixed::trig)use fixed::{FRAC_PI_2, Fixed, TrigTrait};
let (s, c) = angle.sin_cos(); // one reduction, one z^2, both results
let a = y.atan2(x); // Rust argument order: y.atan2(x)
let half = (dot.acos_clamped()) / 2.into(); // acos of a dot product that rounding pushed past 1
let rad = 45.into::<Fixed>().to_radians();
sin cos sin_cos tan asin acos asin_clamped acos_clamped atan atan2 to_radians to_degrees,
named after Rust's f32. acos_clamped / asin_clamped clamp the argument to [-1, 1] like
glam's acos_approx; acos / asin panic outside it ('Fixed: acos domain',
'Fixed: asin domain'). tan panics with 'Fixed: tan overflow' next to an odd multiple of
pi / 2.DivRem), a match on the reduced
index and a minimax polynomial in Horner form. No CORDIC, no Taylor recursion, no table; the
cost does not depend on the value of the input, only on the branch it takes.sin / cos over a whole turn, 0.55 inside one octant, 1.08 at
1000 * TAU (the Cody-Waite tail of pi / 4 keeps a large angle as accurate as a small one),
3.22 ULP for atan2, 2.96 for acos. The polynomial accumulators carry 24 extra fractional
bits and the final rescale rounds to nearest (the second exception to the floor rule of
docs/DESIGN.md section 2, and a free one), so the error is centred on zero.sin(-x) = -sin(x),
cos(-x) = cos(x), atan(-x) = -atan(x), asin(-x) = -asin(x), sin(0) = 0, cos(0) = 1,
sin(FRAC_PI_2) = 1, cos(PI) = -1, tan(FRAC_PI_4) = 1, acos(1) = 0,
acos(0) = FRAC_PI_2, acos(-1) = PI, asin(0) = 0, and the four axes of atan2
(atan2(0, 0) = 0, atan2(y, 0) = +-FRAC_PI_2, ...).scripts/gen_trig.py, which also holds a bit-exact Python
mirror of every function (gen_trig.py sweep measures the errors, gen_trig.py tables
regenerates the test vectors, gen_trig.py check verifies that the committed constants are up
to date).fixed::exp)use fixed::exp::ExpTrait;
use fixed::{Fixed, HALF, TWO};
let g = (-t / tau).exp(); // decay factor
let db = power.log10(); // ln, log2, log10 share one core
let r = TWO.powf(HALF); // sqrt(2) = exp2(0.5 * log2(2))
let steps = x.log(TWO); // log(self, base), Rust argument order
exp exp2 exp_m1 ln log2 log10 ln_1p log powf, named after Rust's f32 (sqrt and powi
live in FixedTrait). ExpTrait is not re-exported at the crate root yet:
use fixed::exp::ExpTrait;.exp / exp2 panic with 'Fixed: exp overflow' from 31 ln 2 (21.487) / 31
on, and return 0 below -33 ln 2 / -33 (the result is below half an ULP); the logarithms
panic with 'Fixed: ln domain' for x <= 0; powf panics with 'Fixed: powf domain' for a
negative base and a non-integer exponent (Rust returns NaN), accepts 0^0 = 1 and 0^n = 0
like Rust, and computes a negative base with an integer exponent as (-1)^n |x|^n.exp2 is one DivRem (x = k/16 + g), one lookup in a 1 024-entry const
table of 2^(k/16) (the power of two and the segment together) and a degree-6 polynomial;
exp multiplies by a 56-bit log2(e) and reuses it. log2 finds the exponent with an
unrolled binary search on constant thresholds (6 comparisons), then evaluates one of 32
degree-4 segments in the exact 62-bit mantissa; ln / log10 rescale the same accumulator.
powf chains the two on the wide accumulators (n * log2(x) is never rounded to 32 bits).exp2 / exp within 2.02 ULP below 2^16 and 7.2e-6 * 2^-30 relative
above; log2 0.75, ln 0.66, log10 0.57 ULP over the whole positive range; powf within
0.44 * 2^-30 relative for x in [2^-8, 2^8], n in [-4, 4].exp2(k) = 2^k, log2(2^k) = k, exp(0) = 1,
ln(1) = 0; every function is non-decreasing (the table and the final rescale of exp2 floor,
and every segment is sealed so that it ends at or below the start of the next one). The
logarithms round their final rescale to nearest; exp2 / exp / powf floor.scripts/gen_exp.py
(sweep, tables, check), like trig.Sierra gas (l2 gas, what a transaction pays) and prover cost (steps, range checks) of one call, net of the test overhead (X__op - X__base, see scripts/bench.py), measured with scarb 2.19.4, starknet-foundry 0.61.0 (.tool-versions). Source of truth: gas/*.snap; this region is generated by scripts/gas_tables.py, do not edit it.
fixed::fixed)+ / -
840
7
2
*
1 680
14
4
/
3 740
32
6
%
3 170
27
5
<
770
7
1
sqrt
2 020
16
6
recip
3 370
29
5
floor
1 310
11
3
round
2 080
18
4
lerp
1 980
17
4
smoothstep
8 140
69
16
powi(5)
22 950
205
32
fixed::wide)dot2
1 880
16
4
dot3
2 080
18
4
dot4
2 280
20
4
mul_add
1 880
16
4
mul_sub
1 880
16
4
det3
3 800
34
4
norm3
3 340
28
6
distance3
3 640
31
6
normalize3
8 720
72
20
Recip::new
4 400
37
8
Recip::mul
6 820
57
14
fixed::trig)sin
22 730
152
37
cos
22 130
165
41
sin_cos
31 300
243
62
tan
39 850
287
70
atan
22 150
154
35
atan2
28 120
202
44
asin
27 660
223
58
acos
27 390
215
56
acos_clamped
28 930
229
58
to_radians
1 680
14
4
fixed::exp)exp
20 840
168
43
exp2
19 160
154
39
exp_m1
21 680
175
45
ln
19 520
160
37
log2
19 520
160
37
log10
19 520
160
37
ln_1p
21 280
175
39
log
40 710
327
72
powf
47 630
341
81
All the arithmetic is written with core::internal::bounded_int (an unstable corelib API) and is
isolated in the private fixed::internal module, generated by
scripts/gen_bounded.py: every BoundedInt bound and every bias
constant is computed by the script (scripts/gen_bounded.py --check verifies that the committed
files are up to date). A stable-API implementation of mul and div is kept and benchmarked in
benches::alt::fixed (mul_stable, div_stable) as the fallback.
Version 0.1.0
Uploaded 11 hours ago
License MIT
Cairo version ^2.19.4
Size 79.7 KB
Run the following command in your project dir
scarb add fixed@0.1.0
Or add the following line to your Scarb.toml
fixed = "0.1.0"