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}