Skip to content

Commit 8d5436e

Browse files
matmenclaude
andcommitted
WebP: lossy decoding ~45% faster, same pixels
BoolDecoder.tree() returned -i for the leaf, which is -0 for DC_PRED and B_DC_PRED: a heap number, not an int32. Every macroblock with DC prediction deoptimized decode() (bailout "not int32" at IMPLIED_BMODE[ymode]); --trace-opt showed it recompiled by Maglev ~140 times per run, never reaching TurboFan. Returning -i | 0 fixes it. Also: blocks with only a DC coefficient add (dc + 4) >> 3 instead of running the full inverse DCT (libwebp's TransformDC; the same result), and the 16x16/8x8 predictions fill rows by loops instead of fill and copyWithin calls. 1920x1080 q75: 184 -> 100 ms CPU (node --single-threaded), q30 -> 92 ms. 305 decodes (cwebp variants of 39 images: filters, sharpness, segments, sharp YUV, alpha; libwebp samples) identical to before. A --trace-deopt scan over the other codecs found no other deopt loops. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BsU2GAqbnFSNaYDBGD1Km2
1 parent 4730f65 commit 8d5436e

2 files changed

Lines changed: 36 additions & 9 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## 3.12.3
4+
5+
### Performance
6+
7+
- Lossy WebP decoding is about 45% faster (1920x1080 at quality 75: 184 -> 100 ms of CPU time): reading a DC
8+
prediction mode returned -0, a floating point number to V8, which kept deoptimizing the decoder's main loop. Blocks
9+
with only a DC coefficient also skip the full inverse transform. Same pixels.
10+
311
## 3.12.2
412

513
### Changes

‎src/codecs/vp8.ts‎

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,8 @@ class BoolDecoder {
7575
tree(tree: Int8Array, probs: ArrayLike<number>, offset = 0): number {
7676
let i = 0;
7777
while ((i = tree[i + this.read(probs[offset + (i >> 1)])]) > 0);
78-
return -i;
78+
// (| 0: the leaf 0 would be -0, a double, which keeps deoptimizing the callers)
79+
return -i | 0;
7980
}
8081

8182
/** whether the decoder read beyond its data */
@@ -283,6 +284,7 @@ export function decode(data: Uint8Array, maxPixels = MAX_PIXELS): { width: numbe
283284
const an = mbx * 9; // above non-zero flags: 0-3 Y, 4-5 U, 6-7 V, 8 Y2
284285
coeffs.fill(0);
285286
let nonZero = 0; // bit per block (0-15 Y, 16-23 U/V) with a non-zero coefficient, 24: Y2 AC
287+
let ac = 0; // bit per block with AC coefficients (the others only add their DC's share to the prediction)
286288

287289
if (!skip) {
288290
const q = h.quant[segment];
@@ -300,6 +302,7 @@ export function decode(data: Uint8Array, maxPixels = MAX_PIXELS): { width: numbe
300302
const n = readCoefficients(tokens, h.coeffProbs, type, ctx, first, q[0], q[1], coeffs, b * 16);
301303
aboveNz[an + x] = leftNz[y] = n > first ? 1 : 0;
302304
if (n > first || coeffs[b * 16] !== 0) nonZero |= 1 << b;
305+
if (n > 1) ac |= 1 << b;
303306
}
304307

305308
for (let b = 16; b < 24; b++) {
@@ -308,6 +311,7 @@ export function decode(data: Uint8Array, maxPixels = MAX_PIXELS): { width: numbe
308311
const n = readCoefficients(tokens, h.coeffProbs, TYPE_UV, ctx, 0, q[4], q[5], coeffs, b * 16);
309312
aboveNz[an + plane + x] = leftNz[plane + y] = n > 0 ? 1 : 0;
310313
if (n > 0) nonZero |= 1 << b;
314+
if (n > 1) ac |= 1 << b;
311315
}
312316
} else {
313317
// a skipped macroblock has no coefficients, its contexts are cleared (Y2's only if it has one)
@@ -335,12 +339,12 @@ export function decode(data: Uint8Array, maxPixels = MAX_PIXELS): { width: numbe
335339
}
336340
for (let i = 0; i < 4; i++) left[i] = Y[base + i * ys - 1];
337341
predictSubblock(bmodes[b], edge, left, Y, base, ys);
338-
if (nonZero >> b & 1) inverseDCT(coeffs, b * 16, Y, base, ys);
342+
if (nonZero >> b & 1) inverseDCT(coeffs, b * 16, Y, base, ys, ac >> b & 1);
339343
}
340344
} else {
341345
predictBlock(ymode, Y, yBase, ys, 16, mbx, mby);
342346
for (let b = 0; b < 16; b++)
343-
if (nonZero >> b & 1) inverseDCT(coeffs, b * 16, Y, yBase + (b >> 2) * 4 * ys + (b & 3) * 4, ys);
347+
if (nonZero >> b & 1) inverseDCT(coeffs, b * 16, Y, yBase + (b >> 2) * 4 * ys + (b & 3) * 4, ys, ac >> b & 1);
344348
}
345349

346350
const cBase = (mby * 8 + 1) * cs + mbx * 8 + 1;
@@ -349,7 +353,7 @@ export function decode(data: Uint8Array, maxPixels = MAX_PIXELS): { width: numbe
349353
for (let b = 16; b < 24; b++) {
350354
if (!(nonZero >> b & 1)) continue;
351355
const plane = b < 20 ? U : V, i = b & 3;
352-
inverseDCT(coeffs, b * 16, plane, cBase + (i >> 1) * 4 * cs + (i & 1) * 4, cs);
356+
inverseDCT(coeffs, b * 16, plane, cBase + (i >> 1) * 4 * cs + (i & 1) * 4, cs, ac >> b & 1);
353357
}
354358
}
355359

@@ -438,7 +442,19 @@ function inverseWHT(c: Int16Array, offset: number, count: number): void {
438442
// inverse DCT (RFC 6386 section 14.4), added to the prediction in place
439443
const tmp = new Int32Array(16);
440444

441-
function inverseDCT(c: Int16Array, offset: number, dst: Uint8Array, base: number, stride: number): void {
445+
function inverseDCT(c: Int16Array, offset: number, dst: Uint8Array, base: number, stride: number, ac: number): void {
446+
// (a DC alone adds the same to every pixel, what the full transform gives too)
447+
if (!ac) {
448+
const dc = c[offset] + 4 >> 3;
449+
for (let o = base, end = base + 4 * stride; o < end; o += stride) {
450+
dst[o] = clamp255(dst[o] + dc);
451+
dst[o + 1] = clamp255(dst[o + 1] + dc);
452+
dst[o + 2] = clamp255(dst[o + 2] + dc);
453+
dst[o + 3] = clamp255(dst[o + 3] + dc);
454+
}
455+
return;
456+
}
457+
442458
for (let i = 0; i < 4; i++) {
443459
const i0 = c[offset + i], i4 = c[offset + 4 + i], i8 = c[offset + 8 + i], i12 = c[offset + 12 + i];
444460
const a1 = i0 + i8, b1 = i0 - i8;
@@ -485,13 +501,16 @@ function predictBlock(mode: number, p: Uint8Array, base: number, stride: number,
485501
count++;
486502
}
487503
const v = count === 0 ? 128 : sum + (1 << shift + count - 1) >> shift + count;
488-
for (let y = 0; y < size; y++) p.fill(v, base + y * stride, base + y * stride + size);
504+
// (rows by loops: for 8 or 16 bytes, fill and copyWithin cost more than they do)
505+
for (let y = 0; y < size; y++)
506+
for (let row = base + y * stride, x = 0; x < size; x++) p[row + x] = v;
489507
} else if (mode === V_PRED) {
490-
for (let y = 0; y < size; y++) p.copyWithin(base + y * stride, above, above + size);
508+
for (let y = 0; y < size; y++)
509+
for (let row = base + y * stride, x = 0; x < size; x++) p[row + x] = p[above + x];
491510
} else if (mode === H_PRED) {
492511
for (let y = 0; y < size; y++) {
493-
const row = base + y * stride;
494-
p.fill(p[row - 1], row, row + size);
512+
const row = base + y * stride, v = p[row - 1];
513+
for (let x = 0; x < size; x++) p[row + x] = v;
495514
}
496515
} else {
497516
const corner = p[above - 1];

0 commit comments

Comments
 (0)