# Experiments Index



Navigation map for `PERF_EXPERIMENTS.md`. Read this first; open the full entry only when you need numbers or code. Updated: 2026-07-06.

## Legend

Status: **K** = Kept · **R** = Reverted · **X** = Rejected · **D** = Diagnostic/infra · **F** = Needs follow-up

---

## Experiment Table

| ID | Date | Component | Status | Effect | Notes / Related |
|----|------|-----------|--------|--------|-----------------|
| ZP_U64 | 2026-07-06 | ZP decoder (djvu-zp/jb2/iw44/bzz, `bit_buf` width) | **R** | no reproducible ≥3% win on either arch: M1 5-pair ratios span and cross zero (sign flips by pair 5); x86 CI same-code replay proved an 11.7% control-only noise spread and a 33-point swing on one bench from identical code | Reopened round-47's `X86_CI_BENCH` lead — widened `bit_buf` u32→u64 for bulk 4-byte refill in all 4 inline `refill!` sites, byte-exact throughout (556-page corpus digest identical at every stage, `pos`/EOF-padding semantics preserved, `ZP_EOF_SLACK_BYTES=16` margin intact). Two-stage rollout (iw44-only, then unified) both fully byte-exact and both fully reverted — decision hinged entirely on the honest multi-sample benchmark read, not correctness. Round-55 |
| IW44_ENTROPY_PROBE | 2026-07-06 | IW44 encoder (size, diagnostic) | **D** | localizes Round-52's gap to band 0/DC on small pages (`corr(bytes,ratio)=−0.591` across 62 colorbook pages, worst page 1.251×); best lever (chunking) only −0.04 to −1.7%, below the 3% bar | Instrumented `PlaneEncoder` with a thread-local, decoder-unchanged `iw44-probe` feature (`djvu_iw44::encode::probe`: per-band bytes + block-band/bucket-NEW/activate/refine call-true counts), guarded by a `probe_does_not_change_output` regression test. `examples/iw44_band_probe.rs` found watchmaker (smooth) is 98.6–99.4% band-0 at ~parity, while colorbook's worst-ratio pages (61, 2, 59, 1, 60) are >79% band-0 vs its best-ratio pages (35, 36, 25, 21, 54) at ≥55% mid+fine share — reframing round-22's "textured" label as "small/DC-dominated," not "high-frequency." Candidate (a) band-dependent activation threshold: already fully closed by IW44_ACT_THRESH (round pre-5), zero slack confirmed. Candidate (b) coefficient pre-quantization: inherently PSNR-coupled (forward DWT is a tested exact i32 mirror), out of scope for a zero-PSNR probe, not attempted. Candidate (c) chunk/slice boundary layout (`slices_per_chunk`): tested {10,25,50,74,99} via `examples/iw44_chunking_lever.rs` (ddjvu-validated, 3 full pages, 0 maxΔpx) and `examples/iw44_chunking_sweep.rs` (96 real BG44 pages across 3 corpora, bit-identical decode asserted) — real but small savings (0.04–1.7%, best on small smooth pages), non-monotonic, interop-safe, left as an existing option (`Iw44EncodeOptions::slices_per_chunk`), not a new default. Root cause of the residual band-0 gap (likely encoder-vs-DjVuLibre coefficient-value differences in the forward wavelet transform) not validated — no DjVuLibre C++ source available locally to diff. Round-51 |
| TXTZ_OCR | 2026-07-06 | encode (`PageEncoder`, opt-in text layer) | **K** (opt-in) | +2,682 B (2.62 KB) TXTz cost on watchmaker.djvu page 0 (2550×3301); 242 word zones round-trip with sane coords | New `PageEncoder::with_text_layer`/`with_ocr_text_layer` (`src/djvu_encode.rs`) — runs an `OcrBackend` on the page image during encode and appends a BZZ-compressed `TXTz` chunk; default omits it entirely (byte-identical, tested). Reused, not rebuilt: `OcrBackend::recognize` already returned the full page→line→word zone tree (PR #125), `text_encode::encode_text_layer`/`bzz_encode` already serialized it (used by PR #315's post-hoc `djvu ocr --output` mutation path) — the actual gap was `PageEncoder` having no text-layer awareness at all. New `bitmap_to_pixmap` expands bilevel sources for OCR (mirrors `examples/ocr_qa.rs`'s `mask_to_pixmap`). Real Tesseract 5.5.2 run on `watchmaker.djvu`: OCR text reads correctly, decodes back through `DjVuDocument::text_layer()`, and exports to a `/Font`-bearing PDF via the existing `pdf_with_text_layer` path with no changes. New `examples/txtz_ocr_demo.rs` + 6 unit tests. Round-51 |
| PDF_G4 | 2026-07-06 | PDF export (bilevel mask encoding, opt-in) | **K** (opt-in) | mask-only: 1.6–2.31× smaller on 3/4 corpus docs (wash on halftone-heavy content); whole-file up to −43.1% combined with `adaptive_raster` | Round-4-deferred lever: JB2 masks re-encoded as generic Deflate raster in PDF export, never exploiting row-to-row redundancy. New `smmr::encode_g4` — full T.6 2D encoder (pass/vertical/horizontal modes, changing-element arrays + `partition_point` binary search) producing PDF-`CCITTFaxDecode`-ready payloads; wired into `pdf::collect_mask_stream` behind `PdfOptions::ccitt_g4` (default off, byte-identical), "encode both keep smaller" pattern like round-28's `adaptive_raster`. First attempt reusing the existing H-mode-only `encode_smmr` was 2–4× *larger* than Deflate — the whole win needs real vertical/pass mode, hence new code. **Found and fixed a real, pre-existing T.6 spec bug** while validating externally (poppler `pdftoppm`, libtiff via Pillow): `find_b1` (decoder, used for production `Smmr`-chunk decoding too) and the new `find_transition` (encoder) searched the reference line's next changing element *inclusive* of the current coding position `a0` instead of strictly after it — spec-equivalent only for a row's first element, wrong on every later step, which is exactly when vertical/pass mode matter. Both sides shared the bug symmetrically, so round-tripping through this crate's own decoder never caught it; a previous code-review comment had flagged and reverted an `a0+1` fix because it broke a hand-crafted decoder test whose *expected* bitstream had itself been hand-reasoned under the same wrong assumption. Fixed via a uniform `i64` sentinel-`-1` convention in both `find_b1`/`decode_row_pixels` and `find_transition`/`encode_row_2d`; rewrote the one broken hand-crafted test with a non-colliding case, bitstream generated via `encode_g4` instead of hand bit-twiddling. Re-validated: 7/7 synthetic bitmaps byte-identical via libtiff (previously 6/7); zero CCITTFax syntax errors and pixel-identical (`compare -metric AE` → 0) rasterization across all pages of all 4 target corpus PDFs via `pdftoppm` (previously dozens of syntax errors per file). 51 `smmr` + 5 new `pdf` tests; full `cargo test --lib` (723) and `make check` green. Round-53 |
| INTEROP_STREAMS | 2026-07-06 | IW44/JB2 stream handling (interop vs DjVuLibre) | **Fixed** (2a, 2b-serial) / **Benign, documented** (2b-swallow, 4) | fuzz seed 42: `our-renders-what-they-reject` 310→241 (−69, moved to `both-render-fail`); 0 new crashes | Round-50 DIFF_FUZZ follow-up on findings 2 & 4 (findings 1/3 = separate INFO_VERSION effort, out of scope). Added `Iw44Error::UnexpectedSerial` + `next_serial` continuity check in `Iw44Image::decode_chunk` (`crates/djvu-iw44/src/lib.rs`), mirroring DjVuLibre's `cserial`/`wrong_serial` check — round-26 BUG-ZPSHORT empty-payload tolerance for in-order chunks confirmed untouched. Added `iw44_reduction_is_legal` INFO-vs-BG44 dimension cross-check (`src/djvu_render.rs`, mirrors `DjVuFile::get_dpi`'s 1:1..1:12 reduction-ratio rule) wired into `PageLayers::bg44`/`bg44_partial`/bounded decode branch; verified against the exact round-45 `boy.djvu` 192×384-vs-192×256 case (real `ddjvu`: "Cannot decode page 1", exit 10; us pre-fix: silent exit 0, post-fix: `IW44 decode error`, exit 1). `PageLayers::bg44`'s unconditional swallow-on-error partial-decode kept as-is — pixel-diffed against clean baselines (max diff 5–26/255, <0.3% pixels) confirms graceful degradation, not garbage. JB2 "unknown record type" (finding 4) root-caused: DjVuLibre's `JB2Image.cpp` throws the identical `bad_type` hard error on out-of-range record types (fetched source, confirmed same `CodeNum(0,11,…)` construction) — no handling gap; pixel-diffed `ddjvu`'s own render on 2 repros against clean baselines (0.5%/7.6% pixels differing >10/255) proves the divergence is post-corruption arithmetic-coder chaos, not a spec gap — unfixable by alignment, recorded benign. 5 new tests. Round-46 |
| C5_COMPRESS | 2026-07-06 | memory (render cache, opt-in) | **K** (opt-in) | **~23% faster** sub=2/thumbnail re-render after eviction; same byte ceiling either way | New opt-in middle eviction tier: `PageLayers::downgrade` clears `bg44`/`bg44_partial`/mask layers/`bg_rgb_s1`/tile cache but **preserves** already-cached `bg_rgb_s2`/`bg_rgb_s4`; `decode_background_chunks` checks the downscaled cache before forcing a decode. `DjVuDocument::enforce_cache_budget_with(budget, protect, CacheBudgetOptions{downgrade_before_drop})` — two-pass sweep (downgrade LRU-first, fall back to full drop if still over budget); default `false` = byte-identical to `enforce_cache_budget`. Design B (DEFLATE compress-on-evict via `miniz_oxide`) **rejected**: round-trip 32–260 ms vs 10–58 ms cold decode. Ruled out: keeping `bg44` coefficients doesn't save memory vs the RGB pixmap (`PlaneDecoder` allocates full-size on chunk 0). `examples/c5_compress_bench.rs`. Round-48 |
| PY_CI | 2026-07-06 | djvu-py (test/CI infra) | **K** (infra) | 41 pytest tests, all green (~3.3s); dedicated GIL-release timing test measured 5.9× speedup (gate at lenient 1.3×) | New `djvu-py/tests/` (conftest + 5 modules: document open/from_bytes/errors, render dims/non-blank/DPI-scale, text extraction, round-41's buffer protocol/zero-copy/lifetime safety, GIL-release threading) + `djvu-py/Makefile` (`py-venv`/`py-build`/`py-test`, `uv`-managed venv + `maturin develop --release`). New non-required `djvu-py` job in `.github/workflows/ci.yml` (ubuntu-latest, one Python version, cached cargo) — wired like `OCR (tesseract)`/`Coverage`: not in the branch-protection required-checks list. Follow-up from round-41 (PY_ZEROCOPY_BUFFER/PY_GIL_DETACH, #530), which shipped with only ad hoc scratchpad scripts. Round-46 |
| TH44_GRID | 2026-07-06 | render (thumbnail grid, opt-in) | **K** (opt-in) | ~1.9–14.9× (colour), ~4.1× (bilevel); SSIM 0.36–0.63 confirms D5's 0.50–0.68 at 128px | New `Document::thumbnails`/`thumbnails_with_strategy` + `ThumbnailStrategy::{Auto,Th44Only,RenderOnly}` (`src/lib.rs`), parallel under `parallel` feature. Builds the grid API D5_TH44_PREVIEW called for: `Auto` prefers embedded `TH44`, falls back to a byte-identical real render when absent. Structural test corrupts every `Sjbz` payload in a synthesized bundle to prove `Th44Only`/`Auto` never touch JB2/BG44 decode (`RenderOnly` fails on the corrupted data, `Th44Only`/`Auto` don't). No corpus file embeds `TH44` (per D5) — benched/measured on bundles synthesized with the #476 thumbnail encoder. Round-44 |
| OCR_QA | 2026-07-06 | JB2 lossy levers (quality infra) | **K** (infra) | `lossy_text()` (2%) OCR-invisible (100/100% char/word agree) on both corpora; SSIM and OCR **disagree past that point in both directions** | New `examples/ocr_qa.rs`: OCRs (Tesseract, via the existing `OcrBackend` seam) the lossless vs. lossy JB2-mask decode at the same operating points as `jb2_lossy_b0`/`jb2_despeckle` (`lossy_threshold`∈{2,5,8,10}%, `despeckle=8`), scores Levenshtein char/word agreement (lossy-OCR vs lossless-OCR, not ground truth). Real Tesseract run (macOS, tesseract 5.5.2+leptonica 1.87.0): scan corpus `lossy 8%` already shows a real SSIM dip (0.9956) at **100% OCR agreement** (SSIM over-warns); text corpus `lossy 8/10%` shows word-agreement (99.86/99.58%) falling faster than SSIM (0.9986/0.9985) — OCR is the more sensitive instrument there. Both metrics agree at the shipped 2% preset. Graceful fallback (`n/a` columns) without `ocr-tesseract`/a working Tesseract install; 11 unit tests against a mock `OcrBackend` need neither. No shipped-code change. Round-43 |
| CARTE_INFO_TRUNCATED | 2026-07-06 | djvu-iff / INFO chunk parse | **Fixed** | `carte.djvu` (skipped as "truncated" since Round-42) now parses — root cause was a too-strict 10-byte minimum on `PageInfo::parse`, not a corrupt fixture | `carte.djvu`'s `INFO` chunk is a real-world 5-byte short form (width, height, version byte; no dpi/gamma/flags) — DjVuLibre (`djvudump`/`ddjvu`/`djvused`, all confirmed on this file) tolerates it, defaulting the missing fields; our parser hard-required 10 bytes. `src/info.rs`'s `PageInfo::parse` now requires only 4 bytes (width+height) and defaults dpi=300/gamma=2.2/rotation=None when the chunk ends early. Tests: `carte_style_five_byte_info_parses_with_defaults`, `nine_bytes_parses_dpi_and_gamma_defaults_only_flags`, `djvu_document::tests::parse_carte_with_short_info_chunk`. Note: the file's *rendered pixels* still diverge from ddjvu (separate, pre-existing chroma-noise bug in `djvu-iw44`, pinned since #99) — unrelated to and unblocked by this parse fix. Round-41 |
| MMAP_TEST_THREADLOCAL | 2026-07-06 | test infra (`djvu_render` progressive decode count) | **Fixed (test)** | `progressive_decoder_chunk_decodes_are_on_not_on_squared` failure reported "under `--features mmap`" was actually caused by `parallel`, not `mmap` — bisected and reproduced with `parallel` alone | `decode_layers` runs the naive session's BG44 decode inside `rayon::join` (`#440`/PAR_DEC); calling `join` from a non-worker thread (the test thread) bridges execution onto a rayon *global-pool* worker thread, so the `#[cfg(test)]` `thread_local!` decode counter set/read on the test thread never sees the increments (confirmed via `ThreadId` instrumentation — 3 different worker threads across one test run). Behavior (O(N) vs O(N²) decode counts) was already correct; only the counting mechanism was thread-blind. Fix: run the measurement inside a dedicated single-worker `rayon::ThreadPoolBuilder` pool (`#[cfg(feature = "parallel")]` only) so any bridged `join` resolves on that one worker, keeping counter set/read on one thread without polluting/being polluted by other concurrent tests' decode calls. Verified green under `parallel`, `cli,mmap,parallel`, and default; full suite 693/693 under `cli,mmap,parallel`. Round-42 |
| PY_ZEROCOPY_BUFFER | 2026-07-06 | djvu-py (Python bindings, `Pixmap`) | **K** | `data()`/`to_numpy()`/`to_pil()` copy 23–51 ms per call on a 248 MB page; zero-copy equivalents ~0.001–0.005 ms | `Pixmap::__getbuffer__`/`__releasebuffer__` (flat read-only buffer, pyo3's own tested `test_buffer_protocol.rs` pattern) makes `memoryview(pixmap)`/`bytes(pixmap)`/`numpy.frombuffer(pixmap,...)` work directly with no copy; buffer holds an owned ref so the `Pixmap` outlives any view into it. Additive `to_numpy_zerocopy()`/`to_pil_zerocopy()` convenience wrappers, byte-identical output to the existing (untouched) `to_numpy()`/`to_pil()`. Round-41 |
| PY_GIL_DETACH | 2026-07-06 | djvu-py (Python bindings, render/open/text) | **K** | 2 threads **1.92–1.93x**, 4 threads **3.29–3.91x** page-render speedup | `py.detach()` (pyo3 0.29's renamed `allow_threads`) wraps `Document::open`/`from_bytes` and `Page::render`/`text` — previously ran fully under the GIL. Safe with no `unsafe`: core `Document` is compile-time asserted `Send+Sync` (`src/lib.rs:625`), `Page`/`Pixmap` have no interior mutability. `Document::page()` left alone (confirmed cheap, metadata-only borrow). Measured via 8-page `threading.Thread`-sharded render on `colorbook.djvu`. Round-41 |
| WASM_THREADS | 2026-07-06 | wasm (opt-in threads) | **D-infra** | buildable (nightly + build-std) but +1cores≈noise (native dpi), **9× SLOWER** on downscale-compositor path | New opt-in `wasm-threads` feature (`wasm` + `parallel` + `wasm-bindgen-rayon`) layers a Web Worker rayon pool over the existing PARALLEL/IW44_PAR code paths. Confirmed real, working nightly build (`-Z build-std=panic_abort,std`, `+atomics,+bulk-memory` + `--shared-memory`/`--import-memory`/TLS-export link-args — plain `+atomics` alone throws `DataCloneError` postMessage-ing the Memory) and validated live in an actual Chrome tab (COOP/COEP server) against `colorbook.djvu`. Measured, not simulated: native-dpi full IW44 decode (2260×3669) single 55.8–57.3 ms vs pool=10 57.0–54.8 ms (within noise, no reliable win — only 3-way rayon::join in IW44_PAR, not enough parallel granularity to amortize Worker dispatch); 150-dpi downscale-compositor path single 18.6 ms vs pool=10 **167 ms** (9×slower — many small `par_chunks_exact_mut` chunks, Worker/Atomics sync cost dwarfs the tiny per-chunk work). Default `wasm` build untouched (stable `cargo check --target wasm32-unknown-unknown --features wasm` still green); not part of any required CI gate — see `make wasm-threads-check` |
| C4_TILE_CACHE | 2026-07-06 | render (pan/zoom viewer) | **K** (opt-in) | pan sequences save **~20–25% compositor time** on both colour and bilevel pages at 2×/4× zoom | New `render_region_tiled` (std-only, separate entry point — `render_region` untouched, zero overhead for existing callers): assembles a region from `TILE_SIZE=256` composited RGBA tiles cached per-page (`PageLayers::tile_cache`, FIFO, `TILE_CACHE_MAX_BYTES=8MiB`), folded into C5's `cached_bytes`/`enforce_cache_budget` (no new LRU). Byte-identical by construction (`composite_into` keys off absolute pixel position) + test (`render_region_tiled_matches_render_region` et al). Eligible only for Bilinear + identity rotation + strict decode; else falls back to `render_region`. Justified by VIEWER_BENCH. Round-38 |
| VIEWER_BENCH | 2026-07-06 | render (bench infra) | D (infra) | pan step redoes ~75–100% of viewport work per step with no cache; incremental-strip proxy is 20–25% cheaper | New `benches/viewer.rs`: scripted open→full render→zoom2×→12-step 25%-overlap pan→zoom4×→pan, on a colour page + a bilevel page. `full_recomposite` vs `incremental_strip` (proxy for a tile cache) via existing `render_region`. Whole-sequence totals: colour 2× 5.37→4.25ms (−20.9%), colour 4× 21.89→16.78ms (−23.3%), bilevel 2× 5.46→4.32ms (−20.9%), bilevel 4× 20.90→16.78ms (−19.7%). Motivated C4_TILE_CACHE. Round-38 |
| IW44_RATE_TARGET | 2026-07-06 | IW44 encoder (byte-budget target) | D (already shipped) | `Iw44Target::Bpp` was already merged (#475, 2026-07-01) — dangling branch ref, not abandoned work | Task asked to "resurrect" `feat/iw44-quality-target`, presumed unmerged (no open PR). Found: PR #475 from that exact branch is MERGED (squash `a1e3d54b`, confirmed in `main`'s ancestry); the remote branch ref was just never deleted. `Iw44EncodeOptions::target: Iw44Target::{Slices, Bpp(f32)}` already wired through both `encode_iw44_color`/`encode_iw44_gray`, default `Slices` byte-identical, CLI `--bpp` flag, 6 existing unit tests. The brief's "ideally a PSNR floor" restates IW44_ENTROPY_GAP's (Round-37) own already-recorded verdict — a fixed budget can't capture the 30× smooth-vs-textured saturation spread, a real quality floor needs a content-adaptive stop, "~100B/page, low EV" — so not built (would contradict standing analysis, not a quick win). Added `tests/iw44_rate_target.rs`: determinism, budget-within-one-slice (via caller-set `slices_per_chunk=1`), a real 3-corpus sweep (watchmaker/colorbook/conquete_paix, bpp∈{0.05..1.0}, bytes+PSNR monotone, default-unchanged), and truncated-stream progressive round-trip. No shipped-code change. Round-36 |
| QUALITY_COLOR | 2026-07-06 | quality infra (colour-aware D1) | **K** (infra) | FGBZ_MEDIANCUT k=2 rerun: luma SSIM −0.0056 (looks fine) vs **Cr SSIM −63%** (0.880→0.326), ΔE mean doubles (12.4→25.6) | New `quality::compare_color` (`src/quality.rs`): YCbCr per-channel SSIM (Y/Cb/Cr + 0.8/0.1/0.1-weighted combined) + mean/max CIE76 ΔE, additive to the untouched luma-only `psnr`/`ssim`/`compare`. Closes the round-31 FGBZ_MEDIANCUT honesty caveat ("D1 harness is luma-only, chroma blind") — re-running that experiment's synthetic fixture at k=2/4 shows the new metric catching a colour wash-out luma-SSIM misses entirely; recovers to baseline at k≥6, agreeing with round-31's crop-inspection conclusion. Synthetic isoluminant unit test (same-luma, shifted-hue solid fields) proves the discrimination directly. Wired into `examples/quality_harness.rs` and `examples/fgbz_mediancut_harness.rs` as extra columns. No new deps (std-only f64 math, module already `#[cfg(feature = "std")]`-gated). Round-36 |
| COLD_OPEN_HARNESS | 2026-07-06 | cold-open measurement (infra) | **K** (infra) | cold/warm ratio **4.76x** (page 0, F_NOCACHE fresh-copy strategy) | `examples/cold_open_bench.rs` — unblocks round-8 "B6/B7 need a cold-disk harness". macOS has no `posix_fadvise(DONTNEED)`; copies corpus to a fresh temp path per iteration via an `F_NOCACHE`-flagged fd so the copy's pages aren't cache-resident. `sudo purge` documented as manual cross-check. Median+MAD stats. `--mode validate\|madvise\|prefetch`. Round-40 |
| B6_MADVISE | 2026-07-06 | mmap cold-open (readahead hint) | X (neutral; kept opt-in) | scoped page-range hint: −1.6% (noise); naive whole-prefix hint: **reproducibly −12%** | `MmapDocument::advise_page_willneed` via `memmap2::Mmap::advise_range(WillNeed)` (safe wrapper, no unsafe). Whole-`0..range.end`-prefix version regressed cold open ~12% (over-advising is actively harmful, not just wasted) — corrected to the target page's own FORM range only, which is then a wash: `MmapDocument::open()` already pays the structural DIRM/page-range walk before the hint can be issued, and per-page ranges are small vs fast local NVMe demand-fault latency. Kept as tested opt-in primitive (D/F status, like AVX2_IDWT) — may win on higher-latency storage. Round-40 |
| B7_PREFETCH_PAGE | 2026-07-06 | mmap page-turn latency (background decode) | **K** (opt-in) | **+24–90%** faster next-page render (dwell-dependent); dwell=0 correctly shows no win/regression | `DjVuDocument::prefetch_page(self: &Arc<Self>, idx)` (`parallel` feature): `rayon::spawn`'d background decode into the existing `OnceLock`-backed `PageLayers` cache (no new cache, no unsafe, `OnceLock::get_or_init` dedups vs a later sync render). Page0/dwell150ms: +23.8% and +26.5% (two runs); page100/dwell300ms: +89.7% (heavier baseline decode, more time to hide it); dwell=0 negative control: −0.7% (noise) — honest disclosure that benefit needs dwell ≳ decode time. Respects C5 cache budget (same cache, same eviction). Round-40 |
| X86_CI_BENCH | 2026-07-06 | CI infra (x86 bench gating, `bench.yml`) | **K** (infra) | first real x86 numbers: ZP-decode-only benches **−4 to −28%** faster under `-C target-cpu=x86-64-v3`; IDWT-inclusive `to_rgb` benches **+3 to +14%** regressed | Confirmed the "Benchmark (x86-64-v3 AVX2 validation)" job's PR-SKIPPED badges are by-design (detect job only runs it on PRs touching IW44/SIMD files, per #363), not rot — already `success` on every push to `main`/tags; `workflow_dispatch` was already wired (unused until this round). Widened the job's `iw44_to_rgb\|iw44_decode` bench filter to add `jb2_decode\|bzz_decode` so the ZP-decoder `refill!` hot loop (shared, separately inlined, across jb2/iw44/bzz) is measured on x86 too, alongside the already-covered IDWT path (`to_rgb`→`inverse_wavelet_transform`). Dispatched a real run ([28784274181](https://github.com/matyushkin/djvu-rs/actions/runs/28784274181)): ZP-entropy-only benches (`jb2_decode`, `jb2_decode_corpus_bilevel`, `jb2_decode_large_600dpi`, `iw44_decode_first_chunk`, `iw44_decode_corpus_color`, `bzz_decode`) all get faster under `x86-64-v3` codegen — concrete positive first data point for **ZP-U64** (round-3/4 deferred); `iw44_to_rgb_colorbook` (which runs the actual IDWT row/col pass) instead *regresses* — shows **AVX2_IDWT** (round-5 #8) needs a real hand-written SIMD path, not just a codegen flag, before it can win. Ratio methodology (same runner, back-to-back, default vs `x86-64-v3`, Δ%) already matches this repo's same-run noisy-cloud-runner pattern — documented, unchanged. No source/runtime code touched. Round-47 |
| DIFF_FUZZ | 2026-07-06 | fuzz infra (differential vs DjVuLibre) | **K** (infra) | 2100 mutants, **0 crashes/timeouts**; 5 confirmed semantic-divergence classes (none fixed — no small/obvious fix without spec-level judgment) | New `examples/diff_fuzz.rs` + shared `examples/support/mod.rs` (diff logic deduped out of `interop_pixdiff.rs`): deterministic (splitmix64) corpus-mutation driver (truncate/bitflip/resize-length ops) over 3 corpus files, classifying each mutant on a symmetric 3-level ladder (structural-reject/render-fail/rendered) computed independently for us and DjVuLibre (`djvudump`+`ddjvu`) — catches cases a binary accept/reject compare misses (djvudump structurally accepts but ddjvu's own decoder still refuses). 2100 mutants/153s: both-reject 1266, both-accept-match 429, our-renders-what-they-reject 310, both-render-fail 63, our-laxer 19, our-render-fail 4, our-stricter 4 (verified NOT a bug — we validate outer FORM's declared length upfront, djvudump doesn't), pixel-mismatch 4, dim-mismatch 1. Confirmed findings recorded as follow-ups: INFO version-ceiling not enforced, BG44/IW44 truncated-stream tolerance (+ INFO-vs-payload dimension cross-check gap), INFO minor-version byte affecting rendered pixels on both sides differently, JB2 "unknown record type" divergence (unconfirmed root cause). Repros under `fuzz/corpus-regressions/diff_fuzz/`. Round-45 |
| INTEROP_INFO | 2026-07-06 | djvu-iff / INFO chunk parse (interop) | **Fixed** | DIFF_FUZZ pixel-mismatch 4→0, dim-mismatch 1→0; `our-renders-what-they-reject` −35 (moved to `our-stricter`, matching DjVuLibre's own renderer) | Fixes Round-49 findings 1 & 3. (1) Version ceiling: DjVuLibre's `DJVUVERSION_TOO_NEW=50` (`libdjvu/DjVuInfo.h`, enforced in `DjVuFile::decode_chunk`) wasn't enforced by us — added identical `>=50` rejection (new `IffError::UnsupportedVersion`) in `PageInfo::parse` (`src/info.rs`), version computed the same way DjVuLibre does (minor byte, optionally combined with major byte unless major==0xff sentinel). Round-42's 5-byte short-INFO tolerance unaffected (absent version → 0). (2) Finding 3 reframed: the minor-version byte itself affects nothing pixel-wise on either side; the real driver in 4/5 boy.djvu repros was an **unclamped gamma byte** — DjVuLibre always clamps `0.1*gamma_byte` to `[0.3,5.0]` (even when present-but-zero), we didn't clamp at all. Fixed to match. The 5th repro (dim-mismatch) was a **diff_fuzz harness bug**, not a product bug: `our_attempt()` returned pre-rotation `page.width()/height()` instead of the rendered pixmap's own (post-rotation) dims; fixed in `examples/diff_fuzz.rs`. Incidental: patched a stray non-zero major-version byte in the synthetic `bgjp_test.djvu` fixture (now correctly rejected by the new ceiling, was clearly unintentional). Tests: 7 new in `src/info.rs`. `make check` 1092 passed. Round-47 |
| IW44_SLICE_RD | 2026-07-04 | IW44 encoder (slice budget) | D | default well-tuned; no change | RD curve via D1: past 100 slices PSNR pinned (22.42/39.03 dB), SSIM +≤0.003 while size grows 2–25× (colorbook 100→150 = +1.8 MB for +0.0003 SSIM). total_slices=100 sits at the knee. Refutes IW44_DIAG "starved in 100 slices" — avg_abs=8.72 is the transform's quantization floor, not slice starvation. BG44 lever was BG_DIFFUSE (r17, smaller input), not the slice schedule. Round-35 |
| INTEROP_ENCODE | 2026-07-04 | encoder interop (infra) | **K** (infra) | **21/21 our encodes decode in ddjvu**; real-scan fidelity mean<0.3 | `examples/interop_encode.rs` — encode → ddjvu-decode gate (unblocks masked-wavelet: "ddjvu must accept our stream"). Encode-side complement to interop_pixdiff/diff_djvulibre (decode-side). Gate: non-zero exit if ddjvu rejects. Real colour scans mean|Δ|<0.3 vs ddjvu; degenerate re-encodes (bilevel/photo-as-colour) show mask-AA edge diffs (follow-up). Round-34 |
| LANCZOS_RGBA_ACC | 2026-07-04 | Lanczos-3 resampler | **K** | **−22 to −27% Lanczos cost** (auto-vec, bit-identical) | Both separable passes accumulated 3 scalar r/g/b from a stride-4 RGBA read, blocking LLVM auto-vec. Restructure: horizontal → `[f32;4]` per pixel over 4 contiguous bytes/tap; vertical → single interleaved `acc[dw*4]` contiguous SAXPY. Alpha lane accumulates 255, ignored. Bit-identical (digest 1154d165a6a72d7d). No unsafe / target_feature → holds on all auto-vec targets (unlike manual-NEON dead-ends #3). On top of PAR_LANCZOS + LANCZOS_HOIST. Round-33 |
| D3_BICUBIC | 2026-07-06 | render FG44 upsampling (quality) | X (no perceptible gain) | SSIM ≥0.9982 bilinear-vs-bicubic (FG-only isolated), 0.9999 whole-page; ~17% slower when enabled | Catmull-Rom bicubic swap for `sample_bilinear` in the compositor's B-series (upscale) FG44 path; measured 12.0× FG44 subsample ratio is already below the perceptual floor under bilinear. Byte-identical default, macro-hoisted flag (zero cost off), but rejected on quality grounds — not shipped, reverted from `src/djvu_render.rs`. Crops visually indistinguishable. Round-32 |
| FGBZ_MEDIANCUT | 2026-07-06 | FGbz encoder (foreground palette) | **K** (opt-in) | synthetic fixture: FGbz −96.4% (2255B→82B), total −24.1% at k=6, SSIM −0.0031 (wash); at k≥true-colour-count SSIM matches baseline | Closes Round-31 backlog item. `foreground_fgbz` (`src/djvu_encode.rs`) previously emitted one exact palette entry per distinct per-blit average colour, bloating on scanner-noise/AA near-duplicates. New `FgbzPaletteOptions::MedianCut{max_colors}` via `PageEncoder::with_fgbz_options` runs classic median-cut quantisation + nearest-colour remap; default stays `Exact`, byte-identical (enforced by test `fgbz_mediancut_is_opt_in_default_stays_exact`). `irish.djvu` real fixture (already near-minimal 40-entry palette) shows small win only. D1 harness is luma-only (chroma blind) — compensated with before/after crops in `_pr_assets/`, visually indistinguishable at k=6/32. Not defaulted: picking `k` is content-dependent, too-small `k` (e.g. 4) does cost measurable SSIM. |
| JB2_DESPECKLE | 2026-07-06 | JB2 encoder (speck-removal pre-pass) | **K** (stable opt-in) | pathogenic (600 dpi scan) −0.94%/−1.59%/**−2.43%** Sjbz at despeckle=2/4/8 (SSIM 0.9995/0.9990/0.9985); watchmaker (text) byte-identical no-op at all levels | Round-30. First lossy lever to move the noisy-scan corpus at all — same-size `lossy_threshold` alone ≈0% there (round 19 B0), cross-size lossy reverted (round 19 B1, dominated). New `Jb2EncodeOptions::despeckle: Option<u32>` drops CCs with `pixel_count <= max_px` (true ink area, not bbox) before clustering/dedup; stable (not `experimental`), default `None` = byte-identical. `with_despeckle()` builder + `lossy_scan()` preset (`despeckle=8, lossy_threshold=0.02`). Punctuation/diacritic-dot survival validated by targeted test (16px dot/period survive, 1px dust removed) at every tested level. |
| JB2_DICT_ORDER | 2026-07-06 | JB2 encoder (shared dict) | D (negative) | Round-29; frequency/bucket-order permutation of shared dict: −0.05%…+0.05% (both directions), 30–60× below the ~1.5% bar | Confirmed rec-6 topological constraint (refiner must reference an already-emitted entry) is satisfied automatically for any in-block permutation, since the whole shared block is always emitted before page-local symbols. Measured real bytes (Djbz+Sjbz) on 75-symbol (`conquete_paix`, 22p) and 5,164-symbol (`pathogenic_bacteria_1896`, 517p) shared dicts; 0/539×3 round-trip failures. `NumContext` is an adaptive binary coder — it learns whatever empirical index distribution results from any fixed permutation, so static frequency/similarity reordering has no fixed-Huffman-style win to capture. No shipped-code change; recorded so dict reordering isn't re-proposed without a non-adaptive index coder |
| B5_STREAMING_DECODER | 2026-07-04 | progressive decode (API) | **K** | O(N) streaming progressive decode (render-as-you-receive) | Closes the deferred B5 stateful API. New `pub ProgressiveDecoder`: `new(page,opts)` decodes FG once, `push_bg44_chunk(chunk)` accumulates one BG44 chunk in a shared `Iw44Image` and returns the refined frame — for network viewers that render per arriving chunk (vs render_progressive O(N²) or render_progressive_all needing all chunks up front). Strict Bilinear only (Lanczos/permissive → UnsupportedOption). render_progressive_all's fast path refactored to dogfood it. Byte-identical (streaming-vs-batch test + direct vs render_progressive_step on chicken/colorbook; O(N) proven by chunk-decode counter: chicken 6→3, colorbook 10→4). Round-27 |
| BUG_ZPSHORT | 2026-07-06 | IW44 decode (BG44 refinement) | **K** (fix) | fixes `Iw44(ZpTooShort)` on watchmaker.djvu page 0; correctness (was silently using 2/4 chunks) | `Iw44Image::decode_chunk` hard-errored on a legitimate zero-length BG44 refinement payload; `ZpDecoder::new` requires ≥2 bytes. Pad short payloads to 2 bytes with `0xFF` (same convention `ZpDecoder::read_byte` already uses past a stream's end), scoped to djvu-iw44 only. Surfaced while validating PR #510's `ProgressiveDecoder`. Round-26 |
| JB2_AUTO_REC6 | 2026-07-06 | JB2 encoder (same-size rec-6 auto-policy) | **K** (experimental) | watchmaker −11.67% Sjbz (auto fires); pathogenic/conquete_paix byte-identical (auto stays off) | A3 follow-up of SAME_SIZE_REC6: `Jb2EncodeOptions::same_size_rec6_auto()` runs a bounded (≤1000-fresh-CC) density probe (`probe_same_size_rec6_density`) and enables `same_size_rec6=Some(0.02)` when ≥5% of sampled fresh CCs have a same-size ≤5% Hamming twin, else leaves it off. Threshold calibrated against **real emitted-byte deltas** on 4 corpora (not just population): watchmaker 39.6% density → −11.67%; cable_1973 12.4% → −0.43%; conquete_paix 1.7% → +0.49% (loss); pathogenic 0.9% → +0.00%. 0.05 sits ~2.9×/2.5× margin either side of the measured loss/win crossover. All round-trips pixel-exact. Still behind `experimental`, not default — same A3 shipping decision as SAME_SIZE_REC6. |
| PDF_ADAPTIVE_RASTER | 2026-07-06 | PDF export (colour raster) | **K** | **1.62× smaller** whole-file PDF on watchmaker.djvu (regression case), 1.05× on mixed content, unchanged when JPEG already wins | Round-28; implements PDF_DCT_PROBE's follow-up. Opt-in `PdfOptions::adaptive_raster` (default off, byte-identical). Per page, encodes both DCTDecode(JPEG-80) and FlateDecode, keeps smaller; loser dropped immediately (no O(1)-per-page memory regression, #449). Composes with the rayon parallel path (#298) — verified byte-identical seq vs parallel output. Lossless when Deflate wins, unchanged JPEG-80 when JPEG wins |
| PDF_DCT_PROBE | 2026-07-06 | PDF export (colour raster) | D | Round-23; shipped JPEG-80 default: 3.6–11.2× smaller on 2/3 pages, **3.1× LARGER** on a near-flat colour scan | Corrected a stale premise: DCTDecode is already the shipped default (#59), not Deflate. Content-dependent — found a real regression case (watchmaker p0: Deflate 245 KB vs JPEG-80 767 KB, SSIM already ≥0.999 either way). q85 measured strictly worse than q80 (larger, same SSIM). Recommend opt-in per-page adaptive Deflate/JPEG choice as follow-up — **implemented as PDF_ADAPTIVE_RASTER, round-26** |
| BZZ_ENC_DIAG | 2026-07-06 | BZZ encoder (TXTz/DIRM/NAVM/ANTz) | D | Round-23; 5–19% of a text-bearing page's encode time; real corpus blocks are 1.6–6.4 KB avg (~10.6 KB max) | Suffix sort is already `O(n log n)` prefix-doubling + counting sort (not naive `O(n² log n)`), 4 MiB block cap (= DjVuLibre MAXBLOCK). Real-text scaling 100 KB→2.25 MB measured ~n^1.3 (cache-unfriendly at scale) — SA-IS/divsufsort would help *if* blocks ever reach 100 KB–4 MB, but DjVu's actual per-page/per-chunk blocks stay in the low-KB range where absolute cost is already sub-2ms. Added `bench_bzz_encode` (none existed). Low-priority queue item, gated on a real large-single-block use case or an internal BWT-vs-MTF-vs-ZP profile split (not done here) |
| IW44_ENTROPY_GAP | 2026-07-04 | IW44 encoder (size) | D | c44 gap splits by content: smooth≈parity, textured ~1.3× | RD parity (matched SSIM, our decoder for both). Smooth BG (watchmaker): ours saturates at 30 slices/57B — beats c44 (84B@0.9976); raw slice-100 gap is over-coding past saturation (r18), needs a content-adaptive decibel target (~0.008 vs 0.265 bpp → 30× spread, fixed cap impossible), ~100B/page low-EV. Textured BG (colorbook): real ~1.3× at matched SSIM (345 vs 257B@0.9592) — interop-fragile IW44_ACT_THRESH normative territory. Round-22 |
| IW44_MASKED_TRANSFORM | 2026-07-04 | IW44 encoder (size) | X (unnecessary) | normative masked transform gains 0 after BG_DIFFUSE | **Closes IW44_MASKED_WAVELET.** Measured c44 `-mask` on raw BG ≈ c44 no-mask on our diffused BG (1471≈1445, 117=117, 2063=2063, 643≈644) → BG_DIFFUSE fully subsumes the masked lever; the normative transform would shrink BG44 by 0. Residual 1.18–1.62× gap vs c44 is entropy coding (IW44_ACT_THRESH), not masking. Big risky normative change shown not worth doing. Round-21 |
| BG_DIFFUSE | 2026-07-04 | IW44 encoder (BG size + quality) | **K** | **−52% to −97% BG44** on text scans, SSIM/PSNR up | Interop-safe down-payment on IW44_MASKED_WAVELET. Fully-masked (invisible) BG cells were filled with ink colour by default → wasted wavelet bits + dark halos. New `SegmentOptions::bg_diffuse`: harmonic (Laplace/Gauss-Seidel) fill from confident cells; enabled in Quality/Archival profiles. Strict win — smaller AND higher SSIM (colorbook 18092→16044 B, conquete 28348→6134 B, SSIM up on all). Encoder-only, decoder never sees it. `--bg-inpaint` CLI now = legacy ring. Round-20 |
| CLUSTER_BUCKET_HASH_DEDUP | 2026-07-04 | JB2 encoder (clustering) | **K** | **−81% (≈5.2×) cluster_shared_symbols (517p)** | `bucket_page_ccs` linear `packed_hamming` scan (max_diff==0, byte-exact) → `symbol_hash`-keyed `BTreeMap` + equality verify. Byte-identical (digest 1d797be493bab1fe, 5164 syms unchanged). Analog of CLUSTER_DEDUP #446 for the clustering pass. Est. was ~2% (fraction of full encode) — isolated on dense 517p corpus the bucketing scan itself dominated. New `bench_cluster_shared_symbols`. Round-15, swarm P4 |
| SHARED_DICT_CLONE_PER_PAGE | 2026-07-04 | JB2 encoder (bundle) | **K** | byte-identical; ~2.67M fewer clones (517p bundle); wall-clock ~12% but within thermal noise | `encode_jb2_dict_with_options` held `dict_entries: Vec<Bitmap>` and deep-copied the shared dict on every page (5164×517≈2.67M clones). Now `Vec<&Bitmap>` borrowing `shared_symbols`+`ccs` (both outlive the encode); helpers take `&[&Bitmap]`, no public sig change. Digest identical (conquete f2e5…, pathogenic f6ec…). Peak RSS flat (clones transient). Kept on strictly-≤-cost grounds like CLUSTER_DEDUP/PDF_STREAM. Round-16, swarm P2 |
| D4_GAMMA_DOWNSCALE | 2026-07-03 | render downscale (quality) | X (low headroom) | gamma vs linear-light SSIM ≥0.979 | Diagnostic via D1: gamma-space vs correct linear-light downscale differ negligibly on real pages (photo ≥0.997, text 0.979 worst). Hard edges live in the JB2 mask, not the smooth BG. Rejected; also deprioritises A3. Lanczos (D2) is the real text-downscale lever. Round-9 |
| D5_TH44_PREVIEW | 2026-07-03 | render (preview fast path) | X (opt-in only) | 20–30× faster but SSIM 0.50–0.68 | TH44 thumbnail preview: measured via D1. Big speed win but low fidelity + **no corpus file embeds TH44**. Viable only as explicit opt-in for thumbnail grids, not a default. Round-9 |
| C3_ZOOM_SCOPE | 2026-07-04 | render zoom/region | D | already efficient (~5.5 ns/px, flat vs zoom) | Diagnostic: region/zoom render is scoped, linear in viewport pixels, no per-call overhead, same per-pixel rate as full-page compositor. No speed win; residual is cold ROI_IDWT (rejected). Remaining zoom work is quality (mask AA). Round-14 |
| C5_LRU_BUDGET | 2026-07-04 | memory (auto LRU) | **K** | cache held under ceiling; RSS 714→255 MB | `enforce_cache_budget(max, protect)` + per-page LRU tick + `render_cache_bytes`. Automatic form of retain_render_caches: evicts least-recently-used pages to a byte budget. Byte-identical (test). C5 follow-up, round-13 |
| D_AA_ZOOM | 2026-07-06 | render mask AA (quality) | **K** (opt-in) | PSNR/MSE favour AA (+0.3–0.7dB, −6…−15% MSE), SSIM favours nearest (known blur-vs-SSIM tradeoff); visibly smoother glyph edges at 4× zoom | Resolves QUALITY_AA's deferred #13: opt-in `RenderOptions::mask_aa` (default false, byte-identical, proven no-op at scale≤1 incl. bg-subsampled-at-1:1 corner case) bilinearly interpolates JB2 mask coverage at genuine upscale only. ~2.2–2.8× slower than nearest when enabled. New `examples/mask_aa_quality.rs` (D1 SSIM harness) + `examples/mask_aa_crops.rs` (`_pr_assets/` PNG evidence). Round-25 |
| C5_RENDER_CACHE_EVICTION | 2026-07-04 | memory (long-lived viewer) | **K** | **−86% peak RSS** (714→103 MB, 62 pp) | Found: per-page render caches never evict → RSS grows ~11 MB/page unbounded (OOM risk on big books). Fix: additive `evict_render_cache`/`evict_render_caches`/`retain_render_caches`. Byte-identical (test). Backlog C5, round-12 |
| B5_INCREMENTAL_PROGRESSIVE | 2026-07-04 | progressive decode | **K** (modest) | 1.04–1.50× (grows w/ chunk count) | `render_progressive_all` decodes FG once + accumulates BG chunks in one Iw44Image instead of O(N²) per-frame re-decode. Byte-identical (test). Modest because IDWT reconstruct+composite are inherently per-frame; only ZP redundancy removed. Streaming-step case still needs stateful decoder API. Backlog B5, round-11 |
| PAR_LANCZOS | 2026-07-04 | Lanczos-3 resampler | **K** | **−69% (3.2×)** large-page Lanczos (parallel) | Row-parallel both separable passes behind `parallel` feature (par_chunks_mut + for_each_init scratch). Bit-identical. Isolated on colorbook native→½: 100.5→31.2ms. Amplifies D2 (Lanczos now ~3× not ~10× a bilinear downscale). Backlog A4, round-10 |
| QUALITY_HARNESS_D1 | 2026-07-03 | quality infra (unblocks D branch) | **K** (infra) | perceptual gate + D2 decided | New `src/quality.rs` (PSNR/SSIM/`compare`, 6 tests) + `examples/quality_harness.rs` (vs ddjvu). Immediate finding: Lanczos3 downscale SSIM 0.993/0.989 vs Bilinear 0.959/0.861 → **Lanczos decisively better** (#423/D2 answered). Gates A3/D3/D4/D5. Round-9 |
| GRAY_DIRECT | 2026-07-03 | IW44 decode (gray axis) | **K** | **−71% seq / −46% par** gray decode of colour page | New `Iw44Image::to_gray8[_subsample]`: reconstruct Y plane only, skip both chroma IDWTs + YCbCr math. Grayscale imgs byte-identical to `to_rgb().to_gray8()`; colour = DjVu luma (mean<4/255 vs Rec.601). Additive, no risk. Round-7, backlog B2 |
| TRIAGE_ROUND8 | 2026-07-03 | breadth triage (axes A–D) | D | verdicts for ~25 proposals | B3 (early wavelet stop) + B4-hot (word blit) **already implemented**; C1-compositor + C2 **ruled out**; B5 (progressive O(N²) decode) **top opportunity, measured 4.8× on colorbook, deferred → needs stateful decoder**; rest NEEDS-INFRA / QUALITY-GATED (D1 harness unblocks D branch). See PERF round 8 |
| GATHER_ZIGZAG_INV | 2026-07-03 | IW44 encode | **K** | ~1% iw44_encode + cleanup | Row-major plane read + ZIGZAG_INV scatter into L1 block (mirrors decoder); deletes 2 dup zigzag tables. Byte-identical. Round-6 swarm P5 |
| EPUB_PNG_COMPRESSION | 2026-07-03 | EPUB export | X | rejected on verification | Swarm P1 claimed −43.5% via png Fast, but Fast makes files **2.84× larger** (measured 271KB→770KB) — bad trade for a doc format. Default already balanced. Case study: verify swarm claims |
| LAZY_PAGE_CONSTRUCT | 2026-07-03 | container cold-open | **K** | **−48% from_bytes(520pp), −9% open+render-first**; mmap zero-copy | Bundled pages defer per-chunk copy: `ChunkStore::Lazy` holds shared backing (`Arc<dyn AsRef<[u8]>>`) + FORM range, materialises on first access. from_bytes/mmap funnel owned backing. Byte-identical (mmap parity test). Swarm P3 |
| PGO | 2026-07-03 | build profile (all paths) | **K** (opt-in) | **−15% cold render**; codec kernels flat/±1% | Profile-guided opt over fat-LTO. Big win on branch-heavy cold end-to-end render; neutral on LTO-inlined SIMD decode kernels (2 micro-benches +1–2%). Opt-in only (`make pgo`, corpus/host-specific profdata, can't ship to crates.io). `examples/pgo_train.rs` + `scripts/pgo.sh`. Round-5 #4 |
| INTEROP_PIXDIFF | 2026-07-03 | render quality (infra) | **K** (tool) | quality floor: mean <0.25/255 vs ddjvu | `examples/interop_pixdiff.rs` — per-pixel diff vs DjVuLibre. Palette/bilevel byte-identical; colour mean <0.25 (bilinear chroma #422). Validates round-5 changes pixel-faithful. Round-5 #14 |
| QUALITY_AA | 2026-07-03 | color/mask AA | D/F | deferred | Linear-light blend (#12) + mask upscale AA (#13) would diverge from the near-identical DjVuLibre reference; opt-in quality-mode territory, needs aesthetic judgement. Gate on scale>1. Round-5 #12/#13 |
| AVX2_IDWT | 2026-07-06 | IW44 IDWT (x86) | **X** (rejected) | activating existing AVX2 column-pass kernel **regresses** `iw44_to_rgb_colorbook` ~10-24% (two independent activation mechanisms agree) | Runtime-dispatched (`is_x86_feature_detected!`, `OnceLock`) the pre-existing but compile-time-dead `load8s_s1_avx2`/`store8s_s1_avx2` kernels; bit-exact (exhaustive equivalence test, CI `Test (stable)` on x86_64) but measurably slower, not faster — confirms round-47's flag-based finding with a real hand-written kernel. Reverted; closed with data. Round-5 #8, Round-47, Round-54 |
| ROI_IDWT | 2026-07-03 | render_region | X | low-value | Cold `decode_layers` cost is ZP-decode-bound (serial entropy stream, no spatial subset); only IDWT+convert is narrowable and it's cold-once. Warm region render already scoped. Round-5 #1 |
| DOC_SHARED_DICT_CACHE | 2026-07-03 | decode pipeline | **K** | **−37% multi-page shared-dict mask decode** | Decode shared JB2 dict once per document, not per page. `SharedDict{raw,OnceLock}` folds decode into the shared `Arc`. Byte-identical; scales with page:dict ratio. Fixtures existed (DjVu3Spec 71/5, czech 85/2, pathogenic 520/52). Round-5 #3 |
| SUB4_RGB_CACHE | 2026-07-03 | decode pipeline | **K** | **−4.9% color_downscale (sub=4 warm)** | Cache decoded BG44 RGB at sub=4 (from partial image); mirror of BG_CACHE/BG_CACHE_S2 for heavy-downscale/thumbnail. Byte-identical, ~2 MB/page. Round-5 open hypothesis #20 |
| BZZ_DEC_MTF | 2026-07-03 | BZZ decode | X | already covered | PS1 `copy_within` memmove already present at decode.rs:418; residual loop is a freq-sorted early-exit insert, not a plain shift. Round-5 #19 |
| IDWT_PAR_PLANE | 2026-07-03 | IW44 decode | D/F | cold-only, 3.26 ms reconstruct+convert baseline | Within-Y-plane wavelet parallelism. Deferred: cold-only (warm uses bg_rgb_s1), convert already parallel, needs per-thread scratch + per-scale barriers in normative SIMD loop, marginal on corpus. Design + fixture need recorded. Round-5 #2 |
| RENDER_REGION_SCOPE | 2026-07-03 | render_region | D | no change | `render_region` already composites only the region (offset + out_w×out_h); no full-page composite/crop. Residual cold-decode narrowing = ROI_IDWT (#1). Round-5 #1/#10 |

| IW44_MASKED_WAVELET | 2026-07-02 | IW44 encoder (size) | **CLOSED** (r21) | superseded by BG_DIFFUSE (r20) | Masked BG encoding (DjVuLibre c44 lever). **Resolved:** the low-risk first step (BG_DIFFUSE, r20) captured the whole win interop-safely; IW44_MASKED_TRANSFORM (r21) then measured the normative transform adds 0 on top (c44 -mask ≈ c44-on-diffused). No further masked work needed; residual gap is entropy coding, not masking. |
| LTO_FAT | 2026-07-02 | build profile (all paths) | **K** | **−65% jb2_encode_dict, −7% bzz/segment, −2…3% iw44** | `lto="fat"` + `codegen-units=1` release/bench profile (none existed before). Cross-crate ZP inlining; render flat. Cost: ~2× build time |
| PAR_ENCODE | 2026-07-02 | encoder (multi-page) | **K** | **−35…43% layered, −39% bundle_jb2** | Parallel per-page encoding via rayon in both DJVM bundlers (`parallel` feature); byte-identical, seq fallback. Decode was parallel (PAR_DEC) but encode wasn't — largest untouched lever |
| ENC_SPEED_DIAG | 2026-06-26 | encoder (speed) | D | IW44 1.46× faster than c44; JB2 ~par with cjb2 | Head-to-head on identical inputs. Encode speed competitive — no hot-spot; axis healthy, not a gap |
| JB2_PAGE_SYM_CAP | 2026-06-25 | JB2 decode (robustness) | **K** | fuzz_jb2 timeout fixed; per-page decode bounded 256→16 MP | 409 B input did 23 K refinement records = 47.7 MP (~626 ms→10 s ASAN). Split cap: 16 MP page / 256 MP dict (32→16 for fuzz margin). Regression test + seed corpus |
| IW44_SWARM_REST | 2026-06-25 | IW44 encoder (size) | R/X | dead-end / interop-break | #8 ycbcr-round (PSNR worse), #6/7 early-term (chunks not null), #3/4/5/10/12 (change normative tables/ctx → break DjVuLibre interop), #2 (quality knob) |
| IW44_ACT_THRESH (IW44-1) | 2026-06-25 | IW44 encoder (size) | **K** | **−9.1% BG44, PSNR-identical** | Activation prediction s/2→11s/16 matches real gate; removes wasted NEW bits. Encoder-only, interop-safe. Gap 14.3%→3.9% |
| ENC_SIZE_DIAG | 2026-06-25 | encoder (size) | D | mask=parity, IW44 BG44=1.143× | Chunk breakdown: BG44/FG44 dominate colour/photo docs (94–99.9%); our IW44 is ~14% larger → next size lever |
| LAYERED_SHARED_DJBZ (#452) | 2026-06-24 | JB2 encoder (size) | **K** | **−34% (spec) / −5.5% (watchmaker)** | Shared Djbz for layered multi-page encode; closes the real DjVuLibre size gap (per-page dict dup). Round-trip verified |
| CHROMA_BILINEAR (#422) | 2026-06-24 | IW44 YCbCr | **K** | quality (chroma_half) | Bilinear chroma upsampling via per-row pre-upsample + reuse full-res SIMD kernel; only IW44 v2 pages, common path byte-identical |
| REFROW_REG (#445) | 2026-06-24 | JB2 decode | R | no benefit | Rolling m_r2 register; col_shift unbounded so guard can't drop; safe version just relocates read |
| IDWT_S2_NEON (#442) | 2026-06-24 | IW44 IDWT NEON | X | incorrect premise | Reuse s1_row for s==2 fails correctness: s==2 ⟹ sd==1 (stride-2 cols), s1_row assumes stride-1 |
| IDWT_SPLAT (#441) | 2026-06-24 | IW44 IDWT NEON | R | unmeasurable | Hoist vdupq_n_s32 splat; movi is free in ALU-bound loop, no benefit. C16/C8 pattern doesn't transfer |
| F2_GAMMA (#443) | 2026-06-24 | color 1:1 bilinear | **K** | narrow (non-identity gamma) | F2 all-bg fast path for non-identity gamma (LUT pass); free-on-miss, byte-identical. Identity path unchanged |
| PDF_STREAM (#449) | 2026-06-24 | PDF export | **K** | peak RSS O(pages)→O(1) bodies | Stream render→emit→drop in sequential path; byte-identical, strictly ≤ peak. Unmeasurable on corpus (no many-large-body doc) |
| LANCZOS_HOIST (#448) | 2026-06-24 | Lanczos resample | **K** | **−22.5% Lanczos** | Hoist sin weights + row-major accumulate in vertical pass; bit-identical |
| ROTATE_TILE (#447) | 2026-06-24 | rotate_pixmap | **K** | ~2–6% rotated (transpose much faster) | 32×32 tiled transpose for Cw90/Ccw90; cache-local vs strided. Byte-identical |
| CLUSTER_DEDUP (#446) | 2026-06-24 | JB2 encoder | **K** | O(P²)→O(P) | pages_seen.contains→last() under monotonic page order; strictly ≤ cost, helps large multi-page encodes |
| FGPAL_CACHE (#444) | 2026-06-24 | decode pipeline | R | +1% regression | Cache parsed FGbz palette; navm_fgbz chunk is 21 bytes (trivial parse) → cache overhead > saving |
| PAR_DEC (#440) | 2026-06-24 | decode pipeline | **K** | **−22% cold render** | rayon::join BG/FG layer decode (parallel feature); FG overlaps BG ZP phase. Warm free, cold first-open faster |
| COLOR_AA (#439) | 2026-06-24 | color area-avg | **K** | quality (68 vs 28 colors) | Proportional fg/bg blend via mask_box_coverage; AA color downscale, removes blocky halos. mask_box_any deleted |
| AREAAVG_ALLBG (#438) | 2026-06-24 | color area-avg | **K** | ~skip mask_box_any (50–71% fire) | All-bg band short-circuit skips footprint scan; byte-identical, #428 analog for color downscale |
| EXACT_SLICE (#437) | 2026-06-24 | color 1:1 bilinear | R | unmeasurable | Exact-length row slices for bounds-check elision; unwrap_or(&[]) defeats fg length-tracking; machine too hot to verify |
| BSERIES_MASKEXP (#436) | 2026-06-24 | B-series upscale | R | unmeasurable + overhead | MASK_EXPAND in B-series (G1b retry on upscale); expansion overhead not worth it on resampling-bound path |
| BSERIES_ALLBG (#435) | 2026-06-24 | B-series bilinear | **K** | ~3–8% (52.6% fire) | All-bg row short-circuit skips is_fg; byte-identical, F2 analog. Benchmark noisy, firing-rate evidence |
| FG_FX_ACCUM (#434) | 2026-06-24 | B-series bilinear | R | +13–27% regression | Q48 accumulator for fg_fx; advance is unconditional (100%) but fg multiply was conditional (~20%). Inverse of B1 |
| P2_REGION (#433) | 2026-06-24 | bilevel 1:1 | **K** | compositor faster, e2e within noise | Generalize P2 to byte-aligned offset_x (render_region); safe ext, zero regression on offset_x==0 |
| MASK_ANY_BYTE (#432) | 2026-06-24 | color area-avg | R | +5.1% regression | Byte-scan mask_box_any; early-exit + small footprint → setup overhead > saving. POPCNT doesn't transfer |
| TIFF_LUT (#431) | 2026-06-24 | TIFF export | **K** | **~18% bilevel export** | LUT byte-expansion in extract_bilevel_pixels (Gray8 analog of P2 table) |
| TIFF_DEFLATE (#430) | 2026-06-24 | TIFF export | **K** | **~149× size** | Deflate-compressed bilevel TIFF (1-bit not supported by tiff 0.9; Deflate beats it) |
| CROP_BYTECOPY (#429) | 2026-06-24 | JB2 direct encoder | **K** | **~14–15% multitile** | Byte-aligned crop_bitmap row-copy; direct encode_jb2 path only (non-shipping) |
| I3_DOWNSCALE (#428) | 2026-06-24 | bilevel downscale | **K** | **~6.5% (94.4% fire)** | Blank-band fill(255) in AA downscale path; benchmark thermal-noisy, evidence from fire rate + cost model |
| MASK_IDX_CACHE (#427) | 2026-06-24 | decode pipeline | **K** | **~5% palette** | Cache indexed JB2 mask+blit-map; avoids 33.6 MB re-alloc + JB2 re-decode per render |
| BG_CACHE_S2 (#426) | 2026-06-24 | decode pipeline | **K** | **−7–9% downscale** | Cache decoded BG44 RGB at sub=2; mirror of BG_CACHE for 150 DPI |
| C2c | 2026-06-24 | area-avg compositor | R | 0% | LLVM LICM already hoists fg_fy. Related: C2b |
| C2b | 2026-06-24 | B-series bilinear | R | 0% | LLVM LICM already hoists fg_fy. See dead-end §1 |
| P2 | 2026-06-24 | bilevel 1:1 | **K** | **+18–24% bilevel** | BILEVEL_RGBA 256×32B table; NEON `ldp q0,q1/stp q0,q1`. Replaces E1 |
| Q1 | 2026-06-24 | bilevel 1:1 | R | 0% | Clamp removal; LLVM still scalar. Superseded by P2 |
| G1b | 2026-06-24 | B-series bilinear | R | −10% regression | MASK_EXPAND in downscale path; overhead > saving at 72 DPI |
| I3 | 2026-06-24 | bilevel 1:1 | **K** | **+2.3% bilevel** | All-zero mask row → `row_buf.fill(255)`. Fires before P2 |
| G2 | 2026-06-24 | color 1:1 bilinear | R | 0% | bg-fill-then-fg-fixup; LLVM already pipelines loads. See dead-end §2 |
| G1 | 2026-06-24 | color 1:1 bilinear | **K** | **+25% color** | MASK_EXPAND to 4096B stack buf; 1 byte-load replaces 7-op bit-extract |
| F2 | 2026-06-24 | color 1:1 bilinear | **K** | **+3.4% color** | All-bg mask row → bulk `copy_from_slice`. Mirrors I3 |
| E1 | 2026-06-24 | bilevel 1:1 | R | 0% | MASK_EXPAND+fill in bilevel path; LLVM auto-vec already optimal |
| C3 | 2026-06-24 | color 1:1 bilinear | **K** | ~0% (cleanup) | Mask-row hoist; LLVM did LICM; kept for code clarity |
| POPCNT | 2026-06-23 | bilevel downscale | **K** | **−24% @72DPI** | Byte-level popcount in `mask_box_coverage`; replaces per-pixel bits |
| AA | 2026-06-23 | bilevel downscale | **K** | quality ↑ | Anti-aliased bilevel via `mask_box_coverage`; was binary `mask_box_any` |
| C2 | 2026-06-23 | color 1:1 bilinear | **K** | −1.4% color | FG44 y-row + bg-row hoist in general 1:1 path |
| BILINEAR | 2026-06-22 | color 1:1 bilinear | **K** | ~0% (+0.4% noise) | quality: `sample_nearest` → `sample_bilinear` for FG44 |
| ZEROED | 2026-06-22 | output allocation | X | −1% seq / **+62% par** | `Pixmap::zeroed` triggers VM page-fault storm in parallel path |
| COW_FG | 2026-06-21 | decode pipeline | **K** | −1.7% seq / −6.1% par | Cow for FG44+Mask; avoids 4.75 MB clone each warm render |
| FILL_OVR | 2026-06-21 | color 1:1 bilinear | X | +3.3% regression | Fill+overlay 2-pass; doubles output bandwidth vs 1-pass |
| COW_BG | 2026-06-21 | decode pipeline | **K** | −1.2% seq / −7.2% par | Cow for BG44; avoids 33.6 MB clone each warm render |
| ALPHA_INL | 2026-06-21 | color 1:1 bilinear | **K** | −2.3% seq / **−12.6% par** | Inline alpha=255 per pixel; removes 33.6 MB sequential post-pass |
| BG_CACHE | 2026-06-21 | decode pipeline | **K** | −5.1% color | Cache decoded BG44 RGB Pixmap; avoids 2.8 ms IDWT+YCbCr per call |
| BILEVEL_NEON | 2026-06-21 | bilevel 1:1 | X | +1.8% regression | Manual NEON `vst4_u8`; LLVM auto-vec already optimal. See dead-end §3 |
| COLOR_NEON | 2026-06-21 | color 1:1 bilinear | X | +0.9–1.3% regression | Manual NEON for A2 path; inner loop not bottleneck |
| SIMD_AREA | 2026-06-21 | area-avg downscale | X | +19.9% regression | NEON/SSSE3 accumulate; 2×2 footprint too small for SIMD threshold |
| PARALLEL | 2026-06-18 | all paths | **K** | **3.8× parallel** | `par_chunks_exact_mut` in `composite_into`; opt-in via `parallel` feature |
| IW44_PAR | 2026-06-21 | IW44 decode | **K** | **2.2× IDWT** | Parallel Y/Cb/Cr IDWT via `rayon::join`; opt-in |
| AREA_FIX | 2026-06-18 | area-avg downscale | **K** | **−52% colorbook** | Fix off-by-one exclusive bound; was 3×3 at 2×, now correct 2×2 |
| JB2_CROSS6 | 2026-06-15 | JB2 encoder | R | +4.37% size | Cross-size record-6; geometric misalign makes context mispredict |
| IW44_DIAG | 2026-06-15 | IW44 encoder | D | — | Loss localized to fine-band quantization; transform is bit-exact |
| RENDER_DIRECT | 2026-05-16 | render pipeline | **K** | **−18–20%** | `render_pixmap` direct → `render_into`; removes row-copy adapter |
| TIFF_STREAM | 2026-05-16 | TIFF export | **K** | −116 MB peak RSS | `render_streaming` feeds TIFF rows; no full RGBA pixmap |
| ADAPTIVE_SEG | 2026-05-16 | encoder segmentation | **K** | quality ↑ | Sauvola binarization + BG inpainting; opt-in via `SegmentOptions` |
| DJVM_MULTI | 2026-05-16 | encoder | **K** | — | Layered multi-page DJVM via CLI `--quality` |
| JB2_QUALITY | 2026-05-17 | JB2 encoder | F | dict=1.356× orig | Round-trip ok; byte cost remains too high vs original |
| WASM_SIMD | 2026-05-17 | wasm | D | −7% render | Harness: scalar vs simd128; simd128 confirmed win from existing IW44 code |
| ROW_SCRATCH | 2026-05-17 | render pipeline | X | inconsistent | Noisy A/B; rejected as production heuristic |
| COMP_BASELINES | 2026-05-17 | infrastructure | D | — | Added `render_compositor_only` bench group |

---

## Render Path Map

```
render_pixmap (RENDER_DIRECT: kept)
└── composite_into / composite_rows
    ├── [parallel] composite_rows via par_chunks_exact_mut   (PARALLEL: kept)
    ├── composite_rows_bilevel_one                            ← bilevel 1:1 path
    │   ├── I3: all-zero row → fill(255)                     KEPT
    │   ├── P2: BILEVEL_RGBA table, offset_x==0             KEPT  ← hot path
    │   ├── mask_bilinear_coverage (opt-in mask_aa, upscale) KEPT (D_AA_ZOOM)
    │   └── scalar fallback (bit-extract per pixel)
    ├── composite_rows_bilinear_one
    │   ├── [1:1] general fast path                          ← color 1:1 path
    │   │   ├── F2: all-bg mask row → copy_from_slice        KEPT
    │   │   ├── G1: MASK_EXPAND pre-expand to stack buf      KEPT  ← hot path
    │   │   └── per-pixel bilinear blend
    │   └── [B-series] non-1:1 path                         ← color downscale/upscale
    │       ├── sample_bilinear(fg, fg_fx, fg_fy)            (C2b: LLVM already LICM)
    │       ├── mask_box_coverage / POPCNT                   KEPT (downscale)
    │       └── mask_bilinear_coverage (opt-in mask_aa)       KEPT (D_AA_ZOOM, upscale)
    └── composite_rows_area_avg_one                          ← area-avg downscale
        ├── AREA_FIX: correct 2×2 exclusive bound            KEPT
        └── sample_area_avg_bounds                           (SIMD: rejected)

Decode pipeline:
  BG44 → RGB cache (BG_CACHE) → Cow borrow (COW_BG) → compositor
  FG44 → Cow borrow (COW_FG) → compositor
  JB2  → Cow borrow (COW_FG) → compositor
  IW44 IDWT: parallel Y/Cb/Cr (IW44_PAR)
  Output alpha: inlined per-pixel (ALPHA_INL)
```

---

## Current Baselines (cool state, M1 Max, aarch64, Rust 1.88)

| Benchmark | Time | Path exercised |
|-----------|------|----------------|
| `bilevel_native_cached` | ~37 ms | bilevel 1:1 (P2 + I3 active) |
| `color_native_cached` | ~47 ms | color 1:1 (G1 + F2 active) |
| `color_downscale_mixed_cached` | ~15–20 ms | B-series bilinear (watchmaker @150/300) |
| `render_corpus_bilevel_dpi/72` | ~7 ms | bilevel downscale (POPCNT) |
| `render_corpus_bilevel_dpi/150` | ~23 ms | bilevel downscale (POPCNT) |
| `render_colorbook` | ~3.5 ms | area-avg downscale (AREA_FIX) |

Thermal methodology: use intra-session ratio `target / control` to cancel M1 Max throttling.
Control for bilevel experiments: `color_native_cached`. Control for color: `bilevel_native_cached`.

---

## Known Dead Ends (do not retry without new evidence)

1. **LLVM LICM** covers all y-row hoists in B-series / area-avg paths (C2b, C2c). Confirmed by intra-session ratio showing zero consistent trend. Any explicit hoisting adds Option wrapping that may inhibit other opts.
2. **LLVM out-of-order pipelining** covers independent bg_row + g1_mask loads in the G1 inner loop (G2). A second pass over g1_mask to find fg pixels adds sequential dependency that prevents the same pipelining.
3. **LLVM auto-vectorization** of the bilevel scalar loop is already optimal on aarch64-apple-darwin (BILEVEL_NEON, E1, Q1). Manual NEON / `#[target_feature]` on stable Rust cannot use `#[inline(always)]` (issue #145574).
4. **SIMD for small footprints** (SIMD_AREA): at 2×2 downscale, each `sample_area_avg_bounds` row has 2 pixels = 8 bytes, below any SIMD threshold. Falls through to scalar tail every call.
5. **`Pixmap::zeroed`** (lazy VM pages): causes page-fault storm in parallel compositor on macOS; 21–62% regression in parallel path.

---

## Open Hypotheses (tracked as GitHub issues)

Generated 2026-06-24 by the `perf-experiment-swarm` workflow (14 discovery agents →
adversarial validation → synthesis). Each survived a skeptical validator that checked
against all 37 prior experiments and the 5 dead-ends. Pick one, run the experiment,
record the result in `PERF_EXPERIMENTS.md`, close the issue.

**P1 (highest leverage):**
- ~~#426 — cache decoded BG44 RGB Pixmap at subsample=2~~ **DONE (Kept, −7–9%)** → BG_CACHE_S2
- ~~#427 — cache decoded JB2 indexed mask in PageLayers~~ **DONE (Kept, ~5%)** → MASK_IDX_CACHE
- ~~#428 — all-white band fast path in bilevel downscale compositor~~ **DONE (Kept, ~6.5%)** → I3_DOWNSCALE
- ~~#429 — crop_bitmap aligned byte-copy fast path in JB2 encoder~~ **DONE (Kept, ~14–15% multitile)** → CROP_BYTECOPY
- ~~#430 — 1-bit TIFF output for bilevel pages~~ **DONE (Kept, ~149× via Deflate; 1-bit unsupported by tiff 0.9)** → TIFF_DEFLATE

**P2:**
- ~~#431 LUT byte-expansion in bilevel TIFF~~ **DONE (~18%)** → TIFF_LUT · ~~#432 byte-level early-exit in `mask_box_any`~~ **REVERTED (+5% regression)** → MASK_ANY_BYTE
- ~~#433 extend P2 to byte-aligned offset_x~~ **DONE (Kept, safe ext)** → P2_REGION · ~~#434 B-series fg_fx Q48 accumulator~~ **REVERTED (+13–27% regression)** → FG_FX_ACCUM
- ~~#435 B-series all-bg row fast path~~ **DONE (Kept, 52.6% fire)** → BSERIES_ALLBG · ~~#436 MASK_EXPAND in B-series upscale~~ **REVERTED (overhead, unmeasurable)** → BSERIES_MASKEXP
- ~~#437 exact-length FG44/BG44 row slices~~ **REVERTED (unmeasurable, mechanism defeated)** → EXACT_SLICE · ~~#438 area-avg all-bg row fast path~~ **DONE (Kept, 50–71% fire)** → AREAAVG_ALLBG
- ~~#439 proportional fg/bg blend in color area-avg~~ **DONE (Kept, quality)** → COLOR_AA · ~~#440 PAR-DEC parallel layer decode~~ **DONE (Kept, −22% cold)** → PAR_DEC

**P3:**
- #441 hoist NEON splat consts in IDWT · ~~#442 NEON s=2 row pass in IW44 IDWT~~ **REJECTED (incorrect premise: s==2 implies sd==1)** → IDWT_S2_NEON
- ~~#443 F2 fast path for non-identity gamma~~ **DONE (Kept, safe ext)** → F2_GAMMA · ~~#444 cache parsed FGbz palette~~ **REVERTED (+1%, 21-byte chunk)** → FGPAL_CACHE
- ~~#445 rolling 1-bit register in decode_ref_row~~ **REVERTED (col_shift unbounded, no benefit)** → REFROW_REG · ~~#446 O(1) dedup in cluster_shared_symbols~~ **DONE (Kept, O(P²)→O(P))** → CLUSTER_DEDUP
- ~~#447 cache-tiled transpose in rotate_pixmap~~ **DONE (Kept, ~2–6%)** → ROTATE_TILE · ~~#448 Lanczos3 vertical-pass weight precompute~~ **DONE (Kept, −22.5%)** → LANCZOS_HOIST
- ~~#449 PDF streaming render-and-emit~~ **DONE (Kept, strictly ≤ peak)** → PDF_STREAM

**Pre-existing (not from swarm):** #422 bilinear chroma upsampling · #423 Lanczos-3 resampling option

**Still open from prior analysis (no issue yet):** JB2 byte-cost estimator before cross-size emit
(#301; JB2_CROSS6 was +4% size — need cost model first). ~~IW44 quantization/slice-budget increase
for fine bands (IW44_DIAG)~~ **CLOSED by IW44_SLICE_RD (round 18):** the RD curve shows
`total_slices=100` is at the knee and avg_abs=8.72 is the transform's quantization floor, not
slice starvation — more slices explode size for ≤0.003 SSIM.
