Skip to main content

embedded_3dgfx/
fixed_math.rs

1//! Q16.16 fixed-point math module
2//!
3//! Provides fixed-point math operations for embedded environments:
4//! - Type alias `Q16` (i32 with 16 fractional bits, range ≈ ±32767.9999847)
5//! - Conversion: `f32 ↔ Q16`, `i16 ↔ Q16`, `q31 ↔ Q16`
6//! - Arithmetic: `mul_q16`, `mul_n_q16`, `div_q16`, `div_n_q16`
7//! - Saturating: `qadd_q16`, `qsub_q16`, `abs_q16`
8//! - Helpers: `lerp_q16`, `angle_to_q16`, `recip_q16`
9//! - Scanline: [`ScanlineInterp`] — accelerated per-scanline z + (u, v)
10//!   interpolation for the 3D rasterizer inner loop
11
12// ─────────────────────────────────────────────────────────────────────────────
13// Constants
14// ─────────────────────────────────────────────────────────────────────────────
15
16/// Fractional bit count for Q16.16.
17pub const FP_SHIFT: u32 = 16;
18
19/// 1.0 in Q16.16 representation.
20pub const FP_ONE: i32 = 1_i32 << FP_SHIFT;
21
22/// Maximum positive value of a Q16.16 number (same as i32::MAX).
23pub const Q16_MAX: i32 = i32::MAX;
24
25/// Minimum value of a Q16.16 number (same as i32::MIN).
26pub const Q16_MIN: i32 = i32::MIN;
27
28/// Type alias: `Q16` is `i32` stored in Q16.16 fixed-point format.
29///
30/// The integer part occupies bits 31..16, the fractional part bits 15..0.
31pub type Q16 = i32;
32
33// ─────────────────────────────────────────────────────────────────────────────
34// Conversions
35// ─────────────────────────────────────────────────────────────────────────────
36
37/// Convert `f32` → `Q16.16` with correct rounding.
38#[inline(always)]
39pub fn to_q16(v: f32) -> Q16 {
40    (v * 65536.0_f32 + if v >= 0.0 { 0.5 } else { -0.5 }) as i32
41}
42
43/// Convert `Q16.16` → `f32`.
44#[inline(always)]
45pub fn from_q16(v: Q16) -> f32 {
46    v as f32 / 65536.0_f32
47}
48
49/// Convert `i16` integer → `Q16.16` (shift left 16).
50#[inline(always)]
51pub fn from_i16_q16(v: i16) -> Q16 {
52    (v as i32) << 16
53}
54
55/// Convert `Q16.16` → `i16` integer (truncate fractional bits).
56#[inline(always)]
57pub fn to_i16_q16(v: Q16) -> i16 {
58    (v >> 16) as i16
59}
60
61/// Reinterpret a Q31 value as Q16.16 by shifting right 15 bits.
62#[inline(always)]
63pub fn q31_to_q16(q31: i32) -> Q16 {
64    q31 >> 15
65}
66
67/// Reinterpret Q16.16 as Q31 by shifting left 16 bits (use i64 to avoid overflow).
68#[inline(always)]
69pub fn q16_to_q31(v: Q16) -> i64 {
70    (v as i64) << 16
71}
72
73// ─────────────────────────────────────────────────────────────────────────────
74// Arithmetic
75// ─────────────────────────────────────────────────────────────────────────────
76
77/// Multiply two Q16.16 values. Uses i64 intermediate (SMULL on Cortex-M).
78#[inline(always)]
79pub fn mul_q16(a: Q16, b: Q16) -> Q16 {
80    ((a as i64 * b as i64) >> 16) as i32
81}
82
83/// Multiply a Q16.16 value by a plain `i32` integer (no fractional scaling).
84#[inline(always)]
85pub fn mul_n_q16(a: Q16, n: i32) -> Q16 {
86    a.wrapping_mul(n)
87}
88
89/// Multiply a Q16.16 value by an `f32` scalar.
90#[inline(always)]
91pub fn mul_f_q16(a: Q16, f: f32) -> Q16 {
92    mul_q16(a, to_q16(f))
93}
94
95/// Divide two Q16.16 values. Returns 0 on division by zero.
96#[inline(always)]
97pub fn div_q16(a: Q16, b: Q16) -> Q16 {
98    if b == 0 {
99        return 0;
100    }
101    (((a as i64) << 16) / b as i64) as i32
102}
103
104/// Divide a Q16.16 value by a plain `i32` integer. Returns 0 on division by zero.
105#[inline(always)]
106pub fn div_n_q16(a: Q16, n: i32) -> Q16 {
107    if n == 0 {
108        return 0;
109    }
110    a / n
111}
112
113/// Divide a Q16.16 value by an `f32` scalar.
114#[inline(always)]
115pub fn div_f_q16(a: Q16, f: f32) -> Q16 {
116    div_q16(a, to_q16(f))
117}
118
119// ─────────────────────────────────────────────────────────────────────────────
120// Saturating arithmetic
121// ─────────────────────────────────────────────────────────────────────────────
122
123/// Saturating add: clamps the result to `[i32::MIN, i32::MAX]`.
124#[inline(always)]
125pub fn qadd_q16(a: Q16, b: Q16) -> Q16 {
126    (a as i64 + b as i64).clamp(Q16_MIN as i64, Q16_MAX as i64) as i32
127}
128
129/// Saturating subtract: clamps the result to `[i32::MIN, i32::MAX]`.
130#[inline(always)]
131pub fn qsub_q16(a: Q16, b: Q16) -> Q16 {
132    (a as i64 - b as i64).clamp(Q16_MIN as i64, Q16_MAX as i64) as i32
133}
134
135/// Absolute value of a Q16.16 number.
136#[inline(always)]
137pub fn abs_q16(a: Q16) -> Q16 {
138    a.abs()
139}
140
141// ─────────────────────────────────────────────────────────────────────────────
142// Helpers
143// ─────────────────────────────────────────────────────────────────────────────
144
145/// Linear interpolation in Q16.16:  `a + (b - a) * t / denom`.
146///
147/// `t` and `denom` are plain integers (scan-line step counts).
148/// Uses i64 to avoid overflow on large `(b - a)` spans.
149#[inline(always)]
150pub fn lerp_q16(a: Q16, b: Q16, t: i32, denom: i32) -> Q16 {
151    if denom == 0 {
152        return a;
153    }
154    let diff = b as i64 - a as i64;
155    (a as i64 + diff * t as i64 / denom as i64) as i32
156}
157
158/// Convert an angle in degrees to Q16.16 radians.
159#[inline(always)]
160pub fn angle_to_q16(degrees: f32) -> Q16 {
161    to_q16(degrees * core::f32::consts::PI / 180.0)
162}
163
164/// Fast reciprocal approximation: `1.0 / v` in Q16.16.
165///
166/// Uses integer shift: `(1 << 32) / v` — gives approx 6 correct decimal digits.
167/// Returns `Q16_MAX` for zero or near-zero input (safe sentinel).
168#[inline(always)]
169pub fn recip_q16(v: Q16) -> Q16 {
170    if v == 0 {
171        return Q16_MAX;
172    }
173    ((1i64 << 32) / v as i64) as i32
174}
175
176// ─────────────────────────────────────────────────────────────────────────────
177// Legacy API shims (keep binary-compat with existing callers in draw.rs / lib.rs)
178// ─────────────────────────────────────────────────────────────────────────────
179
180/// Convert `f32` to Q16.16 (legacy alias for [`to_q16`]).
181#[inline(always)]
182pub fn to_fp(v: f32) -> i32 {
183    to_q16(v)
184}
185
186/// Convert Q16.16 to `f32` (legacy alias for [`from_q16`]).
187#[inline(always)]
188pub fn from_fp(v: i32) -> f32 {
189    from_q16(v)
190}
191
192/// Multiply two Q16.16 values (legacy alias for [`mul_q16`]).
193#[inline(always)]
194pub fn mul_fp(a: i32, b: i32) -> i32 {
195    mul_q16(a, b)
196}
197
198/// Divide two Q16.16 values, returning `Option` (legacy alias for [`div_q16`]).
199#[inline(always)]
200pub fn div_fp(a: i32, b: i32) -> Option<i32> {
201    if b == 0 { None } else { Some(div_q16(a, b)) }
202}
203
204// ─────────────────────────────────────────────────────────────────────────────
205// ScanlineInterp — accelerated z-buffer + UV interpolation
206// ─────────────────────────────────────────────────────────────────────────────
207
208/// Per-scanline interpolator for z-buffer depth and (u, v) texture coordinates.
209///
210/// Accelerated scanline rasterization helper.
211/// It pre-computes Q16.16 per-pixel step values for `z`, `u`, and `v` so
212/// the inner loop only does three wrapping additions instead of floating-point
213/// divisions per pixel — critical for MCU scanline performance.
214///
215/// # Usage
216/// ```rust,no_run
217/// # use embedded_3dgfx::fixed_math::ScanlineInterp;
218/// # let left_z = 0u32;
219/// # let right_z = 0u32;
220/// # let left_u = 0u32;
221/// # let right_u = 0u32;
222/// # let left_v = 0u32;
223/// # let right_v = 0u32;
224/// # let span_pixels = 0i32;
225/// let mut interp = ScanlineInterp::new(
226///     left_z,  right_z,   // u32 depth values (Q16.16)
227///     left_u,  right_u,   // u32 U texture coords (Q16.16)
228///     left_v,  right_v,   // u32 V texture coords (Q16.16)
229///     span_pixels,         // number of pixels across the scanline
230/// );
231///
232/// for _x in 0..=span_pixels {
233///     let z = interp.z();
234///     let u = interp.u();
235///     let v = interp.v();
236///     // ... depth test, texture sample, write pixel ...
237///     interp.step();
238/// }
239/// ```
240#[derive(Debug, Clone, Copy)]
241pub struct ScanlineInterp {
242    z_cur: u32,
243    z_step: i32,
244    u_cur: u32,
245    u_step: i32,
246    v_cur: u32,
247    v_step: i32,
248}
249
250impl ScanlineInterp {
251    /// Create a new scanline interpolator.
252    ///
253    /// # Arguments
254    /// - `z_left`, `z_right` — depth at the left and right scanline endpoints (Q16.16 `u32`)
255    /// - `u_left`, `u_right` — U texture coordinates (Q16.16 `u32`, range `[0, 65536]`)
256    /// - `v_left`, `v_right` — V texture coordinates (Q16.16 `u32`, range `[0, 65536]`)
257    /// - `span` — number of pixels across the scanline (0 is valid — returns left values)
258    #[inline]
259    pub fn new(
260        z_left: u32,
261        z_right: u32,
262        u_left: u32,
263        u_right: u32,
264        v_left: u32,
265        v_right: u32,
266        span: i32,
267    ) -> Self {
268        let (z_step, u_step, v_step) = if span > 0 {
269            let z_step = ((z_right as i64 - z_left as i64) / span as i64) as i32;
270            let u_step = ((u_right as i64 - u_left as i64) / span as i64) as i32;
271            let v_step = ((v_right as i64 - v_left as i64) / span as i64) as i32;
272            (z_step, u_step, v_step)
273        } else {
274            (0, 0, 0)
275        };
276        Self {
277            z_cur: z_left,
278            z_step,
279            u_cur: u_left,
280            u_step,
281            v_cur: v_left,
282            v_step,
283        }
284    }
285
286    /// Create an interpolator for depth-only scanlines (no texture mapping).
287    #[inline]
288    pub fn depth_only(z_left: u32, z_right: u32, span: i32) -> Self {
289        Self::new(z_left, z_right, 0, 0, 0, 0, span)
290    }
291
292    /// Current depth value (Q16.16 `u32`).
293    #[inline(always)]
294    pub fn z(&self) -> u32 {
295        self.z_cur
296    }
297
298    /// Current U texture coordinate (Q16.16 `u32`).
299    #[inline(always)]
300    pub fn u(&self) -> u32 {
301        self.u_cur
302    }
303
304    /// Current V texture coordinate (Q16.16 `u32`).
305    #[inline(always)]
306    pub fn v(&self) -> u32 {
307        self.v_cur
308    }
309
310    /// Advance all interpolators by one pixel.
311    #[inline(always)]
312    pub fn step(&mut self) {
313        self.z_cur = self.z_cur.wrapping_add_signed(self.z_step);
314        self.u_cur = self.u_cur.wrapping_add_signed(self.u_step);
315        self.v_cur = self.v_cur.wrapping_add_signed(self.v_step);
316    }
317
318    /// Advance `n` pixels at once (useful for skipping clipped scanline segments).
319    #[inline]
320    pub fn step_n(&mut self, n: i32) {
321        self.z_cur = self.z_cur.wrapping_add_signed(self.z_step.wrapping_mul(n));
322        self.u_cur = self.u_cur.wrapping_add_signed(self.u_step.wrapping_mul(n));
323        self.v_cur = self.v_cur.wrapping_add_signed(self.v_step.wrapping_mul(n));
324    }
325
326    /// Current depth as `f32`.
327    #[inline(always)]
328    pub fn z_f32(&self) -> f32 {
329        self.z_cur as f32 / 65536.0
330    }
331}
332
333// ─────────────────────────────────────────────────────────────────────────────
334// Tests
335// ─────────────────────────────────────────────────────────────────────────────
336
337#[cfg(test)]
338mod tests {
339    extern crate std;
340    use super::*;
341
342    // ── Conversions ──────────────────────────────────────────────────────────
343
344    #[test]
345    fn roundtrip_f32_q16() {
346        let values = [0.0f32, 0.25, -0.5, 1.0, 3.14159, -100.0, 32767.0];
347        for v in values {
348            let q = to_q16(v);
349            let back = from_q16(q);
350            let lsb = 1.0 / 65536.0_f32;
351            assert!(
352                (back - v).abs() <= lsb,
353                "roundtrip failed for {v}: got {back}"
354            );
355        }
356    }
357
358    #[test]
359    fn roundtrip_i16_q16() {
360        for v in [-32768i16, -1, 0, 1, 100, 32767] {
361            let q = from_i16_q16(v);
362            let back = to_i16_q16(q);
363            assert_eq!(back, v, "i16 roundtrip failed for {v}");
364        }
365    }
366
367    #[test]
368    fn q31_q16_shifts() {
369        let q31_one = 0x7FFF_FFFFi32;
370        let q16 = q31_to_q16(q31_one);
371        let f = from_q16(q16);
372        assert!(
373            (f - 1.0).abs() < 1e-4,
374            "Q31->Q16 should be near 1.0, got {f}"
375        );
376    }
377
378    // ── Arithmetic ───────────────────────────────────────────────────────────
379
380    #[test]
381    fn mul_q16_basic() {
382        let a = to_q16(1.5);
383        let b = to_q16(2.0);
384        let result = from_q16(mul_q16(a, b));
385        assert!((result - 3.0).abs() < 1e-4, "1.5 * 2.0 = {result}");
386    }
387
388    #[test]
389    fn mul_q16_negative() {
390        let a = to_q16(-2.5);
391        let b = to_q16(4.0);
392        let result = from_q16(mul_q16(a, b));
393        assert!((result - (-10.0)).abs() < 1e-4, "-2.5 * 4.0 = {result}");
394    }
395
396    #[test]
397    fn mul_n_q16_integer_scale() {
398        let a = to_q16(3.5);
399        let result = from_q16(mul_n_q16(a, 4));
400        assert!((result - 14.0).abs() < 1e-4, "3.5 * 4 = {result}");
401    }
402
403    #[test]
404    fn mul_f_q16_float_scale() {
405        let a = to_q16(2.0);
406        let result = from_q16(mul_f_q16(a, 1.5));
407        assert!((result - 3.0).abs() < 1e-3, "2.0 * 1.5f = {result}");
408    }
409
410    #[test]
411    fn div_q16_basic() {
412        let result = from_q16(div_q16(to_q16(3.0), to_q16(2.0)));
413        assert!((result - 1.5).abs() < 1e-4, "3.0 / 2.0 = {result}");
414    }
415
416    #[test]
417    fn div_q16_zero_denominator() {
418        let result = div_q16(to_q16(5.0), 0);
419        assert_eq!(result, 0, "division by zero must return 0");
420    }
421
422    #[test]
423    fn div_n_q16_integer_divisor() {
424        let result = from_q16(div_n_q16(to_q16(9.0), 3));
425        assert!((result - 3.0).abs() < 1e-4, "9.0 / 3 = {result}");
426    }
427
428    #[test]
429    fn div_f_q16_float_divisor() {
430        let result = from_q16(div_f_q16(to_q16(6.0), 2.0));
431        assert!((result - 3.0).abs() < 1e-3, "6.0 / 2.0f = {result}");
432    }
433
434    // ── Saturating arithmetic ─────────────────────────────────────────────────
435
436    #[test]
437    fn qadd_q16_no_overflow() {
438        assert_eq!(
439            from_q16(qadd_q16(to_q16(1.0), to_q16(2.0))).round() as i32,
440            3
441        );
442    }
443
444    #[test]
445    fn qadd_q16_saturates_at_max() {
446        let result = qadd_q16(Q16_MAX, Q16_MAX);
447        assert_eq!(result, Q16_MAX, "overflow must saturate at Q16_MAX");
448    }
449
450    #[test]
451    fn qsub_q16_saturates_at_min() {
452        let result = qsub_q16(Q16_MIN, Q16_MAX);
453        assert_eq!(result, Q16_MIN, "underflow must saturate at Q16_MIN");
454    }
455
456    #[test]
457    fn abs_q16_positive_unchanged() {
458        let v = to_q16(5.75);
459        assert_eq!(abs_q16(v), v);
460    }
461
462    #[test]
463    fn abs_q16_negated() {
464        let v = to_q16(-3.14);
465        let expected = to_q16(3.14);
466        assert_eq!(abs_q16(v), expected);
467    }
468
469    // ── Helpers ───────────────────────────────────────────────────────────────
470
471    #[test]
472    fn lerp_q16_midpoint() {
473        let a = to_q16(0.0);
474        let b = to_q16(1.0);
475        let mid = from_q16(lerp_q16(a, b, 1, 2));
476        assert!((mid - 0.5).abs() < 1e-4, "lerp mid = {mid}");
477    }
478
479    #[test]
480    fn lerp_q16_endpoints() {
481        let a = to_q16(10.0);
482        let b = to_q16(20.0);
483        assert_eq!(lerp_q16(a, b, 0, 10), a, "t=0 must return left endpoint");
484        assert_eq!(
485            lerp_q16(a, b, 10, 10),
486            b,
487            "t=denom must return right endpoint"
488        );
489    }
490
491    #[test]
492    fn lerp_q16_zero_denom_returns_left() {
493        let a = to_q16(5.0);
494        let b = to_q16(9.0);
495        assert_eq!(lerp_q16(a, b, 0, 0), a, "zero denom must return left");
496    }
497
498    #[test]
499    fn angle_to_q16_90_degrees() {
500        let q = angle_to_q16(90.0);
501        let rad = from_q16(q);
502        assert!(
503            (rad - core::f32::consts::FRAC_PI_2).abs() < 1e-4,
504            "90 deg should be pi/2, got {rad}"
505        );
506    }
507
508    #[test]
509    fn angle_to_q16_360_degrees() {
510        let q = angle_to_q16(360.0);
511        let rad = from_q16(q);
512        assert!(
513            (rad - 2.0 * core::f32::consts::PI).abs() < 1e-4,
514            "360 deg should be 2*pi, got {rad}"
515        );
516    }
517
518    #[test]
519    fn recip_q16_one() {
520        let result = from_q16(recip_q16(FP_ONE));
521        assert!(
522            (result - 1.0).abs() < 0.01,
523            "recip(1.0) approx 1.0, got {result}"
524        );
525    }
526
527    #[test]
528    fn recip_q16_two() {
529        let result = from_q16(recip_q16(to_q16(2.0)));
530        assert!(
531            (result - 0.5).abs() < 0.01,
532            "recip(2.0) approx 0.5, got {result}"
533        );
534    }
535
536    #[test]
537    fn recip_q16_zero_returns_sentinel() {
538        assert_eq!(
539            recip_q16(0),
540            Q16_MAX,
541            "recip(0) must return Q16_MAX sentinel"
542        );
543    }
544
545    // ── Legacy shims ──────────────────────────────────────────────────────────
546
547    #[test]
548    fn legacy_shims_match_new_api() {
549        let a = to_q16(3.0);
550        let b = to_q16(4.0);
551        assert_eq!(to_fp(3.0), to_q16(3.0));
552        assert_eq!(from_fp(a), from_q16(a));
553        assert_eq!(mul_fp(a, b), mul_q16(a, b));
554        assert_eq!(div_fp(a, b), Some(div_q16(a, b)));
555        assert_eq!(div_fp(a, 0), None);
556    }
557
558    // ── ScanlineInterp ────────────────────────────────────────────────────────
559
560    #[test]
561    fn scanline_interp_starts_at_left() {
562        let interp = ScanlineInterp::new(100, 200, 0, 65536, 0, 32768, 10);
563        assert_eq!(interp.z(), 100);
564        assert_eq!(interp.u(), 0);
565        assert_eq!(interp.v(), 0);
566    }
567
568    #[test]
569    fn scanline_interp_step_reaches_right() {
570        // z: 0 → 65536 over 10 pixels; step = 65536 / 10 = 6553 (truncated).
571        // After 10 steps: 10 * 6553 = 65530. Truncation error is span % denom = 6.
572        let mut interp = ScanlineInterp::depth_only(0, 65536, 10);
573        for _ in 0..10 {
574            interp.step();
575        }
576        // Max truncation error = step_size_remainder * steps_remaining ≤ span % denom
577        let diff = (interp.z() as i64 - 65536i64).abs();
578        assert!(
579            diff <= 10,
580            "z after 10 steps should be within 10 of 65536, got {} (diff={})",
581            interp.z(),
582            diff
583        );
584    }
585
586    #[test]
587    fn scanline_interp_uv_reaches_right() {
588        let mut interp = ScanlineInterp::new(0, 0, 0, 65536, 0, 65536, 8);
589        for _ in 0..8 {
590            interp.step();
591        }
592        let u_err = (interp.u() as i64 - 65536i64).abs();
593        let v_err = (interp.v() as i64 - 65536i64).abs();
594        assert!(
595            u_err <= 1,
596            "u after 8 steps should be 65536 +/- 1, got {}",
597            interp.u()
598        );
599        assert!(
600            v_err <= 1,
601            "v after 8 steps should be 65536 +/- 1, got {}",
602            interp.v()
603        );
604    }
605
606    #[test]
607    fn scanline_interp_zero_span() {
608        let mut interp = ScanlineInterp::new(1000, 9999, 0, 65536, 0, 65536, 0);
609        interp.step();
610        interp.step();
611        assert_eq!(interp.z(), 1000, "zero-span z must not move");
612        assert_eq!(interp.u(), 0, "zero-span u must not move");
613    }
614
615    #[test]
616    fn scanline_interp_step_n() {
617        let mut interp = ScanlineInterp::depth_only(0, 100000, 10);
618        interp.step_n(5);
619        let expected = 50000u32;
620        let diff = (interp.z() as i64 - expected as i64).abs();
621        assert!(
622            diff <= 1,
623            "step_n(5) should land at 50000 +/- 1, got {}",
624            interp.z()
625        );
626    }
627
628    #[test]
629    fn scanline_interp_z_f32() {
630        let interp = ScanlineInterp::depth_only(65536, 65536, 0);
631        assert!((interp.z_f32() - 1.0).abs() < 1e-5, "z_f32 should be 1.0");
632    }
633
634    // ── Legacy shim round-trip (from original fixed_math tests) ──────────────
635
636    #[test]
637    fn fixed_roundtrip_is_close() {
638        let values = [0.0f32, 0.25, -0.5, 1.0, 3.14159];
639        for v in values {
640            let fp = to_fp(v);
641            let out = from_fp(fp);
642            assert!((out - v).abs() <= 1.0 / FP_ONE as f32);
643        }
644    }
645
646    #[test]
647    fn fixed_mul_div_behaves_as_expected() {
648        let a = to_fp(1.5);
649        let b = to_fp(2.0);
650        let prod = from_fp(mul_fp(a, b));
651        assert!((prod - 3.0).abs() < 0.001);
652
653        let quot = from_fp(div_fp(to_fp(3.0), to_fp(2.0)).unwrap());
654        assert!((quot - 1.5).abs() < 0.001);
655    }
656
657    #[test]
658    fn fixed_algebraic_identities() {
659        let val = to_fp(123.456);
660        let one = to_fp(1.0);
661        let zero = to_fp(0.0);
662        assert_eq!(mul_fp(val, one), val);
663        assert_eq!(mul_fp(val, zero), zero);
664        assert_eq!(div_fp(val, one), Some(val));
665        assert_eq!(div_fp(val, val), Some(one));
666        assert_eq!(div_fp(val, zero), None);
667    }
668
669    #[test]
670    fn fixed_signed_arithmetic_correctness() {
671        let pos = to_fp(4.25);
672        let neg = to_fp(-2.5);
673        let prod1 = from_fp(mul_fp(pos, neg));
674        assert!((prod1 - (-10.625)).abs() < 0.001);
675        let prod2 = from_fp(mul_fp(neg, neg));
676        assert!((prod2 - 6.25).abs() < 0.001);
677        let quot1 = from_fp(div_fp(pos, neg).unwrap());
678        assert!((quot1 - (-1.7)).abs() < 0.001);
679    }
680
681    #[test]
682    fn fixed_quantization_resolution_bound() {
683        let lsb = 1.0 / (FP_ONE as f32);
684        let val = 100.12345;
685        let fp = to_fp(val);
686        let reconstructed = from_fp(fp);
687        assert!((reconstructed - val).abs() <= lsb);
688    }
689}