Skip to content

Commit 2e7ed8c

Browse files
matmenclaude
andcommitted
AVIF encoder: unrolled forward DCTs, fewer trials
- the 4-, 8- and 16-point DCTs (also the leaves of the 32 and 64 point ones) as straight-line code, generated by scripts/av1-fdct.py: 2-4 times faster, the same coefficients - the projections of the other transform types four coefficients at a time - below effort 2, the second luma and chroma modes are only coded if their SATD is within 10% / 20% of the best - the color conversion without Uint8Array.from's callbacks Together with d5b9af2 about 40% less CPU time at effort 1 than 35be9bf, BD-rate -0.8% to +1.8% per image. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lfv41PqHEGbU7SLg5onpG1
1 parent d5b9af2 commit 2e7ed8c

5 files changed

Lines changed: 238 additions & 16 deletions

File tree

‎scripts/av1-fdct.py‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Writes src/codecs/av1/fdct-lee.ts: the unnormalized 4-, 8- and 16-point DCT-IIs of src/codecs/av1/fdct.ts's recursion
2+
# (Lee's) as straight-line code (the same operations, on locals instead of scratch arrays):
3+
# python scripts/av1-fdct.py > src/codecs/av1/fdct-lee.ts
4+
import math
5+
counter = [0]
6+
lines = []
7+
def tmp(expr):
8+
name = f't{counter[0]}'
9+
counter[0] += 1
10+
lines.append(f' const {name} = {expr};')
11+
return name
12+
13+
# 1 / (2 cos(pi (2i + 1) / 2n))
14+
twiddle = lambda i, n: repr(1 / (2 * math.cos(math.pi * (2 * i + 1) / (2 * n))))
15+
16+
def dct(x):
17+
n = len(x)
18+
if n == 2:
19+
return [tmp(f'{x[0]} + {x[1]}'), tmp(f'({x[0]} - {x[1]}) * {twiddle(0, 2)}')]
20+
half = n // 2
21+
a = [tmp(f'{x[i]} + {x[n - 1 - i]}') for i in range(half)]
22+
b = [tmp(f'({x[i]} - {x[n - 1 - i]}) * {twiddle(i, n)}') for i in range(half)]
23+
even, odd = dct(a), dct(b)
24+
out = [None] * n
25+
for k in range(half):
26+
out[2 * k] = even[k]
27+
out[2 * k + 1] = tmp(f'{odd[k]} + {odd[k + 1]}') if k + 1 < half else odd[k]
28+
return out
29+
30+
print('''/*!
31+
* Generated by scripts/av1-fdct.py: the 4-, 8- and 16-point DCTs of fdct.ts's recursion, unrolled
32+
*/''')
33+
for n in (4, 8, 16):
34+
counter[0] = 0; lines.clear()
35+
at = lambda i: f'x[o + {i}]' if i else 'x[o]'
36+
out = dct([at(i) for i in range(n)])
37+
print(f'\n// the unnormalized {n}-point DCT-II of x[o] in place')
38+
print(f'export function dct{n}(x: Float64Array, o: number): void {{')
39+
for l in lines: print(l)
40+
for i in range(n): print(f' {at(i)} = {out[i]};')
41+
print('}')

‎src/codecs/av1/encode.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,9 @@ class TileEncoder {
332332
candidates.sort((x, y) => x.est - y.est);
333333
// (the best one's reconstruction kept)
334334
let best = Infinity, bestMode = candidates[0], dist = 0, bits = 0, coded = false, kept: Uint16Array | null = null, bestTx = DCT_DCT;
335-
const tries = candidates.slice(0, [1, 2, 3][this.params.effort]);
335+
// (below effort 2 only those about as good as the best)
336+
const close = this.params.effort < 2 ? 1.1 : Infinity;
337+
const tries = candidates.slice(0, [1, 2, 3][this.params.effort]).filter(c => c.est <= candidates[0].est * close);
336338
for (const [i, c] of tries.entries()) {
337339
b.yMode = c.mode;
338340
b.yAngle = c.angle;
@@ -396,7 +398,8 @@ class TileEncoder {
396398
}
397399
uvCandidates.sort((x, y) => x.est - y.est);
398400
let bestUv = Infinity, bestUvMode = uvCandidates[0], uvDist = 0, uvBits = 0, uvCoded = false, keptUv: Uint16Array[] | null = null;
399-
const uvTries = uvCandidates.slice(0, [1, 2, 2][this.params.effort]);
401+
const uvClose = this.params.effort < 2 ? 1.2 : Infinity;
402+
const uvTries = uvCandidates.slice(0, [1, 2, 2][this.params.effort]).filter(c => c.est <= uvCandidates[0].est * uvClose);
400403
for (const [i, c] of uvTries.entries()) {
401404
b.uvMode = c.mode;
402405
b.uvAngle = c.angle;

‎src/codecs/av1/fdct-lee.ts‎

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/*!
2+
* Generated by scripts/av1-fdct.py: the 4-, 8- and 16-point DCTs of fdct.ts's recursion, unrolled
3+
*/
4+
5+
// the unnormalized 4-point DCT-II of x[o] in place
6+
export function dct4(x: Float64Array, o: number): void {
7+
const t0 = x[o] + x[o + 3];
8+
const t1 = x[o + 1] + x[o + 2];
9+
const t2 = (x[o] - x[o + 3]) * 0.541196100146197;
10+
const t3 = (x[o + 1] - x[o + 2]) * 1.3065629648763764;
11+
const t4 = t0 + t1;
12+
const t5 = (t0 - t1) * 0.7071067811865475;
13+
const t6 = t2 + t3;
14+
const t7 = (t2 - t3) * 0.7071067811865475;
15+
const t8 = t6 + t7;
16+
x[o] = t4;
17+
x[o + 1] = t8;
18+
x[o + 2] = t5;
19+
x[o + 3] = t7;
20+
}
21+
22+
// the unnormalized 8-point DCT-II of x[o] in place
23+
export function dct8(x: Float64Array, o: number): void {
24+
const t0 = x[o] + x[o + 7];
25+
const t1 = x[o + 1] + x[o + 6];
26+
const t2 = x[o + 2] + x[o + 5];
27+
const t3 = x[o + 3] + x[o + 4];
28+
const t4 = (x[o] - x[o + 7]) * 0.5097955791041592;
29+
const t5 = (x[o + 1] - x[o + 6]) * 0.6013448869350453;
30+
const t6 = (x[o + 2] - x[o + 5]) * 0.8999762231364156;
31+
const t7 = (x[o + 3] - x[o + 4]) * 2.5629154477415055;
32+
const t8 = t0 + t3;
33+
const t9 = t1 + t2;
34+
const t10 = (t0 - t3) * 0.541196100146197;
35+
const t11 = (t1 - t2) * 1.3065629648763764;
36+
const t12 = t8 + t9;
37+
const t13 = (t8 - t9) * 0.7071067811865475;
38+
const t14 = t10 + t11;
39+
const t15 = (t10 - t11) * 0.7071067811865475;
40+
const t16 = t14 + t15;
41+
const t17 = t4 + t7;
42+
const t18 = t5 + t6;
43+
const t19 = (t4 - t7) * 0.541196100146197;
44+
const t20 = (t5 - t6) * 1.3065629648763764;
45+
const t21 = t17 + t18;
46+
const t22 = (t17 - t18) * 0.7071067811865475;
47+
const t23 = t19 + t20;
48+
const t24 = (t19 - t20) * 0.7071067811865475;
49+
const t25 = t23 + t24;
50+
const t26 = t21 + t25;
51+
const t27 = t25 + t22;
52+
const t28 = t22 + t24;
53+
x[o] = t12;
54+
x[o + 1] = t26;
55+
x[o + 2] = t16;
56+
x[o + 3] = t27;
57+
x[o + 4] = t13;
58+
x[o + 5] = t28;
59+
x[o + 6] = t15;
60+
x[o + 7] = t24;
61+
}
62+
63+
// the unnormalized 16-point DCT-II of x[o] in place
64+
export function dct16(x: Float64Array, o: number): void {
65+
const t0 = x[o] + x[o + 15];
66+
const t1 = x[o + 1] + x[o + 14];
67+
const t2 = x[o + 2] + x[o + 13];
68+
const t3 = x[o + 3] + x[o + 12];
69+
const t4 = x[o + 4] + x[o + 11];
70+
const t5 = x[o + 5] + x[o + 10];
71+
const t6 = x[o + 6] + x[o + 9];
72+
const t7 = x[o + 7] + x[o + 8];
73+
const t8 = (x[o] - x[o + 15]) * 0.5024192861881557;
74+
const t9 = (x[o + 1] - x[o + 14]) * 0.5224986149396889;
75+
const t10 = (x[o + 2] - x[o + 13]) * 0.5669440348163577;
76+
const t11 = (x[o + 3] - x[o + 12]) * 0.6468217833599901;
77+
const t12 = (x[o + 4] - x[o + 11]) * 0.7881546234512502;
78+
const t13 = (x[o + 5] - x[o + 10]) * 1.060677685990347;
79+
const t14 = (x[o + 6] - x[o + 9]) * 1.7224470982383342;
80+
const t15 = (x[o + 7] - x[o + 8]) * 5.101148618689155;
81+
const t16 = t0 + t7;
82+
const t17 = t1 + t6;
83+
const t18 = t2 + t5;
84+
const t19 = t3 + t4;
85+
const t20 = (t0 - t7) * 0.5097955791041592;
86+
const t21 = (t1 - t6) * 0.6013448869350453;
87+
const t22 = (t2 - t5) * 0.8999762231364156;
88+
const t23 = (t3 - t4) * 2.5629154477415055;
89+
const t24 = t16 + t19;
90+
const t25 = t17 + t18;
91+
const t26 = (t16 - t19) * 0.541196100146197;
92+
const t27 = (t17 - t18) * 1.3065629648763764;
93+
const t28 = t24 + t25;
94+
const t29 = (t24 - t25) * 0.7071067811865475;
95+
const t30 = t26 + t27;
96+
const t31 = (t26 - t27) * 0.7071067811865475;
97+
const t32 = t30 + t31;
98+
const t33 = t20 + t23;
99+
const t34 = t21 + t22;
100+
const t35 = (t20 - t23) * 0.541196100146197;
101+
const t36 = (t21 - t22) * 1.3065629648763764;
102+
const t37 = t33 + t34;
103+
const t38 = (t33 - t34) * 0.7071067811865475;
104+
const t39 = t35 + t36;
105+
const t40 = (t35 - t36) * 0.7071067811865475;
106+
const t41 = t39 + t40;
107+
const t42 = t37 + t41;
108+
const t43 = t41 + t38;
109+
const t44 = t38 + t40;
110+
const t45 = t8 + t15;
111+
const t46 = t9 + t14;
112+
const t47 = t10 + t13;
113+
const t48 = t11 + t12;
114+
const t49 = (t8 - t15) * 0.5097955791041592;
115+
const t50 = (t9 - t14) * 0.6013448869350453;
116+
const t51 = (t10 - t13) * 0.8999762231364156;
117+
const t52 = (t11 - t12) * 2.5629154477415055;
118+
const t53 = t45 + t48;
119+
const t54 = t46 + t47;
120+
const t55 = (t45 - t48) * 0.541196100146197;
121+
const t56 = (t46 - t47) * 1.3065629648763764;
122+
const t57 = t53 + t54;
123+
const t58 = (t53 - t54) * 0.7071067811865475;
124+
const t59 = t55 + t56;
125+
const t60 = (t55 - t56) * 0.7071067811865475;
126+
const t61 = t59 + t60;
127+
const t62 = t49 + t52;
128+
const t63 = t50 + t51;
129+
const t64 = (t49 - t52) * 0.541196100146197;
130+
const t65 = (t50 - t51) * 1.3065629648763764;
131+
const t66 = t62 + t63;
132+
const t67 = (t62 - t63) * 0.7071067811865475;
133+
const t68 = t64 + t65;
134+
const t69 = (t64 - t65) * 0.7071067811865475;
135+
const t70 = t68 + t69;
136+
const t71 = t66 + t70;
137+
const t72 = t70 + t67;
138+
const t73 = t67 + t69;
139+
const t74 = t57 + t71;
140+
const t75 = t71 + t61;
141+
const t76 = t61 + t72;
142+
const t77 = t72 + t58;
143+
const t78 = t58 + t73;
144+
const t79 = t73 + t60;
145+
const t80 = t60 + t69;
146+
x[o] = t28;
147+
x[o + 1] = t74;
148+
x[o + 2] = t42;
149+
x[o + 3] = t75;
150+
x[o + 4] = t32;
151+
x[o + 5] = t76;
152+
x[o + 6] = t43;
153+
x[o + 7] = t77;
154+
x[o + 8] = t29;
155+
x[o + 9] = t78;
156+
x[o + 10] = t44;
157+
x[o + 11] = t79;
158+
x[o + 12] = t31;
159+
x[o + 13] = t80;
160+
x[o + 14] = t40;
161+
x[o + 15] = t69;
162+
}

‎src/codecs/av1/fdct.ts‎

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66
import { TXFM_DIMENSIONS } from './tables.ts';
77
import { inverseBasis, transformTypes1D, passShift } from './itx.ts';
8+
import { dct4, dct8, dct16 } from './fdct-lee.ts';
89

910
// the pseudo inverse (B^T B)^-1 B^T of a basis of n x k values: k x n
1011
function pseudoInverse(b: Float64Array, n: number, k: number): Float64Array {
@@ -62,19 +63,16 @@ const tmp = new Float64Array(64 * 32);
6263

6364
// the unnormalized DCT-II (X_k = sum of x_i cos(pi (2i + 1) k / 2n)) of n values at x[o] (step 1) in place, by Lee's
6465
// recursion (scratch: n values)
65-
// (1 / (2 cos(pi (2i + 1) / 2n)) for n = 2-64, at n / 2 + i)
66+
// (1 / (2 cos(pi (2i + 1) / 2n)) for n = 32 and 64, at n / 2 + i)
6667
const TWIDDLES = new Float64Array(64);
67-
for (let n = 2; n <= 64; n *= 2) for (let i = 0; i < n / 2; i++) TWIDDLES[n / 2 + i] = 1 / (2 * Math.cos(Math.PI * (2 * i + 1) / (2 * n)));
68+
for (let n = 32; n <= 64; n *= 2) for (let i = 0; i < n / 2; i++) TWIDDLES[n / 2 + i] = 1 / (2 * Math.cos(Math.PI * (2 * i + 1) / (2 * n)));
6869

6970
function dct(x: Float64Array, o: number, n: number, scratch: Float64Array, so: number): void {
70-
if (n === 1) return;
71+
// (up to 16: unrolled)
72+
if (n === 4) return dct4(x, o);
73+
if (n === 8) return dct8(x, o);
74+
if (n === 16) return dct16(x, o);
7175
const half = n / 2;
72-
if (n === 2) {
73-
const a = x[o], b = x[o + 1];
74-
x[o] = a + b;
75-
x[o + 1] = (a - b) * TWIDDLES[1];
76-
return;
77-
}
7876
for (let i = 0; i < half; i++) {
7977
const a = x[o + i], b = x[o + n - 1 - i];
8078
scratch[so + i] = a + b;
@@ -113,11 +111,22 @@ function forward1D(log2: number, type: number, inp: Float64Array, io: number, st
113111
for (let j = 0; j < k; j++) out[oo + j * outStep] = line[j] * s[j];
114112
return;
115113
}
114+
// (four coefficients at a time)
116115
const p = projection(log2, type);
117-
for (let j = 0; j < k; j++) {
118-
let sum = 0;
119-
for (let i = 0; i < n; i++) sum += p[j * n + i] * inp[io + i * step];
120-
out[oo + j * outStep] = sum;
116+
for (let i = 0; i < n; i++) line[i] = inp[io + i * step];
117+
for (let j = 0; j < k; j += 4) {
118+
let s0 = 0, s1 = 0, s2 = 0, s3 = 0;
119+
for (let i = 0, r = j * n; i < n; i++, r++) {
120+
const v = line[i];
121+
s0 += p[r] * v;
122+
s1 += p[r + n] * v;
123+
s2 += p[r + 2 * n] * v;
124+
s3 += p[r + 3 * n] * v;
125+
}
126+
out[oo + j * outStep] = s0;
127+
out[oo + (j + 1) * outStep] = s1;
128+
out[oo + (j + 2) * outStep] = s2;
129+
out[oo + (j + 3) * outStep] = s3;
121130
}
122131
}
123132

‎src/codecs/avif-enc.ts‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,14 @@ function toYUV(rgba: Uint8Array, width: number, height: number, subsampled: bool
4040
v[i] = (r - l) / (2 * (1 - KR)) + 128;
4141
}
4242
const byte = (x: number): number => x < 0 ? 0 : x > 255 ? 255 : Math.round(x);
43-
if (!subsampled) return [y, Uint8Array.from(u, byte), Uint8Array.from(v, byte)];
43+
if (!subsampled) {
44+
const cu = new Uint8Array(n), cv = new Uint8Array(n);
45+
for (let i = 0; i < n; i++) {
46+
cu[i] = byte(u[i]);
47+
cv[i] = byte(v[i]);
48+
}
49+
return [y, cu, cv];
50+
}
4451
// (the average of each 2x2 block)
4552
const cw = width + 1 >> 1, ch = height + 1 >> 1, cu = new Uint8Array(cw * ch), cv = new Uint8Array(cw * ch);
4653
for (let cy = 0; cy < ch; cy++) {

0 commit comments

Comments
 (0)