Repository navigation
Releases
v0.14.0
What's Changed
- README and NOTICE: the header validation and fuzzing (#36, v0.13.8) by @tannevaled in #37
- CI: qemufix, which the fleet sweep did not reach (this repo is a fork) by @tannevaled in #38
- security: a sample ceiling; a pixel limit does not bound this decoder by @tannevaled in #40
Full Changelog: v0.13.8...v0.14.0
v0.13.8
v0.13.7
v0.13.6
v0.13.5
v0.13.4
Release backfilled on 2026-10-26; tag created 2026-10-18.
Commits since v0.13.3:
v0.8.0 — the entropy coder was the wall, not the transform
The bottleneck on a scanned page is the entropy coder, not the wavelet
§24 of the conformance baseline named ia-biodiversity as the population where a
typical page ties with poppler — median 1.004×, slower on 128 of 250. Profiling its
worst page put the wavelet nowhere near the top:
DecodeCodeBlock |
1.03 s | 37.1% |
↳ significancePropagationPass |
0.47 s | 16.9% |
↳ cleanupPass |
0.36 s | 13.0% |
And the largest single line inside the significance pass is not the arithmetic
decoder — it is computing the context from eight neighbours (100 ms against the
decoder's 90 ms).
e.state is [][]uint8, so each e.state[y][x] costs two bounds checks and a
pointer load, and countSigNeighbors does eight per coefficient. The rows are now
bound once and indexed; the same reads happen in the same order.
Measured
Three rounds of min-of-4 per binary, all in one interleaved run:
| document | before | after | |
|---|---|---|---|
jahresberichtder710meye.pdf |
801.7 ms | 773.5 ms | −3.5% |
bulletin43peab.pdf |
663.9 ms | 636.4 ms | −4.1% |
b31820797.pdf (wavelet-heavy) |
+0.5%, noise |
That last row is the point: the change helps where the mechanism applies and
nowhere else.
Unchanged output
All 3 215 corpus documents render byte-identical to v0.7.0.
v0.7.0 — the movement was the cost, not the arithmetic
Two passes folded away, bit for bit
Synthesize2D_97_WithDims is 48% of the heaviest page in the corpus, and the
arithmetic is the small part of it:
1.12 s flat in Synthesize2D_97_WithDims |
the gather and scatter |
0.45 s liftstep97_c_f64_neon |
the lifting, NEON assembly |
0.40 s BaseInterleave_fallback_Float64 |
the interleave, pure Go |
0.30 s runtime.memmove |
the two copies |
0.23 s scaleslice_c_f64_neon |
the scaling, NEON assembly |
1.82 s of moving data against 0.68 s of computing it. Two passes carry no
arithmetic of their own and are now folded into the passes either side: the
scaling into the read-out of each subband (a copy then *= K is the same
single multiply as = data[i] * K), and the interleave, in the vertical pass,
into the scatter.
Measured
min-of-5, two interleaved rounds, on the three heaviest JPEG 2000 pages, against
a ±1% noise floor:
| document | before | after | |
|---|---|---|---|
b31820797.pdf |
786.3 ms | 721.2 ms | −7.7% |
bulletinchicagov48nchicg.pdf |
657.7 ms | 636.7 ms | −4.4% |
irrigationrequir1340fort.pdf |
404.7 ms | 388.5 ms | −3.4% |
Unchanged output
All 3 215 corpus documents render byte-identical. TestFewerPassesIsBitIdentical
compares the new synthesis against the shape it had before in exact float64
bits, over twelve geometries including odd sizes and both parities — and it runs
on all six architectures, so the fold is exact on big-endian s390x and on 32-bit
too, where the lifting has no assembly path at all.
Two things measured and NOT done
- A fused kernel in scalar Go (one call over six passes instead of seven calls
over nine) is 1.5× to 1.8× slower. The library's lifting is NEON assembly
with FMA and losing it costs more than the passes save. The same FMA makes that
path unreproducible in Go: the identical expressions differ by 1 to 16 ulp. BaseInterleaveruns the pure-Go fallback on arm64 — the generated NEON
variant exists and is never assigned. Benchmarked head to head, it makes no
difference: both run at 25–28 GB/s, which is memory bandwidth. An interleave is
pure movement; the answer is to remove the pass, not vectorise it.
Also takes go-highway v0.0.12, eight versions newer than what this package was
pinned to. Nothing was updating it.