Skip to main content

embedded_dsp/
fixed_point.rs

1//! Q16.16 fixed-point math.
2//!
3//! A saturating fixed-point number format (`i32`, 16 fractional bits) for
4//! pipelines that can't afford float math (no FPU, or the accuracy/latency
5//! trade-off isn't worth it): rasterizer transforms, angle math, scanline
6//! interpolation. Complements the `q7`/`q15`/`q31`/`q63` aliases in
7//! [`crate::types`], which are narrower fixed-point formats aimed at
8//! CMSIS-DSP-style signal processing rather than screen-space geometry.
9//!
10//! - Type alias [`Q16`], plus [`FP_ONE`], [`Q16_MAX`], [`Q16_MIN`]
11//! - Conversion: `f32 <-> Q16`, `i16 <-> Q16`, `q31 <-> Q16`
12//! - Arithmetic: [`mul_q16`], [`mul_n_q16`], [`mul_f_q16`], [`div_q16`], [`div_n_q16`], [`div_f_q16`]
13//! - Saturating: [`qadd_q16`], [`qsub_q16`], [`abs_q16`]
14//! - Helpers: [`lerp_q16`], [`angle_to_q16`], [`recip_q16`]
15//! - Trig (with the `lut` feature): [`crate::lut::sin_q16`], [`crate::lut::cos_q16`]
16//! - [`ScanlineInterp`] — accelerated per-scanline z + (u, v) interpolation
17
18use crate::types::I16F16;
19
20// ─────────────────────────────────────────────────────────────────────────────
21// Constants
22// ─────────────────────────────────────────────────────────────────────────────
23
24/// Fractional bit count for Q16.16.
25pub const FP_SHIFT: u32 = 16;
26
27/// 1.0 in Q16.16 representation.
28pub const FP_ONE: i32 = 1_i32 << FP_SHIFT;
29
30/// Maximum positive value of a Q16.16 number (same as i32::MAX).
31pub const Q16_MAX: i32 = i32::MAX;
32
33/// Minimum value of a Q16.16 number (same as i32::MIN).
34pub const Q16_MIN: i32 = i32::MIN;
35
36/// Type alias: `Q16` is `i32` stored in Q16.16 fixed-point format.
37///
38/// The integer part occupies bits 31..16, the fractional part bits 15..0.
39pub type Q16 = i32;
40
41// ─────────────────────────────────────────────────────────────────────────────
42// Conversions
43// ─────────────────────────────────────────────────────────────────────────────
44
45/// Convert `f32` -> `Q16.16` with correct rounding (half-to-even at exact ties).
46#[inline(always)]
47pub fn to_q16(v: f32) -> Q16 {
48    I16F16::saturating_from_num(v).to_bits()
49}
50
51/// Convert `Q16.16` -> `f32`.
52#[inline(always)]
53pub fn from_q16(v: Q16) -> f32 {
54    I16F16::from_bits(v).to_num::<f32>()
55}
56
57/// Convert `i16` integer -> `Q16.16` (shift left 16).
58#[inline(always)]
59pub fn from_i16_q16(v: i16) -> Q16 {
60    I16F16::from_num(v).to_bits()
61}
62
63/// Convert `Q16.16` -> `i16` integer (truncate fractional bits).
64#[inline(always)]
65pub fn to_i16_q16(v: Q16) -> i16 {
66    I16F16::from_bits(v).to_num::<i16>()
67}
68
69/// Reinterpret a Q31 value as Q16.16 by shifting right 15 bits.
70#[inline(always)]
71pub fn q31_to_q16(v: i32) -> Q16 {
72    I16F16::from_bits(v >> 15).to_bits()
73}
74
75/// Reinterpret Q16.16 as Q31 by shifting left 16 bits (use i64 to avoid overflow).
76#[inline(always)]
77pub fn q16_to_q31(v: Q16) -> i64 {
78    (I16F16::from_bits(v).to_bits() as i64) << 16
79}
80
81// ─────────────────────────────────────────────────────────────────────────────
82// Arithmetic
83// ─────────────────────────────────────────────────────────────────────────────
84
85/// Multiply two Q16.16 values. Uses i64 intermediate (SMULL on Cortex-M).
86#[inline(always)]
87pub fn mul_q16(a: Q16, b: Q16) -> Q16 {
88    I16F16::from_bits(a).wrapping_mul(I16F16::from_bits(b)).to_bits()
89}
90
91/// Multiply a Q16.16 value by a plain `i32` integer (no fractional scaling).
92#[inline(always)]
93pub fn mul_n_q16(a: Q16, n: i32) -> Q16 {
94    I16F16::from_bits(a).wrapping_mul_int(n).to_bits()
95}
96
97/// Multiply a Q16.16 value by an `f32` scalar.
98#[inline(always)]
99pub fn mul_f_q16(a: Q16, f: f32) -> Q16 {
100    mul_q16(a, to_q16(f))
101}
102
103/// Divide two Q16.16 values. Returns 0 on division by zero.
104#[inline(always)]
105pub fn div_q16(a: Q16, b: Q16) -> Q16 {
106    if b == 0 {
107        return 0;
108    }
109    I16F16::from_bits(a).wrapping_div(I16F16::from_bits(b)).to_bits()
110}
111
112/// Divide a Q16.16 value by a plain `i32` integer. Returns 0 on division by zero.
113#[inline(always)]
114pub fn div_n_q16(a: Q16, n: i32) -> Q16 {
115    if n == 0 {
116        return 0;
117    }
118    I16F16::from_bits(a).wrapping_div_int(n).to_bits()
119}
120
121/// Divide a Q16.16 value by an `f32` scalar.
122#[inline(always)]
123pub fn div_f_q16(a: Q16, f: f32) -> Q16 {
124    div_q16(a, to_q16(f))
125}
126
127// ─────────────────────────────────────────────────────────────────────────────
128// Saturating arithmetic
129// ─────────────────────────────────────────────────────────────────────────────
130
131/// Saturating add: clamps the result to `[i32::MIN, i32::MAX]`.
132#[inline(always)]
133pub const fn qadd_q16(a: Q16, b: Q16) -> Q16 {
134    I16F16::from_bits(a).saturating_add(I16F16::from_bits(b)).to_bits()
135}
136
137/// Saturating subtract: clamps the result to `[i32::MIN, i32::MAX]`.
138#[inline(always)]
139pub const fn qsub_q16(a: Q16, b: Q16) -> Q16 {
140    I16F16::from_bits(a).saturating_sub(I16F16::from_bits(b)).to_bits()
141}
142
143/// Absolute value of a Q16.16 number.
144#[inline(always)]
145pub fn abs_q16(a: Q16) -> Q16 {
146    I16F16::from_bits(a).abs().to_bits()
147}
148
149// ─────────────────────────────────────────────────────────────────────────────
150// Helpers
151// ─────────────────────────────────────────────────────────────────────────────
152
153/// Linear interpolation in Q16.16:  `a + (b - a) * t / denom`.
154///
155/// `t` and `denom` are plain integers (scan-line step counts).
156/// Uses i64 to avoid overflow on large `(b - a)` spans.
157#[inline(always)]
158pub fn lerp_q16(a: Q16, b: Q16, t: i32, denom: i32) -> Q16 {
159    if denom == 0 {
160        return a;
161    }
162    let diff = b as i64 - a as i64;
163    (a as i64 + diff * t as i64 / denom as i64) as i32
164}
165
166/// Convert an angle in degrees to Q16.16 radians.
167#[inline(always)]
168pub fn angle_to_q16(degrees: f32) -> Q16 {
169    to_q16(degrees * core::f32::consts::PI / 180.0)
170}
171
172/// Fast reciprocal approximation: `1.0 / v` in Q16.16.
173///
174/// Uses integer shift: `(1 << 32) / v` — gives approx 6 correct decimal digits.
175/// Returns `Q16_MAX` for zero or near-zero input (safe sentinel).
176#[inline(always)]
177pub fn recip_q16(v: Q16) -> Q16 {
178    if v == 0 {
179        return Q16_MAX;
180    }
181    I16F16::from_bits(v).recip().to_bits()
182}
183
184// ─────────────────────────────────────────────────────────────────────────────
185// ScanlineInterp — accelerated z-buffer + UV interpolation
186// ─────────────────────────────────────────────────────────────────────────────
187
188/// Per-scanline interpolator for z-buffer depth and (u, v) texture coordinates.
189///
190/// Pre-computes Q16.16 per-pixel step values for `z`, `u`, and `v` so the
191/// inner loop only does three wrapping additions instead of floating-point
192/// divisions per pixel — useful for MCU scanline rasterization.
193///
194/// # Usage
195/// ```rust,no_run
196/// # use embedded_dsp::fixed_point::ScanlineInterp;
197/// # let left_z = 0u32;
198/// # let right_z = 0u32;
199/// # let left_u = 0u32;
200/// # let right_u = 0u32;
201/// # let left_v = 0u32;
202/// # let right_v = 0u32;
203/// # let span_pixels = 0i32;
204/// let mut interp = ScanlineInterp::new(
205///     left_z,  right_z,   // u32 depth values (Q16.16)
206///     left_u,  right_u,   // u32 U texture coords (Q16.16)
207///     left_v,  right_v,   // u32 V texture coords (Q16.16)
208///     span_pixels,         // number of pixels across the scanline
209/// );
210///
211/// for _x in 0..=span_pixels {
212///     let z = interp.z();
213///     let u = interp.u();
214///     let v = interp.v();
215///     // ... depth test, texture sample, write pixel ...
216///     interp.step();
217/// }
218/// ```
219#[derive(Debug, Clone, Copy)]
220pub struct ScanlineInterp {
221    z_cur: u32,
222    z_step: i32,
223    u_cur: u32,
224    u_step: i32,
225    v_cur: u32,
226    v_step: i32,
227}
228
229impl ScanlineInterp {
230    /// Create a new scanline interpolator.
231    ///
232    /// # Arguments
233    /// - `z_left`, `z_right` — depth at the left and right scanline endpoints (Q16.16 `u32`)
234    /// - `u_left`, `u_right` — U texture coordinates (Q16.16 `u32`, range `[0, 65536]`)
235    /// - `v_left`, `v_right` — V texture coordinates (Q16.16 `u32`, range `[0, 65536]`)
236    /// - `span` — number of pixels across the scanline (0 is valid — returns left values)
237    #[inline]
238    pub fn new(
239        z_left: u32,
240        z_right: u32,
241        u_left: u32,
242        u_right: u32,
243        v_left: u32,
244        v_right: u32,
245        span: i32,
246    ) -> Self {
247        let (z_step, u_step, v_step) = if span > 0 {
248            let z_step = ((z_right as i64 - z_left as i64) / span as i64) as i32;
249            let u_step = ((u_right as i64 - u_left as i64) / span as i64) as i32;
250            let v_step = ((v_right as i64 - v_left as i64) / span as i64) as i32;
251            (z_step, u_step, v_step)
252        } else {
253            (0, 0, 0)
254        };
255        Self {
256            z_cur: z_left,
257            z_step,
258            u_cur: u_left,
259            u_step,
260            v_cur: v_left,
261            v_step,
262        }
263    }
264
265    /// Create an interpolator for depth-only scanlines (no texture mapping).
266    #[inline]
267    pub fn depth_only(z_left: u32, z_right: u32, span: i32) -> Self {
268        Self::new(z_left, z_right, 0, 0, 0, 0, span)
269    }
270
271    /// Current depth value (Q16.16 `u32`).
272    #[inline(always)]
273    pub fn z(&self) -> u32 {
274        self.z_cur
275    }
276
277    /// Current U texture coordinate (Q16.16 `u32`).
278    #[inline(always)]
279    pub fn u(&self) -> u32 {
280        self.u_cur
281    }
282
283    /// Current V texture coordinate (Q16.16 `u32`).
284    #[inline(always)]
285    pub fn v(&self) -> u32 {
286        self.v_cur
287    }
288
289    /// Advance all interpolators by one pixel.
290    #[inline(always)]
291    pub fn step(&mut self) {
292        self.z_cur = self.z_cur.wrapping_add_signed(self.z_step);
293        self.u_cur = self.u_cur.wrapping_add_signed(self.u_step);
294        self.v_cur = self.v_cur.wrapping_add_signed(self.v_step);
295    }
296
297    /// Advance `n` pixels at once (useful for skipping clipped scanline segments).
298    #[inline]
299    pub fn step_n(&mut self, n: i32) {
300        self.z_cur = self.z_cur.wrapping_add_signed(self.z_step.wrapping_mul(n));
301        self.u_cur = self.u_cur.wrapping_add_signed(self.u_step.wrapping_mul(n));
302        self.v_cur = self.v_cur.wrapping_add_signed(self.v_step.wrapping_mul(n));
303    }
304
305    /// Current depth as `f32`.
306    #[inline(always)]
307    pub fn z_f32(&self) -> f32 {
308        self.z_cur as f32 / 65536.0
309    }
310}
311
312// ─────────────────────────────────────────────────────────────────────────────
313// Tests
314// ─────────────────────────────────────────────────────────────────────────────
315
316#[cfg(test)]
317mod tests {
318    extern crate std;
319    use super::*;
320
321    #[test]
322    fn to_q16_tie_rounding() {
323        // 2.5/65536 is an exact tie: `fixed`'s `saturating_from_num` rounds
324        // half-to-even (2.5 -> 2), unlike the old hand-rolled
325        // half-away-from-zero rounding (2.5 -> 3). This is a confirmed,
326        // accepted behavior change that only manifests at exact ties.
327        let v: f32 = 2.5 / 65536.0;
328        assert_eq!(to_q16(v), 2, "tie value rounds half-to-even under `fixed`");
329    }
330
331    #[test]
332    fn to_i16_q16_negative_fraction_floors() {
333        // -3.5 in Q16.16 must floor to -4 via arithmetic shift, not truncate
334        // toward zero (which would give -3).
335        let q = to_q16(-3.5);
336        assert_eq!(to_i16_q16(q), -4, "negative fraction must floor, not truncate");
337    }
338
339    #[test]
340    fn div_q16_min_by_small_denominator_wraps() {
341        // Q16_MIN << 16 already exceeds i32 range before the division even
342        // happens; the final `as i32` cast wraps (truncates to low 32 bits)
343        // rather than panicking.
344        assert_eq!(div_q16(Q16_MIN, 1), 0, "overflow must wrap, not panic");
345    }
346
347    #[test]
348    fn roundtrip_f32_q16() {
349        let values = [0.0f32, 0.25, -0.5, 1.0, core::f32::consts::PI, -100.0, 32767.0];
350        for v in values {
351            let q = to_q16(v);
352            let back = from_q16(q);
353            let lsb = 1.0 / 65536.0_f32;
354            assert!(
355                (back - v).abs() <= lsb,
356                "roundtrip failed for {v}: got {back}"
357            );
358        }
359    }
360
361    #[test]
362    fn roundtrip_i16_q16() {
363        for v in [-32768i16, -1, 0, 1, 100, 32767] {
364            let q = from_i16_q16(v);
365            let back = to_i16_q16(q);
366            assert_eq!(back, v, "i16 roundtrip failed for {v}");
367        }
368    }
369
370    #[test]
371    fn q31_q16_shifts() {
372        let q31_one = 0x7FFF_FFFFi32;
373        let q16 = q31_to_q16(q31_one);
374        let f = from_q16(q16);
375        assert!(
376            (f - 1.0).abs() < 1e-4,
377            "Q31->Q16 should be near 1.0, got {f}"
378        );
379    }
380
381    #[test]
382    fn mul_q16_basic() {
383        let a = to_q16(1.5);
384        let b = to_q16(2.0);
385        let result = from_q16(mul_q16(a, b));
386        assert!((result - 3.0).abs() < 1e-4, "1.5 * 2.0 = {result}");
387    }
388
389    #[test]
390    fn mul_q16_negative() {
391        let a = to_q16(-2.5);
392        let b = to_q16(4.0);
393        let result = from_q16(mul_q16(a, b));
394        assert!((result - (-10.0)).abs() < 1e-4, "-2.5 * 4.0 = {result}");
395    }
396
397    #[test]
398    fn mul_n_q16_integer_scale() {
399        let a = to_q16(3.5);
400        let result = from_q16(mul_n_q16(a, 4));
401        assert!((result - 14.0).abs() < 1e-4, "3.5 * 4 = {result}");
402    }
403
404    #[test]
405    fn mul_f_q16_float_scale() {
406        let a = to_q16(2.0);
407        let result = from_q16(mul_f_q16(a, 1.5));
408        assert!((result - 3.0).abs() < 1e-3, "2.0 * 1.5f = {result}");
409    }
410
411    #[test]
412    fn div_q16_basic() {
413        let result = from_q16(div_q16(to_q16(3.0), to_q16(2.0)));
414        assert!((result - 1.5).abs() < 1e-4, "3.0 / 2.0 = {result}");
415    }
416
417    #[test]
418    fn div_q16_zero_denominator() {
419        let result = div_q16(to_q16(5.0), 0);
420        assert_eq!(result, 0, "division by zero must return 0");
421    }
422
423    #[test]
424    fn div_n_q16_integer_divisor() {
425        let result = from_q16(div_n_q16(to_q16(9.0), 3));
426        assert!((result - 3.0).abs() < 1e-4, "9.0 / 3 = {result}");
427    }
428
429    #[test]
430    fn div_f_q16_float_divisor() {
431        let result = from_q16(div_f_q16(to_q16(6.0), 2.0));
432        assert!((result - 3.0).abs() < 1e-3, "6.0 / 2.0f = {result}");
433    }
434
435    #[test]
436    fn qadd_q16_no_overflow() {
437        assert_eq!(
438            from_q16(qadd_q16(to_q16(1.0), to_q16(2.0))).round() as i32,
439            3
440        );
441    }
442
443    #[test]
444    fn qadd_q16_saturates_at_max() {
445        let result = qadd_q16(Q16_MAX, Q16_MAX);
446        assert_eq!(result, Q16_MAX, "overflow must saturate at Q16_MAX");
447    }
448
449    #[test]
450    fn qsub_q16_saturates_at_min() {
451        let result = qsub_q16(Q16_MIN, Q16_MAX);
452        assert_eq!(result, Q16_MIN, "underflow must saturate at Q16_MIN");
453    }
454
455    #[test]
456    fn abs_q16_positive_unchanged() {
457        let v = to_q16(5.75);
458        assert_eq!(abs_q16(v), v);
459    }
460
461    #[test]
462    fn abs_q16_negated() {
463        let v = to_q16(-core::f32::consts::PI);
464        let expected = to_q16(core::f32::consts::PI);
465        assert_eq!(abs_q16(v), expected);
466    }
467
468    #[test]
469    fn lerp_q16_midpoint() {
470        let a = to_q16(0.0);
471        let b = to_q16(1.0);
472        let mid = from_q16(lerp_q16(a, b, 1, 2));
473        assert!((mid - 0.5).abs() < 1e-4, "lerp mid = {mid}");
474    }
475
476    #[test]
477    fn lerp_q16_endpoints() {
478        let a = to_q16(10.0);
479        let b = to_q16(20.0);
480        assert_eq!(lerp_q16(a, b, 0, 10), a, "t=0 must return left endpoint");
481        assert_eq!(
482            lerp_q16(a, b, 10, 10),
483            b,
484            "t=denom must return right endpoint"
485        );
486    }
487
488    #[test]
489    fn lerp_q16_zero_denom_returns_left() {
490        let a = to_q16(5.0);
491        let b = to_q16(9.0);
492        assert_eq!(lerp_q16(a, b, 0, 0), a, "zero denom must return left");
493    }
494
495    #[test]
496    fn angle_to_q16_90_degrees() {
497        let q = angle_to_q16(90.0);
498        let rad = from_q16(q);
499        assert!(
500            (rad - core::f32::consts::FRAC_PI_2).abs() < 1e-4,
501            "90 deg should be pi/2, got {rad}"
502        );
503    }
504
505    #[test]
506    fn angle_to_q16_360_degrees() {
507        let q = angle_to_q16(360.0);
508        let rad = from_q16(q);
509        assert!(
510            (rad - 2.0 * core::f32::consts::PI).abs() < 1e-4,
511            "360 deg should be 2*pi, got {rad}"
512        );
513    }
514
515    #[test]
516    fn recip_q16_one() {
517        let result = from_q16(recip_q16(FP_ONE));
518        assert!(
519            (result - 1.0).abs() < 0.01,
520            "recip(1.0) approx 1.0, got {result}"
521        );
522    }
523
524    #[test]
525    fn recip_q16_two() {
526        let result = from_q16(recip_q16(to_q16(2.0)));
527        assert!(
528            (result - 0.5).abs() < 0.01,
529            "recip(2.0) approx 0.5, got {result}"
530        );
531    }
532
533    #[test]
534    fn recip_q16_zero_returns_sentinel() {
535        assert_eq!(
536            recip_q16(0),
537            Q16_MAX,
538            "recip(0) must return Q16_MAX sentinel"
539        );
540    }
541
542    #[test]
543    fn scanline_interp_starts_at_left() {
544        let interp = ScanlineInterp::new(100, 200, 0, 65536, 0, 32768, 10);
545        assert_eq!(interp.z(), 100);
546        assert_eq!(interp.u(), 0);
547        assert_eq!(interp.v(), 0);
548    }
549
550    #[test]
551    fn scanline_interp_step_reaches_right() {
552        let mut interp = ScanlineInterp::depth_only(0, 65536, 10);
553        for _ in 0..10 {
554            interp.step();
555        }
556        let diff = (interp.z() as i64 - 65536i64).abs();
557        assert!(
558            diff <= 10,
559            "z after 10 steps should be within 10 of 65536, got {} (diff={})",
560            interp.z(),
561            diff
562        );
563    }
564
565    #[test]
566    fn scanline_interp_uv_reaches_right() {
567        let mut interp = ScanlineInterp::new(0, 0, 0, 65536, 0, 65536, 8);
568        for _ in 0..8 {
569            interp.step();
570        }
571        let u_err = (interp.u() as i64 - 65536i64).abs();
572        let v_err = (interp.v() as i64 - 65536i64).abs();
573        assert!(
574            u_err <= 1,
575            "u after 8 steps should be 65536 +/- 1, got {}",
576            interp.u()
577        );
578        assert!(
579            v_err <= 1,
580            "v after 8 steps should be 65536 +/- 1, got {}",
581            interp.v()
582        );
583    }
584
585    #[test]
586    fn scanline_interp_zero_span() {
587        let mut interp = ScanlineInterp::new(1000, 9999, 0, 65536, 0, 65536, 0);
588        interp.step();
589        interp.step();
590        assert_eq!(interp.z(), 1000, "zero-span z must not move");
591        assert_eq!(interp.u(), 0, "zero-span u must not move");
592    }
593
594    #[test]
595    fn scanline_interp_step_n() {
596        let mut interp = ScanlineInterp::depth_only(0, 100000, 10);
597        interp.step_n(5);
598        let expected = 50000u32;
599        let diff = (interp.z() as i64 - expected as i64).abs();
600        assert!(
601            diff <= 1,
602            "step_n(5) should land at 50000 +/- 1, got {}",
603            interp.z()
604        );
605    }
606
607    #[test]
608    fn scanline_interp_z_f32() {
609        let interp = ScanlineInterp::depth_only(65536, 65536, 0);
610        assert!((interp.z_f32() - 1.0).abs() < 1e-5, "z_f32 should be 1.0");
611    }
612}