Skip to main content

affine/
transform.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::ratio::{Exact, Ratio, Round};
6use zerocopy::{FromBytes, Immutable, IntoBytes};
7use zr::static_assert;
8
9pub struct Saturate;
10#[allow(non_upper_case_globals)]
11impl Saturate {
12    pub const No: bool = false;
13    pub const Yes: bool = true;
14}
15
16/// A small helper struct which represents a 1 dimensional affine transformation
17/// from a signed 64 bit space A, to a signed 64 bit space B.  Conceptually, this
18/// is the function...
19///
20/// f(a) = b = (a * scale) + offset
21///
22/// Internally, however, the exact function used is
23///
24/// f(a) = b = (((a - A_offset) * B_scale) / A_scale) + B_offset
25///
26/// Where the offsets involved are 64 bit signed integers, and the scale factors
27/// are 32 bit unsigned integers.
28///
29/// Overflow/Underflow saturation behavior is as follows.
30/// The transformation operation is divided into three stages.
31///
32/// 1) Offset by A_offset
33/// 2) Scale by (B_scale / A_scale)
34/// 3) Offset by B_offset
35///
36/// Each stage is saturated independently.  That is to say, if the result of
37/// stage #1 is clamped at int64::min, this is the input value which will be fed
38/// into stage #2.  The calculations are *not* done with infinite precision and
39/// then clamped at the end.
40///
41/// TODO(johngro): Reconsider this.  Clamping at intermediate stages can make it
42/// more difficult to understand that saturation happened at all, and might be
43/// important to a client.  It may be better to either signal explicitly that
44/// this happened, or to extend the precision of the operation in the rare slow
45/// path so that saturation behavior occurs only at the end of the op, and
46/// produces a correct result if the transform would have saturated at an
47/// intermediate step, but got brought back into range by a subsequent operation.
48///
49/// Saturation is enabled by default, but may be disabled by choosing the
50/// Saturate::No form of Apply/ApplyInverse.  When saturation behavior is
51/// disabled, the results of a transformation where over/underflow occurs at any
52/// stage is undefined.
53#[repr(C)]
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, FromBytes, IntoBytes, Immutable)]
55pub struct Transform {
56    a_offset: i64,
57    b_offset: i64,
58    ratio: Ratio,
59}
60
61static_assert!(core::mem::size_of::<Transform>() == 24);
62static_assert!(core::mem::align_of::<Transform>() == 8);
63
64// Transform::default() produces the identity transform
65
66// TODO(https://fxbug.dev/42082948)
67impl Transform {
68    /// Returns the identity Transform (`f(a) = a`).
69    pub const fn const_default() -> Self {
70        Transform { a_offset: 0, b_offset: 0, ratio: Ratio::const_default() }
71    }
72
73    /// Constructs a new Transform.
74    pub const fn new(a_offset: i64, b_offset: i64, ratio: Ratio) -> Self {
75        Transform { a_offset, b_offset, ratio }
76    }
77
78    // Construct a linear transformation (zero offsets) from a ratio
79    pub const fn new_linear(ratio: Ratio) -> Self {
80        Transform { a_offset: 0, b_offset: 0, ratio }
81    }
82
83    pub const fn invertible(&self) -> bool {
84        self.ratio.invertible()
85    }
86
87    pub const fn a_offset(&self) -> i64 {
88        self.a_offset
89    }
90
91    pub const fn b_offset(&self) -> i64 {
92        self.b_offset
93    }
94
95    pub const fn ratio(&self) -> Ratio {
96        self.ratio
97    }
98
99    pub const fn numerator(&self) -> u32 {
100        self.ratio.numerator()
101    }
102
103    pub const fn denominator(&self) -> u32 {
104        self.ratio.denominator()
105    }
106
107    // Construct and return a transform which is the inverse of this transform.
108    pub const fn inverse(&self) -> Self {
109        Transform { a_offset: self.b_offset, b_offset: self.a_offset, ratio: self.ratio.inverse() }
110    }
111
112    // Applies a transformation from A -> B
113    pub fn apply_static<const SATURATE: bool>(
114        a_offset: i64,
115        b_offset: i64,
116        ratio: Ratio,
117        val: i64,
118    ) -> i64 {
119        if SATURATE {
120            let sub = val.saturating_sub(a_offset);
121            let scaled = ratio.scale::<{ Round::DOWN }>(sub);
122            scaled.saturating_add(b_offset)
123        } else {
124            // TODO(johngro): the multiplication by the ratio operation here
125            // actually implements saturation behavior.  If we want this
126            // operation to actually perform no saturation checks at all, we
127            // need to make a Saturate::No version of Ratio::Scale.
128            let sub = val.wrapping_sub(a_offset);
129            let scaled = ratio.scale::<{ Round::DOWN }>(sub);
130            scaled.wrapping_add(b_offset)
131        }
132    }
133
134    // Applies the inverse transformation B -> A
135    pub fn apply_inverse_static<const SATURATE: bool>(
136        a_offset: i64,
137        b_offset: i64,
138        ratio: Ratio,
139        val: i64,
140    ) -> i64 {
141        Self::apply_static::<SATURATE>(b_offset, a_offset, ratio.inverse(), val)
142    }
143
144    // Applies the transformation
145    pub fn apply<const SATURATE: bool>(&self, val: i64) -> i64 {
146        Self::apply_static::<SATURATE>(self.a_offset, self.b_offset, self.ratio, val)
147    }
148
149    // Applies the inverse transformation
150    pub fn apply_inverse<const SATURATE: bool>(&self, val: i64) -> i64 {
151        debug_assert!(self.ratio.denominator() != 0);
152        Self::apply_inverse_static::<SATURATE>(self.a_offset, self.b_offset, self.ratio, val)
153    }
154
155    // Composes two timeline functions B->C and A->B producing A->C. If exact is
156    // Exact::Yes, asserts on loss of precision.
157    //
158    // During composition, the saturation behavior is as follows
159    //
160    // 1) The intermediate offset (bc.a_offset - ab.b_offset) will be saturated
161    //    before distribution to the offsets ac.
162    // 2) Both offsets of ac will be saturated as ab.a_offset and bc.b_offset
163    //    are combined with the distributed intermediate offset.
164    pub fn compose(bc: &Transform, ab: &Transform, exact: Exact) -> Transform {
165        Transform {
166            a_offset: ab.a_offset,
167            b_offset: bc.apply::<{ Saturate::Yes }>(ab.b_offset),
168            ratio: Ratio::product(ab.ratio, bc.ratio, exact),
169        }
170    }
171}
172
173// Operators
174
175/// Composes two timeline functions B->C and A->B producing A->C.
176///
177/// Panics on loss of precision.
178impl core::ops::Mul for Transform {
179    type Output = Self;
180    fn mul(self, rhs: Self) -> Self::Output {
181        Transform::compose(&self, &rhs, Exact::Yes)
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn test_construction() {
191        let t = Transform::default();
192        assert_eq!(t.a_offset(), 0);
193        assert_eq!(t.b_offset(), 0);
194        assert_eq!(t.numerator(), 1);
195        assert_eq!(t.denominator(), 1);
196
197        struct TestVector {
198            a_offset: i64,
199            b_offset: i64,
200            n: u32,
201            d: u32,
202        }
203
204        let valid_vectors = [
205            TestVector { a_offset: 12345, b_offset: 98764, n: 3, d: 2 },
206            TestVector { a_offset: -12345, b_offset: 98764, n: 247, d: 931 },
207            TestVector { a_offset: -12345, b_offset: -98764, n: 48000, d: 44100 },
208            TestVector { a_offset: 12345, b_offset: -98764, n: 1000007, d: 1000000 },
209            TestVector { a_offset: 12345, b_offset: 98764, n: 0, d: 1000000 },
210        ];
211
212        for v in &valid_vectors {
213            let ratio = Ratio::new(v.n, v.d);
214
215            let t_linear = Transform::new_linear(ratio);
216            assert_eq!(t_linear.a_offset(), 0);
217            assert_eq!(t_linear.b_offset(), 0);
218            assert_eq!(t_linear.numerator(), ratio.numerator());
219            assert_eq!(t_linear.denominator(), ratio.denominator());
220
221            let t_affine = Transform::new(v.a_offset, v.b_offset, ratio);
222            assert_eq!(t_affine.a_offset(), v.a_offset);
223            assert_eq!(t_affine.b_offset(), v.b_offset);
224            assert_eq!(t_affine.numerator(), ratio.numerator());
225            assert_eq!(t_affine.denominator(), ratio.denominator());
226        }
227    }
228
229    #[test]
230    fn test_inverse() {
231        struct TestVector {
232            a_offset: i64,
233            b_offset: i64,
234            n: u32,
235            d: u32,
236        }
237
238        let test_vectors = [
239            TestVector { a_offset: 12345, b_offset: 98764, n: 3, d: 2 },
240            TestVector { a_offset: -12345, b_offset: 98764, n: 247, d: 931 },
241            TestVector { a_offset: -12345, b_offset: -98764, n: 48000, d: 44100 },
242            TestVector { a_offset: 12345, b_offset: -98764, n: 1000007, d: 1000000 },
243        ];
244
245        for v in &test_vectors {
246            let ratio = Ratio::new(v.n, v.d);
247            let t = Transform::new(v.a_offset, v.b_offset, ratio);
248
249            if t.invertible() {
250                let res = t.inverse();
251                assert_eq!(t.a_offset(), res.b_offset());
252                assert_eq!(t.b_offset(), res.a_offset());
253                assert_eq!(t.numerator(), res.denominator());
254                assert_eq!(t.denominator(), res.numerator());
255                assert_eq!(t.ratio().inverse().numerator(), res.ratio().numerator());
256                assert_eq!(t.ratio().inverse().denominator(), res.ratio().denominator());
257            }
258        }
259
260        let t_non_inv = Transform::new(12345, 98764, Ratio::new(0, 1000000));
261        assert!(!t_non_inv.invertible());
262    }
263
264    #[test]
265    fn test_apply() {
266        struct TestVector {
267            a_offset: i64,
268            b_offset: i64,
269            n: u32,
270            d: u32,
271            val: i64,
272            expected: i64,
273            expect_ovfl: bool,
274        }
275
276        let test_vectors = [
277            TestVector {
278                a_offset: 0,
279                b_offset: 0,
280                n: 1,
281                d: 1,
282                val: 12345,
283                expected: 12345,
284                expect_ovfl: false,
285            },
286            TestVector {
287                a_offset: 50,
288                b_offset: 0,
289                n: 1,
290                d: 1,
291                val: 12345,
292                expected: 12295,
293                expect_ovfl: false,
294            },
295            TestVector {
296                a_offset: 0,
297                b_offset: -50,
298                n: 1,
299                d: 1,
300                val: 12345,
301                expected: 12295,
302                expect_ovfl: false,
303            },
304            TestVector {
305                a_offset: 50,
306                b_offset: -50,
307                n: 1,
308                d: 1,
309                val: 12345,
310                expected: 12245,
311                expect_ovfl: false,
312            },
313            TestVector {
314                a_offset: 50,
315                b_offset: 50,
316                n: 1,
317                d: 1,
318                val: 12345,
319                expected: 12345,
320                expect_ovfl: false,
321            },
322            TestVector {
323                a_offset: 0,
324                b_offset: 0,
325                n: 48000,
326                d: 44100,
327                val: 12345,
328                expected: 13436,
329                expect_ovfl: false,
330            },
331            TestVector {
332                a_offset: 50,
333                b_offset: 0,
334                n: 48000,
335                d: 44100,
336                val: 12345,
337                expected: 13382,
338                expect_ovfl: false,
339            },
340            TestVector {
341                a_offset: 0,
342                b_offset: -54,
343                n: 48000,
344                d: 44100,
345                val: 12345,
346                expected: 13382,
347                expect_ovfl: false,
348            },
349            TestVector {
350                a_offset: 50,
351                b_offset: -54,
352                n: 48000,
353                d: 44100,
354                val: 12345,
355                expected: 13328,
356                expect_ovfl: false,
357            },
358            TestVector {
359                a_offset: 50,
360                b_offset: 54,
361                n: 48000,
362                d: 44100,
363                val: 12345,
364                expected: 13436,
365                expect_ovfl: false,
366            },
367            TestVector {
368                a_offset: -100,
369                b_offset: -17,
370                n: 1,
371                d: 1,
372                val: i64::MAX - 1,
373                expected: i64::MAX - 17,
374                expect_ovfl: true,
375            },
376            TestVector {
377                a_offset: 100,
378                b_offset: 17,
379                n: 1,
380                d: 1,
381                val: i64::MIN + 1,
382                expected: i64::MIN + 17,
383                expect_ovfl: true,
384            },
385            TestVector {
386                a_offset: 0,
387                b_offset: -17,
388                n: 3,
389                d: 1,
390                val: i64::MAX / 2,
391                expected: i64::MAX - 17,
392                expect_ovfl: true,
393            },
394            TestVector {
395                a_offset: 0,
396                b_offset: 17,
397                n: 3,
398                d: 1,
399                val: i64::MIN / 2,
400                expected: i64::MIN + 17,
401                expect_ovfl: true,
402            },
403            TestVector {
404                a_offset: 0,
405                b_offset: 17,
406                n: 1,
407                d: 1,
408                val: i64::MAX - 10,
409                expected: i64::MAX,
410                expect_ovfl: true,
411            },
412            TestVector {
413                a_offset: 0,
414                b_offset: -17,
415                n: 1,
416                d: 1,
417                val: i64::MIN + 10,
418                expected: i64::MIN,
419                expect_ovfl: true,
420            },
421        ];
422
423        for v in &test_vectors {
424            let t = Transform::new(v.a_offset, v.b_offset, Ratio::new(v.n, v.d));
425
426            let res_sat_static = Transform::apply_static::<{ Saturate::Yes }>(
427                t.a_offset(),
428                t.b_offset(),
429                t.ratio(),
430                v.val,
431            );
432            assert_eq!(res_sat_static, v.expected);
433
434            if !v.expect_ovfl {
435                let res_nosat_static = Transform::apply_static::<{ Saturate::No }>(
436                    t.a_offset(),
437                    t.b_offset(),
438                    t.ratio(),
439                    v.val,
440                );
441                assert_eq!(res_nosat_static, v.expected);
442            }
443
444            let res_sat_obj = t.apply::<{ Saturate::Yes }>(v.val);
445            assert_eq!(res_sat_obj, v.expected);
446
447            if !v.expect_ovfl {
448                let res_nosat_obj = t.apply::<{ Saturate::No }>(v.val);
449                assert_eq!(res_nosat_obj, v.expected);
450            }
451
452            if t.invertible() {
453                let t_inv = t.inverse();
454
455                let res_sat_inv_static = Transform::apply_inverse_static::<{ Saturate::Yes }>(
456                    t_inv.a_offset(),
457                    t_inv.b_offset(),
458                    t_inv.ratio(),
459                    v.val,
460                );
461                assert_eq!(res_sat_inv_static, v.expected);
462
463                if !v.expect_ovfl {
464                    let res_nosat_inv_static = Transform::apply_inverse_static::<{ Saturate::No }>(
465                        t_inv.a_offset(),
466                        t_inv.b_offset(),
467                        t_inv.ratio(),
468                        v.val,
469                    );
470                    assert_eq!(res_nosat_inv_static, v.expected);
471                }
472
473                let res_sat_inv_obj = t_inv.apply_inverse::<{ Saturate::Yes }>(v.val);
474                assert_eq!(res_sat_inv_obj, v.expected);
475
476                if !v.expect_ovfl {
477                    let res_nosat_inv_obj = t_inv.apply_inverse::<{ Saturate::No }>(v.val);
478                    assert_eq!(res_nosat_inv_obj, v.expected);
479                }
480            }
481        }
482    }
483
484    #[test]
485    fn test_compose() {
486        struct TestVector {
487            ab: Transform,
488            bc: Transform,
489            ac: Transform,
490            is_exact: Exact,
491        }
492
493        let test_vectors = [
494            TestVector {
495                ab: Transform::new(0, 0, Ratio::new(1, 1)),
496                bc: Transform::new(0, 0, Ratio::new(1, 1)),
497                ac: Transform::new(0, 0, Ratio::new(1, 1)),
498                is_exact: Exact::Yes,
499            },
500            TestVector {
501                ab: Transform::new(0, 0, Ratio::new(1, 1)),
502                bc: Transform::new(12345, 98765, Ratio::new(17, 7)),
503                ac: Transform::new(0, 68784, Ratio::new(17, 7)),
504                is_exact: Exact::Yes,
505            },
506            TestVector {
507                ab: Transform::new(12345, 98765, Ratio::new(17, 7)),
508                bc: Transform::new(0, 0, Ratio::new(1, 1)),
509                ac: Transform::new(12345, 98765, Ratio::new(17, 7)),
510                is_exact: Exact::Yes,
511            },
512            TestVector {
513                ab: Transform::new(34327, 86539, Ratio::new(1000007, 1000000)),
514                bc: Transform::new(728376, -34265, Ratio::new(48000, 44100)),
515                ac: Transform::new(34327, -732864, Ratio::new(1000007, 918750)),
516                is_exact: Exact::Yes,
517            },
518            TestVector {
519                ab: Transform::new(0, i64::MAX - 5, Ratio::new(1, 1)),
520                bc: Transform::new(-100, 0, Ratio::new(1, 1)),
521                ac: Transform::new(0, i64::MAX, Ratio::new(1, 1)),
522                is_exact: Exact::Yes,
523            },
524            TestVector {
525                ab: Transform::new(0, i64::MIN + 5, Ratio::new(1, 1)),
526                bc: Transform::new(100, 0, Ratio::new(1, 1)),
527                ac: Transform::new(0, i64::MIN, Ratio::new(1, 1)),
528                is_exact: Exact::Yes,
529            },
530            TestVector {
531                ab: Transform::new(0, 100, Ratio::new(1, 1)),
532                bc: Transform::new(0, i64::MAX - 5, Ratio::new(1, 1)),
533                ac: Transform::new(0, i64::MAX, Ratio::new(1, 1)),
534                is_exact: Exact::Yes,
535            },
536            TestVector {
537                ab: Transform::new(0, -100, Ratio::new(1, 1)),
538                bc: Transform::new(0, i64::MIN + 5, Ratio::new(1, 1)),
539                ac: Transform::new(0, i64::MIN, Ratio::new(1, 1)),
540                is_exact: Exact::Yes,
541            },
542            TestVector {
543                ab: Transform::new(0, 0, Ratio::new(3465653567, 2327655023)),
544                bc: Transform::new(0, 0, Ratio::new(1291540343, 3698423317)),
545                ac: Transform::new(0, 0, Ratio::new(317609835, 610852072)),
546                is_exact: Exact::No,
547            },
548            TestVector {
549                ab: Transform::new(0, 20, Ratio::new(3465653567, 2327655023)),
550                bc: Transform::new(-3698423317 + 20, 5, Ratio::new(1291540343, 3698423317)),
551                ac: Transform::new(0, 1291540343 + 5, Ratio::new(317609835, 610852072)),
552                is_exact: Exact::No,
553            },
554        ];
555
556        for v in &test_vectors {
557            if v.is_exact == Exact::Yes {
558                let res_static = Transform::compose(&v.bc, &v.ab, Exact::Yes);
559                assert_eq!(res_static, v.ac);
560
561                let res_op = v.bc * v.ab;
562                assert_eq!(res_op, v.ac);
563            }
564
565            let res_inexact = Transform::compose(&v.bc, &v.ab, Exact::No);
566            assert_eq!(res_inexact, v.ac);
567        }
568    }
569}