Skip to content

Commit 343825d

Browse files
matmenclaude
andcommitted
Lossy WebP encoder: skip modes that can't win earlier
The rate-distortion score only grows with each term, so a mode whose header bits (intra 4x4) or distortion (intra 16x16, chroma) already reach the best score is dropped before the reconstruction, texture distortion or rate is computed. Quantized intra 4x4 blocks without levels are the prediction, without an inverse transform; tTransform keeps its sums in locals. 23-28% faster at effort 3-6, output unchanged (140 hashes over 5 images x efforts 0-6 x 4 qualities, cwebp tests). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lfv41PqHEGbU7SLg5onpG1
1 parent ee517f2 commit 343825d

3 files changed

Lines changed: 59 additions & 28 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@
1515
- AVIF encoding with `effort: 2` also chooses smaller transforms within blocks (transform depth): files about 1% smaller
1616
(up to 4% for graphics), encoding about 10% slower.
1717

18+
### Performance
19+
20+
- Lossy WebP encoding with `effort` 3 to 6 (the default is 4) is about 25% faster: modes that can no longer beat the
21+
best one are dropped before their distortion and rate are computed. The files stay byte for byte what cwebp writes.
22+
1823
## 3.5.0
1924

2025
### Features

‎src/codecs/vp8-enc-dsp.ts‎

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -804,21 +804,28 @@ export function sse16(a: Uint8Array, ao: number, b: Uint8Array, bo: number, h: n
804804
return count;
805805
}
806806

807-
/** The weighted sum of the absolute Hadamard transform coefficients of a 4x4 block */
807+
/** The weighted sum of the absolute Hadamard transform coefficients of a 4x4 block (rows, then columns; unrolled) */
808808
export function tTransform(input: Uint8Array, i: number, w: ArrayLike<number>): number {
809-
let sum = 0;
810-
for (let k = 0; k < 4; k++, i += BPS) {
811-
const a0 = input[i] + input[i + 2], a1 = input[i + 1] + input[i + 3], a2 = input[i + 1] - input[i + 3], a3 = input[i] - input[i + 2];
812-
ftmp[k * 4] = a0 + a1;
813-
ftmp[k * 4 + 1] = a3 + a2;
814-
ftmp[k * 4 + 2] = a3 - a2;
815-
ftmp[k * 4 + 3] = a0 - a1;
816-
}
817-
for (let k = 0; k < 4; k++) {
818-
const a0 = ftmp[k] + ftmp[8 + k], a1 = ftmp[4 + k] + ftmp[12 + k], a2 = ftmp[4 + k] - ftmp[12 + k], a3 = ftmp[k] - ftmp[8 + k];
819-
sum += w[k] * Math.abs(a0 + a1) + w[k + 4] * Math.abs(a3 + a2) + w[k + 8] * Math.abs(a3 - a2) + w[k + 12] * Math.abs(a0 - a1);
820-
}
821-
return sum;
809+
const r00 = input[i], r01 = input[i + 1], r02 = input[i + 2], r03 = input[i + 3];
810+
const a00 = r00 + r02, a01 = r01 + r03, a02 = r01 - r03, a03 = r00 - r02;
811+
const h00 = a00 + a01, h01 = a03 + a02, h02 = a03 - a02, h03 = a00 - a01;
812+
const r10 = input[i + 1 * BPS], r11 = input[i + 1 * BPS + 1], r12 = input[i + 1 * BPS + 2], r13 = input[i + 1 * BPS + 3];
813+
const a10 = r10 + r12, a11 = r11 + r13, a12 = r11 - r13, a13 = r10 - r12;
814+
const h10 = a10 + a11, h11 = a13 + a12, h12 = a13 - a12, h13 = a10 - a11;
815+
const r20 = input[i + 2 * BPS], r21 = input[i + 2 * BPS + 1], r22 = input[i + 2 * BPS + 2], r23 = input[i + 2 * BPS + 3];
816+
const a20 = r20 + r22, a21 = r21 + r23, a22 = r21 - r23, a23 = r20 - r22;
817+
const h20 = a20 + a21, h21 = a23 + a22, h22 = a23 - a22, h23 = a20 - a21;
818+
const r30 = input[i + 3 * BPS], r31 = input[i + 3 * BPS + 1], r32 = input[i + 3 * BPS + 2], r33 = input[i + 3 * BPS + 3];
819+
const a30 = r30 + r32, a31 = r31 + r33, a32 = r31 - r33, a33 = r30 - r32;
820+
const h30 = a30 + a31, h31 = a33 + a32, h32 = a33 - a32, h33 = a30 - a31;
821+
const b00 = h00 + h20, b01 = h10 + h30, b02 = h10 - h30, b03 = h00 - h20;
822+
const b10 = h01 + h21, b11 = h11 + h31, b12 = h11 - h31, b13 = h01 - h21;
823+
const b20 = h02 + h22, b21 = h12 + h32, b22 = h12 - h32, b23 = h02 - h22;
824+
const b30 = h03 + h23, b31 = h13 + h33, b32 = h13 - h33, b33 = h03 - h23;
825+
return w[0] * Math.abs(b00 + b01) + w[4] * Math.abs(b03 + b02) + w[8] * Math.abs(b03 - b02) + w[12] * Math.abs(b00 - b01) +
826+
w[1] * Math.abs(b10 + b11) + w[5] * Math.abs(b13 + b12) + w[9] * Math.abs(b13 - b12) + w[13] * Math.abs(b10 - b11) +
827+
w[2] * Math.abs(b20 + b21) + w[6] * Math.abs(b23 + b22) + w[10] * Math.abs(b23 - b22) + w[14] * Math.abs(b20 - b21) +
828+
w[3] * Math.abs(b30 + b31) + w[7] * Math.abs(b33 + b32) + w[11] * Math.abs(b33 - b32) + w[15] * Math.abs(b30 - b31);
822829
}
823830

824831
/**

‎src/codecs/vp8-enc.ts‎

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1217,7 +1217,17 @@ function reconstructIntra4(it: Iterator, levels: Int16Array, lo: number, src: Ui
12171217
fTransform(src, s, it.yuvP, r, tmpLevels, 0);
12181218
const nz = it.doTrellis ? trellisQuantize(it.enc, tmpLevels, 0, levels, lo, it.topNz[it.i4 & 3] + it.leftNz[it.i4 >> 2], TYPE_I4_AC, dqm.y1, dqm.lambdaTrellisI4)
12191219
: quantizeBlock(tmpLevels, 0, levels, lo, dqm.y1) ? 1 : 0;
1220-
iTransform(it.yuvP, r, tmpLevels, 0, out, o, false);
1220+
// (without levels, the block is the prediction)
1221+
if (nz) iTransform(it.yuvP, r, tmpLevels, 0, out, o, false);
1222+
else {
1223+
const p = it.yuvP;
1224+
for (let k = 0; k < 4 * BPS; k += BPS) {
1225+
out[o + k] = p[r + k];
1226+
out[o + k + 1] = p[r + k + 1];
1227+
out[o + k + 2] = p[r + k + 2];
1228+
out[o + k + 3] = p[r + k + 3];
1229+
}
1230+
}
12211231
return nz;
12221232
}
12231233

@@ -1292,17 +1302,17 @@ function pickBestIntra16(it: Iterator, rd: ModeScore): void {
12921302
rdCur.modeI16 = mode;
12931303
rdCur.nz = reconstructIntra16(it, rdCur, dst, Y_OFF, mode);
12941304
rdCur.D = sse16(it.yuvIn, Y_OFF, dst, Y_OFF, 16);
1295-
rdCur.SD = tlambda ? (tlambda * tDisto16x16(sourceSpectra, dst, Y_OFF, WEIGHT_Y) + 128) >> 8 : 0;
12961305
rdCur.H = FIXED_COSTS_I16[mode];
1306+
// a very flat block: emphasis on low distortion
1307+
if (flat) flat = isFlat(rdCur.yAcLevels, 0, 16, FLATNESS_LIMIT_I16);
1308+
if (flat) rdCur.D *= 2;
1309+
// (modes that can't beat the best without texture distortion and rate are skipped before them)
1310+
rdCur.SD = rdCur.R = 0;
1311+
rdCur.setRDScore(lambda);
1312+
if (mode > 0 && rdCur.score >= rdBest.score) continue;
1313+
rdCur.SD = tlambda ? (tlambda * tDisto16x16(sourceSpectra, dst, Y_OFF, WEIGHT_Y) + 128) >> 8 : 0;
1314+
if (flat) rdCur.SD *= 2;
12971315
rdCur.R = costLuma16(it, rdCur);
1298-
if (flat) {
1299-
// a very flat block: emphasis on low distortion
1300-
flat = isFlat(rdCur.yAcLevels, 0, 16, FLATNESS_LIMIT_I16);
1301-
if (flat) {
1302-
rdCur.D *= 2;
1303-
rdCur.SD *= 2;
1304-
}
1305-
}
13061316
rdCur.setRDScore(lambda);
13071317
if (mode === 0 || rdCur.score < rdBest.score) {
13081318
const swap = rdCur;
@@ -1353,12 +1363,18 @@ function pickBestIntra4(it: Iterator, rd: ModeScore): boolean {
13531363
const spectrum = tlambda ? tTransform(it.yuvIn, src, WEIGHT_Y) : 0;
13541364
for (let mode = 0; mode < 10; mode++) {
13551365
const r = rdTmpI4;
1366+
// (the score only grows from the header bits on: modes that can't beat the best are skipped as early as
1367+
// possible, which doesn't change the choice)
1368+
r.H = costs[modeCosts + mode];
1369+
if (bestMode >= 0 && r.H * lambda >= rdI4.score) continue;
13561370
r.nz = reconstructIntra4(it, tmpLevels2, 0, it.yuvIn, src, tmpDst, td, mode) << it.i4;
13571371
r.D = sse4x4(it.yuvIn, src, tmpDst, td);
1358-
r.SD = tlambda ? (tlambda * tDisto4x4(spectrum, tmpDst, td, WEIGHT_Y) + 128) >> 8 : 0;
1359-
r.H = costs[modeCosts + mode];
13601372
// flat areas shouldn't be predicted by complex modes
13611373
r.R = mode > 0 && isFlat(tmpLevels2, 0, 1, FLATNESS_LIMIT_I4) ? FLATNESS_PENALTY : 0;
1374+
r.SD = 0;
1375+
r.setRDScore(lambda);
1376+
if (bestMode >= 0 && r.score >= rdI4.score) continue;
1377+
r.SD = tlambda ? (tlambda * tDisto4x4(spectrum, tmpDst, td, WEIGHT_Y) + 128) >> 8 : 0;
13621378
r.setRDScore(lambda);
13631379
if (bestMode >= 0 && r.score >= rdI4.score) continue;
13641380
r.R += costLuma4(it, tmpLevels2);
@@ -1412,8 +1428,11 @@ function pickBestUv(it: Iterator, rd: ModeScore): void {
14121428
r.D = sse16(it.yuvIn, U_OFF, tmpDst, U_OFF, 8);
14131429
r.SD = 0;
14141430
r.H = FIXED_COSTS_UV[mode];
1415-
r.R = costUv(it, r);
1416-
if (mode > 0 && isFlat(r.uvLevels, 0, 8, FLATNESS_LIMIT_UV)) r.R += FLATNESS_PENALTY * 8;
1431+
r.R = mode > 0 && isFlat(r.uvLevels, 0, 8, FLATNESS_LIMIT_UV) ? FLATNESS_PENALTY * 8 : 0;
1432+
// (modes that can't beat the best without their rate are skipped before it)
1433+
r.setRDScore(lambda);
1434+
if (mode > 0 && r.score >= rdBestUv.score) continue;
1435+
r.R += costUv(it, r);
14171436
r.setRDScore(lambda);
14181437
if (mode === 0 || r.score < rdBestUv.score) {
14191438
rdBestUv.copyScore(r);

0 commit comments

Comments
 (0)