Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_graphics/src/h264/transform.rs

19.4 KiB, 39 runs

created by r1870400018:21092, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! The inverse transforms, and the quantisation that feeds them.
2//!
3//! H.264's transforms are integer and exact: unlike a discrete cosine transform they are specified
4//! as arithmetic rather than as a mathematical ideal, so two decoders that follow the clauses agree
5//! sample for sample and there is no drift to accumulate. There are four of them, and a picture
6//! uses all four:
7//!
8//! - The **four-by-four** transform (§8.5.12.2), which is what most residual is coded in.
9//! - The **eight-by-eight** transform (§8.5.13.2), which High profile adds and which 861 films in
10//! the corpus turn on.
11//! - A **four-by-four Hadamard** over the sixteen DC coefficients of an `Intra_16x16` macroblock
12//! (§8.5.10), which is how a flat region is coded cheaply: the sixteen blocks' DC terms are
13//! themselves transformed together.
14//! - A **two-by-two Hadamard** over the four chroma DC coefficients (§8.5.11.1).
15//!
16//! # The part that quietly ruins a picture
17//!
18//! Quantisation. `LevelScale` is the product of a *weight* -- from the scaling lists, which are the
19//! flat 16 only when the stream carries none at all -- and a *norm adjustment*, which is a
20//! six-by-six table indexed by the quantisation parameter modulo six and by where in the block the
21//! coefficient sits (§8.5.9). Both halves are easy to get subtly wrong, and neither failure looks
22//! like a failure: the picture comes out, and it is the wrong picture. The norm adjustment table is
23//! parsed out of the published specification by the tests rather than checked against the decoder
24//! that uses it.
25//!
26//! [Written with AI entirely](https://need2know.ai/entirely-ai/code)\
27//! Anthropic Claude
28
29use crate::h264::{
30 Scaling,
31 DEFAULT_4X4_INTRA,
32};
33
34use oxedyne_fe2o3_core::prelude::*;
35
36// Where the four-by-four zig-zag scan puts each coefficient, as a raster index (Table 8-13).
37// The list is idx to c_ij, and c_ij is row i and column j, so the entry is i * 4 + j.
38pub const ZIGZAG_4X4: [usize; 16] = [
39 0, 1, 4, 8, 5, 2, 3, 6, 9, 12, 13, 10, 7, 11, 14, 15,
40];
41
42// The same for the eight-by-eight scan (Table 8-14).
43pub const ZIGZAG_8X8: [usize; 64] = [
44 0, 1, 8, 16, 9, 2, 3, 10,
45 17, 24, 32, 25, 18, 11, 4, 5,
46 12, 19, 26, 33, 40, 48, 41, 34,
47 27, 20, 13, 6, 7, 14, 21, 28,
48 35, 42, 49, 56, 57, 50, 43, 36,
49 29, 22, 15, 23, 30, 37, 44, 51,
50 58, 59, 52, 45, 38, 31, 39, 46,
51 53, 60, 61, 54, 47, 55, 62, 63,
52];
53
54// The norm adjustment for the four-by-four transform, v of equation 8-315.
55// Six rows, one for each quantisation parameter modulo six, and three columns: the value for a
56// coefficient at an even row and even column, the value for an odd row and odd column, and the
57// value for everything else.
58pub const NORM_4X4: [[i32; 3]; 6] = [
59 [10, 16, 13],
60 [11, 18, 14],
61 [13, 20, 16],
62 [14, 23, 18],
63 [16, 25, 20],
64 [18, 29, 23],
65];
66
67// The norm adjustment for the eight-by-eight transform, v of equation 8-318.
68// Six rows again, and six columns for the six cases equation 8-317 distinguishes.
69pub const NORM_8X8: [[i32; 6]; 6] = [
70 [20, 18, 32, 19, 25, 24],
71 [22, 19, 35, 21, 28, 26],
72 [26, 23, 42, 24, 33, 31],
73 [28, 25, 45, 26, 35, 33],
74 [32, 28, 51, 30, 40, 38],
75 [36, 32, 58, 34, 46, 43],
76];
77
78// The chroma quantisation parameter for each luma one from 30 upward (Table 8-15).
79// Below 30 the two are equal; from 30 the chroma parameter climbs more slowly, so that chroma is
80// quantised less harshly than luma where luma is already coarse.
81const CHROMA_QP: [i32; 22] = [
82 29, 30, 31, 32, 32, 33, 34, 34, 35, 35, 36,
83 36, 37, 37, 37, 38, 38, 38, 39, 39, 39, 39,
84];
85
86/// The chroma quantisation parameter for a luma one and a component's offset (§8.5.8).
87pub fn chroma_qp(luma_qp: i32, offset: i32) -> i32 {
88 let qpi = (luma_qp + offset).clamp(0, 51);
89 if qpi < 30 {
90 qpi
91 } else {
92 // The index cannot escape the table: `qpi` is clamped to 51 above, and 51 − 30 is 21.
93 CHROMA_QP[(qpi - 30) as usize]
94 }
95}
96
97/// The weights one block quantises against, already inverse-scanned into raster order.
98///
99/// Held per macroblock kind rather than looked up per coefficient, because the lookup is the same
100/// for every coefficient of a block and the inverse scan is not free.
101#[derive(Clone, Debug)]
102pub struct Weights {
103 pub l4: [[i32; 16]; 6], // LevelScale4x4[m][i * 4 + j], six residues by sixteen positions
104 pub l8: [[i32; 64]; 6], // LevelScale8x8[m][i * 8 + j]
105}
106
107impl Weights {
108
109 /// `component` is 0 for luma, 1 for Cb and 2 for Cr, which is the `iYCbCr` of §8.5.9. Only the
110 /// intra lists are read, because every macroblock this decoder meets is intra.
111 pub fn intra(scaling: &Scaling, component: usize) -> Self {
112 let flat4 = scaling.l4[component.min(2)];
113 let flat8 = scaling.l8[(2 * component.min(2)).min(5)];
114 let mut weight4 = [0i32; 16];
115 for (idx, at) in ZIGZAG_4X4.iter().enumerate() {
116 weight4[*at] = flat4[idx] as i32;
117 }
118 let mut weight8 = [0i32; 64];
119 for (idx, at) in ZIGZAG_8X8.iter().enumerate() {
120 weight8[*at] = flat8[idx] as i32;
121 }
122 let mut l4 = [[0i32; 16]; 6];
123 for (m, row) in l4.iter_mut().enumerate() {
124 for i in 0..4 {
125 for j in 0..4 {
126 let which = match (i % 2, j % 2) {
127 (0, 0) => 0,
128 (1, 1) => 1,
129 _ => 2,
130 };
131 row[i * 4 + j] = weight4[i * 4 + j] * NORM_4X4[m][which];
132 }
133 }
134 }
135 let mut l8 = [[0i32; 64]; 6];
136 for (m, row) in l8.iter_mut().enumerate() {
137 for i in 0..8 {
138 for j in 0..8 {
139 // The six cases of equation 8-317, in the order the specification lists them.
140 let which = if i % 4 == 0 && j % 4 == 0 {
141 0
142 } else if i % 2 == 1 && j % 2 == 1 {
143 1
144 } else if i % 4 == 2 && j % 4 == 2 {
145 2
146 } else if (i % 4 == 0 && j % 2 == 1) || (i % 2 == 1 && j % 4 == 0) {
147 3
148 } else if (i % 4 == 0 && j % 4 == 2) || (i % 4 == 2 && j % 4 == 0) {
149 4
150 } else {
151 5
152 };
153 row[i * 8 + j] = weight8[i * 8 + j] * NORM_8X8[m][which];
154 }
155 }
156 }
157 Self { l4, l8 }
158 }
159}
160
161/// Scales a four-by-four block's coefficients (§8.5.12.1).
162///
163/// `skip_dc` is set where the block's DC term has already been scaled elsewhere -- in an
164/// `Intra_16x16` macroblock, where the sixteen DC terms are transformed together, and in every
165/// chroma block, where the four are.
166pub fn scale_4x4(c: &[i32; 16], w: &Weights, qp: i32, skip_dc: bool) -> [i32; 16] {
167 let m = (qp.rem_euclid(6)) as usize;
168 let shift = qp.div_euclid(6);
169 let mut d = [0i32; 16];
170 for at in 0..16 {
171 if at == 0 && skip_dc {
172 d[0] = c[0];
173 continue;
174 }
175 let scaled = c[at] * w.l4[m][at];
176 d[at] = if shift >= 4 {
177 scaled << (shift - 4)
178 } else {
179 (scaled + (1 << (3 - shift))) >> (4 - shift)
180 };
181 }
182 d
183}
184
185/// Scales an eight-by-eight block's coefficients (§8.5.13.1).
186pub fn scale_8x8(c: &[i32; 64], w: &Weights, qp: i32) -> [i32; 64] {
187 let m = (qp.rem_euclid(6)) as usize;
188 let shift = qp.div_euclid(6);
189 let mut d = [0i32; 64];
190 for at in 0..64 {
191 let scaled = c[at] * w.l8[m][at];
192 d[at] = if shift >= 6 {
193 scaled << (shift - 6)
194 } else {
195 (scaled + (1 << (5 - shift))) >> (6 - shift)
196 };
197 }
198 d
199}
200
201/// The inverse four-by-four transform (§8.5.12.2), including the final rounding shift.
202pub fn inverse_4x4(d: &[i32; 16]) -> [i32; 16] {
203 let mut f = [0i32; 16];
204 // Each row.
205 for i in 0..4 {
206 let r = i * 4;
207 let e0 = d[r] + d[r + 2];
208 let e1 = d[r] - d[r + 2];
209 let e2 = (d[r + 1] >> 1) - d[r + 3];
210 let e3 = d[r + 1] + (d[r + 3] >> 1);
211 f[r] = e0 + e3;
212 f[r + 1] = e1 + e2;
213 f[r + 2] = e1 - e2;
214 f[r + 3] = e0 - e3;
215 }
216 let mut h = [0i32; 16];
217 // Then each column.
218 for j in 0..4 {
219 let g0 = f[j] + f[8 + j];
220 let g1 = f[j] - f[8 + j];
221 let g2 = (f[4 + j] >> 1) - f[12 + j];
222 let g3 = f[4 + j] + (f[12 + j] >> 1);
223 h[j] = g0 + g3;
224 h[4 + j] = g1 + g2;
225 h[8 + j] = g1 - g2;
226 h[12 + j] = g0 - g3;
227 }
228 let mut r = [0i32; 16];
229 for at in 0..16 {
230 r[at] = (h[at] + 32) >> 6;
231 }
232 r
233}
234
235/// One pass of the inverse eight-by-eight transform along a row of eight (§8.5.13.2).
236fn pass_8(d: [i32; 8]) -> [i32; 8] {
237 let e0 = d[0] + d[4];
238 let e1 = -d[3] + d[5] - d[7] - (d[7] >> 1);
239 let e2 = d[0] - d[4];
240 let e3 = d[1] + d[7] - d[3] - (d[3] >> 1);
241 let e4 = (d[2] >> 1) - d[6];
242 let e5 = -d[1] + d[7] + d[5] + (d[5] >> 1);
243 let e6 = d[2] + (d[6] >> 1);
244 let e7 = d[3] + d[5] + d[1] + (d[1] >> 1);
245 let f0 = e0 + e6;
246 let f1 = e1 + (e7 >> 2);
247 let f2 = e2 + e4;
248 let f3 = e3 + (e5 >> 2);
249 let f4 = e2 - e4;
250 let f5 = (e3 >> 2) - e5;
251 let f6 = e0 - e6;
252 let f7 = e7 - (e1 >> 2);
253 [
254 f0 + f7,
255 f2 + f5,
256 f4 + f3,
257 f6 + f1,
258 f6 - f1,
259 f4 - f3,
260 f2 - f5,
261 f0 - f7,
262 ]
263}
264
265/// The inverse eight-by-eight transform (§8.5.13.2), including the final rounding shift.
266pub fn inverse_8x8(d: &[i32; 64]) -> [i32; 64] {
267 let mut g = [0i32; 64];
268 for i in 0..8 {
269 let mut row = [0i32; 8];
270 row.copy_from_slice(&d[i * 8..i * 8 + 8]);
271 let out = pass_8(row);
272 g[i * 8..i * 8 + 8].copy_from_slice(&out);
273 }
274 let mut m = [0i32; 64];
275 for j in 0..8 {
276 let mut col = [0i32; 8];
277 for i in 0..8 {
278 col[i] = g[i * 8 + j];
279 }
280 let out = pass_8(col);
281 for i in 0..8 {
282 m[i * 8 + j] = out[i];
283 }
284 }
285 let mut r = [0i32; 64];
286 for at in 0..64 {
287 r[at] = (m[at] + 32) >> 6;
288 }
289 r
290}
291
292/// The inverse Hadamard and scaling over an `Intra_16x16` macroblock's sixteen DC terms (§8.5.10).
293pub fn luma_dc(c: &[i32; 16], w: &Weights, qp: i32) -> [i32; 16] {
294 let mut f = [0i32; 16];
295 // Rows.
296 for i in 0..4 {
297 let r = i * 4;
298 let a = c[r] + c[r + 1] + c[r + 2] + c[r + 3];
299 let b = c[r] + c[r + 1] - c[r + 2] - c[r + 3];
300 let d = c[r] - c[r + 1] - c[r + 2] + c[r + 3];
301 let e = c[r] - c[r + 1] + c[r + 2] - c[r + 3];
302 f[r] = a;
303 f[r + 1] = b;
304 f[r + 2] = d;
305 f[r + 3] = e;
306 }
307 let mut g = [0i32; 16];
308 // Columns.
309 for j in 0..4 {
310 let a = f[j] + f[4 + j] + f[8 + j] + f[12 + j];
311 let b = f[j] + f[4 + j] - f[8 + j] - f[12 + j];
312 let d = f[j] - f[4 + j] - f[8 + j] + f[12 + j];
313 let e = f[j] - f[4 + j] + f[8 + j] - f[12 + j];
314 g[j] = a;
315 g[4 + j] = b;
316 g[8 + j] = d;
317 g[12 + j] = e;
318 }
319 let m = (qp.rem_euclid(6)) as usize;
320 let shift = qp.div_euclid(6);
321 let level = w.l4[m][0];
322 let mut out = [0i32; 16];
323 for at in 0..16 {
324 out[at] = if qp >= 36 {
325 (g[at] * level) << (shift - 6)
326 } else {
327 (g[at] * level + (1 << (5 - shift))) >> (6 - shift)
328 };
329 }
330 out
331}
332
333/// The inverse Hadamard and scaling over a 4:2:0 chroma block's four DC terms (§8.5.11).
334pub fn chroma_dc(c: &[i32; 4], w: &Weights, qp: i32) -> [i32; 4] {
335 // The two-by-two transform of equation 8-324, with `c` in raster order.
336 let f = [
337 c[0] + c[1] + c[2] + c[3],
338 c[0] - c[1] + c[2] - c[3],
339 c[0] + c[1] - c[2] - c[3],
340 c[0] - c[1] - c[2] + c[3],
341 ];
342 let m = (qp.rem_euclid(6)) as usize;
343 let shift = qp.div_euclid(6);
344 let level = w.l4[m][0];
345 let mut out = [0i32; 4];
346 for at in 0..4 {
347 // Equation 8-326: a left shift by the whole part and then a right shift by five. Written as
348 // one shift it would be wrong, because the left shift happens first and may carry bits into
349 // the top that a combined shift would drop.
350 out[at] = ((f[at] * level) << shift) >> 5;
351 }
352 out
353}
354
355/// Kept beside the transforms because a caller assembling a picture wants the flat case without
356/// building a whole [`Scaling`] to get it.
357pub fn flat_intra_weights() -> Weights {
358 Weights::intra(&Scaling::flat(), 0)
359}
360
361/// Is a set of weights the flat one, which is what most films quantise against?
362pub fn is_flat(scaling: &Scaling) -> bool {
363 scaling.l4.iter().all(|l| l.iter().all(|v| *v == 16))
364 && scaling.l8.iter().all(|l| l.iter().all(|v| *v == 16))
365}
366
367pub const DEFAULT_IS_NOT_FLAT: [u8; 16] = DEFAULT_4X4_INTRA; // the tests lean on this
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372
373 /// The lines of a text rendering of the specification, where one is to hand.
374 fn spec() -> Option<Vec<String>> {
375 let path = match std::env::var("H264_SPEC_TEXT") {
376 Ok(p) => p,
377 Err(_) => {
378 println!(" skipped: set H264_SPEC_TEXT to a text rendering of Rec. ITU-T H.264");
379 return None;
380 },
381 };
382 match std::fs::read_to_string(&path) {
383 Ok(t) => Some(t.lines().map(|l| l.to_string()).collect()),
384 Err(e) => {
385 println!(" skipped: {} would not read ({})", path, e);
386 None
387 },
388 }
389 }
390
391 #[test]
392 fn test_the_scans_are_the_published_ones_01() -> Outcome<()> {
393 // Eighty positions copied out of a document, every one of which puts a coefficient in the
394 // wrong place if it is wrong -- and a coefficient in the wrong place is a picture with a
395 // texture that is not the one that was photographed, not an error. Tables 8-13 and 8-14
396 // give them as `cij` labels in a row, so this reads the labels.
397 //
398 // The two tables must be told apart by their headings and not by the labels, because every
399 // label in the four-by-four table is a legal eight-by-eight one: reading Table 8-13's row
400 // as though it were Table 8-14's yields a plausible sixteen-entry scan that is wrong.
401 let lines = match spec() {
402 Some(l) => l,
403 None => return Ok(()),
404 };
405 for (want, number, side) in [
406 (&ZIGZAG_4X4[..], 13usize, 4usize),
407 (&ZIGZAG_8X8[..], 14usize, 8usize),
408 ] {
409 let heading = fmt!("Table 8-{} ", number);
410 let mut found: Vec<usize> = Vec::new();
411 let mut inside = false;
412 for line in &lines {
413 let trimmed = line.trim_start();
414 if trimmed.starts_with("Table 8-") {
415 // A heading, and its continuations, belong to whichever table they name.
416 inside = trimmed.starts_with(&heading);
417 continue;
418 }
419 if !inside || !trimmed.starts_with("zig-zag") {
420 continue;
421 }
422 for word in trimmed.trim_start_matches("zig-zag").split_whitespace() {
423 let digits: Vec<char> = word.chars().skip(1).collect();
424 if !word.starts_with('c') || digits.len() != 2 {
425 return Err(err!(
426 "Table 8-{}'s zig-zag row holds {:?}, which is not a coefficient \
427 label.", number, word; Test, Invalid));
428 }
429 let (i, j) = match (digits[0].to_digit(10), digits[1].to_digit(10)) {
430 (Some(i), Some(j)) if (i as usize) < side && (j as usize) < side =>
431 (i as usize, j as usize),
432 _ => return Err(err!(
433 "Table 8-{} names {}, which is outside a {} by {} block.",
434 number, word, side, side; Test, Invalid)),
435 };
436 found.push(i * side + j);
437 }
438 }
439 // The table of contents carries the heading too, with nothing under it, so what is
440 // gathered is one whole scan and no more.
441 if found != want {
442 return Err(err!(
443 "Table 8-{}: this decoder scans {:?} and the specification scans {:?}.",
444 number, want, found; Test, Mismatch));
445 }
446 }
447 Ok(())
448 }
449
450 #[test]
451 fn test_a_flat_direct_current_survives_the_transform_02() -> Outcome<()> {
452 // The one property of the inverse transform that can be stated without a second decoder: a
453 // block whose only coefficient is the direct current term comes out flat, at a level the
454 // coefficient sets. A transform with a sign wrong somewhere in the butterfly still produces
455 // a flat block from a flat input, so this is checked with a second, uneven input too.
456 let mut d = [0i32; 16];
457 d[0] = 64;
458 let r = inverse_4x4(&d);
459 for v in r {
460 req!(v, 1, "a block with only a direct current term did not come out flat");
461 }
462 let mut d = [0i32; 64];
463 d[0] = 64;
464 let r = inverse_8x8(&d);
465 for v in r {
466 req!(v, 1, "an eight-by-eight block with only a direct current term was not flat");
467 }
468 // A single coefficient one place along the first row is a horizontal ramp, which is to say
469 // the left half and the right half of every row differ in sign.
470 let mut d = [0i32; 16];
471 d[1] = 128;
472 let r = inverse_4x4(&d);
473 for i in 0..4 {
474 let left = r[i * 4];
475 let right = r[i * 4 + 3];
476 let opposed = left > 0 && right < 0;
477 req!(opposed, true, "row {} of a horizontal ramp runs {:?}", i, &r[i * 4..i * 4 + 4]);
478 }
479 Ok(())
480 }
481
482 #[test]
483 fn test_the_norm_adjustments_are_the_published_ones_03() -> Outcome<()> {
484 // Fifty-four numbers, each of which multiplies every coefficient it touches. One wrong
485 // entry quantises one class of coefficient wrongly at one of the six quantisation
486 // residues, which is a picture that is right five times in six and wrong the sixth -- the
487 // hardest kind of fault to see and the easiest to introduce by copying a matrix out of a
488 // PDF whose rows have been reflowed.
489 //
490 // The matrices are printed as bracketed rows around equations 8-315 and 8-318, and the
491 // brackets, the `v=` and the equation number land on lines of their own. So what is
492 // gathered is every line that is nothing but the right count of integers, and what is
493 // looked for is six of them in a row that say what this module says.
494 let lines = match spec() {
495 Some(l) => l,
496 None => return Ok(()),
497 };
498 for (want, width, eq) in [
499 (NORM_4X4.iter().flatten().copied().collect::<Vec<i32>>(), 3usize, "8-315"),
500 (NORM_8X8.iter().flatten().copied().collect::<Vec<i32>>(), 6usize, "8-318"),
501 ] {
502 let rows: Vec<Vec<i32>> = lines.iter()
503 .filter_map(|l| {
504 // The PDF draws the matrix's brackets as glyphs in the private use area, and
505 // they arrive stuck to the numbers beside them, so a token like `\u{f0ea}13`
506 // parses as nothing at all. Everything outside ASCII becomes a space.
507 let plain: String = l.chars()
508 .map(|c| if c.is_ascii() { c } else { ' ' })
509 .collect();
510 let words: Vec<&str> = plain.split_whitespace().collect();
511 if words.len() != width {
512 return None;
513 }
514 let nums: Vec<i32> = words.iter().filter_map(|w| w.parse::<i32>().ok())
515 .collect();
516 if nums.len() == width { Some(nums) } else { None }
517 })
518 .collect();
519 let mut got = false;
520 for start in 0..rows.len().saturating_sub(5) {
521 let run: Vec<i32> = rows[start..start + 6].iter().flatten().copied().collect();
522 if run == want {
523 got = true;
524 break;
525 }
526 }
527 if !got {
528 return Err(err!(
529 "Equation {}'s matrix, {:?}, is not in the specification text as six rows of \
530 {}.", eq, want, width; Test, Mismatch));
531 }
532 }
533 Ok(())
534 }
535
536 #[test]
537 fn test_the_chroma_quantiser_bends_away_from_the_luma_one_04() -> Outcome<()> {
538 // Below thirty the two are equal; above it the chroma one climbs more slowly, and at the
539 // top it stops at 39 while luma runs on to 51. A decoder that used the luma parameter for
540 // chroma would produce a picture whose colour is quantised far too coarsely wherever the
541 // picture is already coarse, which looks like blotches of colour in shadow.
542 for qp in 0..30 {
543 req!(chroma_qp(qp, 0), qp, "below thirty the two parameters part company");
544 }
545 req!(chroma_qp(30, 0), 29);
546 req!(chroma_qp(39, 0), 35);
547 req!(chroma_qp(51, 0), 39, "the chroma parameter runs past its ceiling");
548 // The offset is applied before the table, and the sum is clipped into range first.
549 req!(chroma_qp(51, 12), 39);
550 req!(chroma_qp(0, -12), 0, "a negative index was not clipped");
551 req!(chroma_qp(40, -12), 28);
552 Ok(())
553 }
554
555 #[test]
556 fn test_the_weights_are_not_flat_where_the_lists_are_not_05() -> Outcome<()> {
557 // The whole point of carrying the scaling lists through: a stream whose lists are the
558 // default intra matrix must quantise differently from one whose lists are flat. If these
559 // two agreed, every one of the 33 films in the corpus that carries lists would decode as
560 // though it carried none.
561 let flat = Weights::intra(&Scaling::flat(), 0);
562 let mut defaults = Scaling::flat();
563 defaults.l4[0] = DEFAULT_4X4_INTRA;
564 let scaled = Weights::intra(&defaults, 0);
565 let same = flat.l4[0] == scaled.l4[0];
566 req!(same, false, "the default intra list quantises exactly as a flat one does");
567 // And the direct current term is the one place they agree, since the default list's first
568 // entry is 6 and a flat one's is 16 -- so in fact they must differ there too.
569 let dc_same = flat.l4[0][0] == scaled.l4[0][0];
570 req!(dc_same, false, "the direct current weight is the same either way");
571 Ok(())
572 }
573}