Skip to main content
← PSoXide SDK guides and examples · SDK f2fec1c301f1 · All Cargo features enabled; check feature gates before use.

psx_fmv/
idct.rs

1// SPDX-License-Identifier: GPL-2.0-or-later
2//! The MDEC's IDCT matrix, generated from the DCT basis.
3//!
4//! MDEC command 3 takes 64 signed 16-bit coefficients, sent as 32 words
5//! with the even-numbered coefficient in each word's low halfword
6//! (psx-spx, "MDEC Decompression", command 3). Coefficient `8k + n` is
7//! the 8-point DCT-II basis function of frequency `k` sampled at `n`,
8//! scaled to 1.15 fixed point and truncated toward zero:
9//!
10//! ```text
11//! M[0][n] = 32768 / sqrt(2)                      (= 16384 * sqrt(2))
12//! M[k][n] = 32768 * cos((2n + 1) * k * pi / 16)   for k = 1..7
13//! ```
14//!
15//! Everything below runs at compile time, in integers: pi from Machin's
16//! formula, cosines from their Taylor series in 2.62 fixed point, and the
17//! DC term from an integer square root. Nothing here exists at run time
18//! except the finished table.
19
20/// One in the 2.62 fixed point used for the intermediate math.
21// psx-numeric-allow-next-line: const-evaluated; only the finished i16 table exists at run time
22const ONE: i128 = 1 << 62;
23
24/// Coefficient scale: the matrix is in 1.15 fixed point.
25// psx-numeric-allow-next-line: const-evaluated; only the finished i16 table exists at run time
26const SCALE: i128 = 1 << 15;
27
28/// arctan(1/x) in 2.62 fixed point, from its alternating series.
29// psx-numeric-allow-next-line: const-evaluated; only the finished i16 table exists at run time
30const fn atan_inv(x: i128) -> i128 {
31    let x2 = x * x;
32    let mut power = ONE / x; // 1 / x^(2i+1)
33    let mut sum = power;
34    let mut i = 1;
35    while power != 0 {
36        power /= x2;
37        let term = power / (2 * i + 1);
38        if i % 2 == 1 {
39            sum -= term;
40        } else {
41            sum += term;
42        }
43        i += 1;
44    }
45    sum
46}
47
48/// pi in 2.62 fixed point (Machin: pi = 16 atan(1/5) - 4 atan(1/239)).
49// psx-numeric-allow-next-line: const-evaluated; only the finished i16 table exists at run time
50const PI: i128 = 16 * atan_inv(5) - 4 * atan_inv(239);
51
52/// cos(a * pi / 16) in 2.62 fixed point, for `a` in 0..=8 (the first
53/// quadrant), from the Taylor series around zero.
54// psx-numeric-allow-next-line: const-evaluated; only the finished i16 table exists at run time
55const fn cos_sixteenths(a: i128) -> i128 {
56    let theta = PI * a / 16;
57    let mut term = ONE;
58    let mut sum = ONE;
59    let mut i = 1;
60    while term != 0 {
61        // term(i) = -term(i-1) * theta^2 / ((2i - 1) * 2i)
62        term = -(term * theta / ONE) * theta / ONE / ((2 * i - 1) * (2 * i));
63        sum += term;
64        i += 1;
65    }
66    sum
67}
68
69/// trunc(32768 * cos(m * pi / 16)) for any `m`, folding the angle into
70/// the first quadrant and carrying the sign separately.
71const fn basis(m: usize) -> i16 {
72    // psx-numeric-allow-next-line: const-evaluated; only the finished i16 table exists at run time
73    let m = (m % 32) as i128;
74    let (a, negative) = match m {
75        0..=8 => (m, false),
76        9..=16 => (16 - m, true),
77        17..=24 => (m - 16, true),
78        _ => (32 - m, false),
79    };
80    let mut c = cos_sixteenths(a);
81    if c < 0 {
82        // Only a rounding sliver at a = 8, where the cosine is zero.
83        c = 0;
84    }
85    let magnitude = (SCALE * c / ONE) as i16;
86    if negative {
87        -magnitude
88    } else {
89        magnitude
90    }
91}
92
93/// floor(sqrt(n)), by bisection.
94// psx-numeric-allow-next-line: const-evaluated; only the finished i16 table exists at run time
95const fn isqrt(n: u64) -> u64 {
96    let (mut lo, mut hi) = (0u64, n + 1);
97    while hi - lo > 1 {
98        let mid = (lo + hi) / 2;
99        if mid * mid <= n {
100            lo = mid;
101        } else {
102            hi = mid;
103        }
104    }
105    lo
106}
107
108/// The 64 coefficients, frequency-major: `MATRIX[8 * k + n]`.
109pub const MATRIX: [i16; 64] = {
110    // 32768 / sqrt(2) = sqrt(2^29).
111    let dc = isqrt(1 << 29) as i16;
112    let mut m = [0i16; 64];
113    let mut n = 0;
114    while n < 8 {
115        m[n] = dc;
116        let mut k = 1;
117        while k < 8 {
118            m[8 * k + n] = basis((2 * n + 1) * k);
119            k += 1;
120        }
121        n += 1;
122    }
123    m
124};
125
126/// [`MATRIX`] packed for the MDEC: word `w` holds coefficient `2w` in its
127/// low halfword and `2w + 1` in its high halfword.
128pub const MATRIX_WORDS: [u32; 32] = {
129    let mut words = [0u32; 32];
130    let mut w = 0;
131    while w < 32 {
132        let lo = MATRIX[2 * w] as u16 as u32;
133        let hi = MATRIX[2 * w + 1] as u16 as u32;
134        words[w] = lo | (hi << 16);
135        w += 1;
136    }
137    words
138};
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    /// The table the driver uploaded before this module generated it,
145    /// captured by running the previous code. The generated one must
146    /// match it word for word.
147    const PREVIOUS: [u32; 32] = [
148        0x5A82_5A82,
149        0x5A82_5A82,
150        0x5A82_5A82,
151        0x5A82_5A82,
152        0x6A6D_7D8A,
153        0x18F8_471C,
154        0xB8E4_E708,
155        0x8276_9593,
156        0x30FB_7641,
157        0x89BF_CF05,
158        0xCF05_89BF,
159        0x7641_30FB,
160        0xE708_6A6D,
161        0xB8E4_8276,
162        0x7D8A_471C,
163        0x9593_18F8,
164        0xA57E_5A82,
165        0x5A82_A57E,
166        0xA57E_5A82,
167        0x5A82_A57E,
168        0x8276_471C,
169        0x6A6D_18F8,
170        0xE708_9593,
171        0xB8E4_7D8A,
172        0x89BF_30FB,
173        0xCF05_7641,
174        0x7641_CF05,
175        0x30FB_89BF,
176        0xB8E4_18F8,
177        0x8276_6A6D,
178        0x9593_7D8A,
179        0xE708_471C,
180    ];
181
182    #[test]
183    fn generated_matrix_matches_the_previous_table() {
184        assert_eq!(MATRIX_WORDS, PREVIOUS);
185    }
186
187    #[test]
188    fn basis_rows_are_orthogonal() {
189        // Distinct DCT-II rows are orthogonal. Truncating to 1.15 moves
190        // each of the 8 products by less than 2 * 32768, against a row
191        // norm near 2^32.
192        for a in 0..8 {
193            for b in 0..a {
194                let dot: i64 = (0..8)
195                    .map(|n| i64::from(MATRIX[8 * a + n]) * i64::from(MATRIX[8 * b + n]))
196                    .sum();
197                assert!(dot.abs() < 1 << 20, "rows {a} and {b}: {dot}");
198            }
199        }
200    }
201}