JPEG 4:2:0→RGB fused decode + v3.11 benchmark review response - #145
Open
sebastient wants to merge 7 commits into
Open
JPEG 4:2:0→RGB fused decode + v3.11 benchmark review response#145sebastient wants to merge 7 commits into
sebastient wants to merge 7 commits into
Conversation
set_output_format(Some(Rgb)) previously honoured only 4:4:4 sources, falling back to native NV12 for the dominant real-world subsampling (4:2:0). Add write_rgb_rows_420: a 2x2 nearest-neighbour (box) chroma upsample fused into the MCU write stage, reusing the existing ycbcr_to_rgb_row SIMD kernels on the upsampled row rather than a new hand-written subsampled colour kernel. 4:2:2 and other non-4:2:0 subsamplings still fall back to native output. Box (not fancy/triangle) upsampling is a deliberate speed tradeoff, measured at 44-50 dB PSNR / max pixel delta 20-24 against a reference decode on the COCO-family test fixtures - comparable to the existing accurate-vs-fast-DCT accuracy cost. Signed-off-by: Sébastien Taylor <[email protected]>
Addresses docs/BENCHMARKS_JPEG_REVIEW_v3.11.md end to end: Blocking: - Mark the v0.22.1 Image Codec Decode JPEG rows superseded in place (decoder internals they measured no longer exist post-rewrite) - Reconcile the nvJPEG June-vs-August conclusions (pre-optimization CPU baseline, single fixture vs corpus, full load_image vs decode-only) - Scope the "two releases behind" banner to the GL/preprocessing matrix only High: - Measure zune-jpeg's neon feature is genuinely engaged (1.16-1.20x A/B on rpi5-hailo) rather than inferred from a features table - Measure Wuffs' RGB row is on its 3-byte swizzle slow path (1.52-1.59x behind its native 4-byte output); add EDGEFIRST_WUFFS_FORCE_4BPP to wuffs_bench for the A/B - Publish per-board libjpeg-turbo version/package-source/compiler (none of the five original hosts run turbo 3.2+) - Add mbp-m2-max to the eight-arm sweep (largest accurate-class lead of any board, 1.41x on val2017) Medium: - Add a Max-spread column to the headline table; correct the "sub-1%" prose it was not actually measuring against - Unify ratio (not percentage) framing throughout, label YUV/RGB pairs - Quantify the zune greyscale-skip asymmetry: exactly zero of val2017's 10 greyscale files land in the n=200 evenly-spaced sample - Run a second independently-launched AWS instance per queue: 4/5 queues within 0.6%, m7i shows a real ~7% instance effect that barely moves the published ratio (1.176x vs 1.185x) The high-priority "4:2:0->RGB implementation" item is addressed by a companion feat commit (write_rgb_rows_420) rather than being deferred as a known frontier; this commit updates BENCHMARKS.md's Control Corpora section with the resulting RGB-on-4:2:0 measurements. Signed-off-by: Sébastien Taylor <[email protected]>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR enhances the edgefirst-codec JPEG CPU decoder by extending the opt-in fused Rgb output path to cover native 4:2:0 sources (the common real-world subsampling), and updates the benchmark harness + documentation to address the v3.11 JPEG benchmark review feedback end-to-end.
Changes:
- Implement fused native-4:2:0 → RGB write (
write_rgb_rows_420) using a 2×2 nearest-neighbour (box) chroma upsample fused into the MCU write path, reusing existing YCbCr→RGB row kernels. - Add/adjust correctness tests for the new 4:2:0 fused RGB path and for rejecting unsupported subsampling shapes (e.g. true 4:2:2) in fused RGB mode.
- Update benchmarking harness/docs (including a new
EDGEFIRST_WUFFS_FORCE_4BPPtoggle) and refresh BENCHMARKS.md narrative/tables to reconcile prior captures and document the new measurements.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/codec/TESTING.md | Expands the test coverage matrix description to include fused RGB for native 4:2:0 and rejection behavior for other subsamplings. |
| crates/codec/src/jpeg/mod.rs | Extends output-format resolution so Rgb can be selected for native 4:2:0 (when sampling matches is_native_420). |
| crates/codec/src/jpeg/mcu.rs | Adds native-4:2:0 fused RGB writer (write_rgb_rows_420), scratch upsample buffers, and new unit tests. |
| crates/codec/src/decoder.rs | Updates public decoder docs to describe fused RGB behavior for 4:4:4 and native 4:2:0. |
| crates/codec/examples/dump_rgb420.rs | Adds a local helper example to dump fused 4:2:0→RGB output as PPM for accuracy comparisons. |
| crates/codec/ARCHITECTURE.md | Updates architecture docs to describe the new fused 4:2:0→RGB path and its accuracy/speed tradeoffs. |
| CHANGELOG.md | Documents the new fused native-4:2:0→RGB decode capability under Unreleased. |
| benchmarks/modules/wuffs/bench.c | Adds EDGEFIRST_WUFFS_FORCE_4BPP to reorder Wuffs pixel format probing for a targeted 3bpp vs 4bpp measurement. |
| BENCHMARKS.md | Incorporates the full v3.11 review response: scope notes, reconciliations, new measurements, and “superseded” annotations. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+25
to
+27
| /// Full-width horizontally-upsampled Cb/Cr rows for the fused 4:2:0→RGB | ||
| /// write (see `write_rgb_rows_420`). Empty until first grown by | ||
| /// `ensure_capacity`/`new` for an image wide enough to need them. |
| * for an isolated 3-byte-swizzle-vs-4-byte-native A/B on Wuffs' own | ||
| * decoder (not a comparison against another arm) — see BENCHMARKS.md | ||
| * § JPEG Decode's Wuffs accuracy/performance note. */ | ||
| int force_4bpp = getenv("EDGEFIRST_WUFFS_FORCE_4BPP") != NULL; |
|
Rebuild and push the multi-arch edgefirst-hal-jpeg-bench container against the fused native-4:2:0->RGB codec (native amd64 build on sebstation, native arm64 build locally, combined into one manifest), then re-run the full 30-job AWS Batch matrix (5 queues x 6 corpora, not just the val2017 headline that the second-instance run covered). EdgeFirst is fastest in every cloud cell, YUV and RGB, every corpus. Bonus finding: the prior revision's m7i cross-corpus DRI anomaly (nonphysical negative delta, caused by a val2017/val2017-dri pair landing on different-turbo-bin instances) does not reproduce on this fresh all-corpora capture -- every queue now shows a physically sensible positive DRI penalty, consistent with the second-instance finding that m7i's absolute latency (not its competitive ratio) is what varies instance-to-instance. Signed-off-by: Sébastien Taylor <[email protected]>
Response to docs/BENCHMARKS_JPEG_REVIEW_v3.12.md: - Add mbp-m2-max to the libjpeg-turbo provenance table (Homebrew jpeg-turbo 3.2.0, Apple clang 21.0.0, macOS 27.0); note the GNU-assembler-NEON-removal caveat is a Linux/Android toolchain concern, not verified to apply to Apple's build - State explicitly that the fused native-4:2:0 box chroma upsample is opt-in (same set_output_format gate as every fused decode output), not a default -- there is no "default RGB output" to compare - Confirm and document that the 4:2:0 YUV ratio shift between captures is measurement variance, not attributable to write_rgb_rows_420: verified write_nv12_rows/write_nv16_nv24_rows are untouched by that diff - Cross-reference the Wuffs 3-byte-swizzle-slow-path caveat from the AWS cloud baselines section Signed-off-by: Sébastien Taylor <[email protected]>
… tooling Response to docs/BENCHMARKS_JPEG_REVIEW_v3.12.md's blocking and high items: - turbojpeg_bench: add --upsample accurate|fast (TJFLAG_FASTUPSAMPLE), mirroring the existing --dct accurate|fast pattern. Turbo's default is fancy/triangle upsampling; --upsample fast selects its box/ nearest-neighbour path -- the accuracy class EdgeFirst's fused native-4:2:0 write uses. Without this, the RGB tables compared a cheaper algorithm (EdgeFirst) against a more expensive one (turbo's default) and called the difference a performance lead. - turbojpeg_bench: add EDGEFIRST_TURBOJPEG_LIB to dlopen an exact libturbojpeg path instead of the built-in candidate search, for A/B testing a source-built libjpeg-turbo against the distro-packaged one on the same host without uninstalling either. - decode-ab-sweep.sh / decode-ab-sweep-macos-local.sh: add a tj_fastupsample arm (RGB-only) alongside tj_islow/tj_ifast. - docker: capture full container provenance (build toolchain, base image, packaged libturbojpeg version+arch, resolved lib path) to <board>_provenance.txt per job. Add a second build stage that compiles libjpeg-turbo 3.1.2 from source and bakes it into the same image as an alternate .so (opt in via EDGEFIRST_TURBOJPEG_LIB), so the source-built-turbo control doesn't need a separate image variant. - benchmarks/scripts/decode-ab-sweep-macos-local.sh: commit the mbp-m2-max local-runner script (previously only in a session scratchpad) -- the most reproducible board in the sweep is now actually reproducible. Verified: turbojpeg_bench builds clean (native + aarch64 cross); --upsample fast measurably faster than accurate on real corpora; EDGEFIRST_TURBOJPEG_LIB correctly loads an alternate .so; Docker image builds clean (arm64), provenance capture and source-built-turbo override both verified working inside the container. Signed-off-by: Sébastien Taylor <[email protected]>
…g fix Adds the turbo TJFLAG_FASTUPSAMPLE (box/nearest-neighbour, matched IDCT accuracy class) comparison the v3.12 review asked for, alongside the existing default-fancy-upsample RGB tables, on boards and AWS. This surfaces a real result the fancy-upsample comparison was masking: on the two in-order cores (A53, A55), EdgeFirst's RGB lead collapses to near-parity or a narrow loss (0.97x-0.98x) on two of three native-4:2:0 corpora once turbo's own box upsample is the baseline, though EdgeFirst stays ahead on the DRI corpus on those boards and on every out-of-order core throughout (boards and all five AWS queues). Also adds a macOS QoS scheduling hint (advisory-only; macOS has no accessible core-affinity API) plus a 5-round default to the mbp-m2-max local runner, cutting worst observed cross-round spread from 7.7% to 6.6%, and re-captures its full six-corpus sweep under the new protocol. BENCHMARKS.md documents all of this plus the resolution of the turbo-version "confound" the review flagged (same-host source-built 3.1.2 A/B via a new EDGEFIRST_TURBOJPEG_LIB override shows no real effect; better-supported explanation is in-order vs out-of-order core class) and adds container/build provenance capture to every AWS Batch job. Signed-off-by: Sébastien Taylor <[email protected]>
…path Blocking: added a pixel-parity harness (benchmarks/modules/rgb_parity, dlopen-linked against libjpeg-turbo) comparing EdgeFirst's box-upsample RGB against turbo TJFLAG_FASTUPSAMPLE, with a phase-shift diagnostic that would catch a systematic half-pixel replication misalignment. 0/720 images across four native-4:2:0 corpora score better at a nonzero shift; worst-case PSNR 51.2-51.3 dB, max|delta| 3 - the phase is measured correct, not assumed. Also addresses the review's remaining checklist items: - set_output_format(Some(Rgb)) on a source that is neither 4:4:4- equivalent nor native 4:2:0 now returns CodecError::UnsupportedFormat instead of silently substituting the native NV12/NV16/Grey format. - Unit tests for expand_row_2x/write_rgb_rows_420 at 1x1, odd x even, even x odd, and odd x odd dimensions. - ChromaUpsample (#[non_exhaustive], Box only for now) and ImageDecoder::set_chroma_upsample, so a future fancy-upsample mode is an additive variant rather than a breaking API change. - Direct (non-dispatch) parity tests for the SSE4.1 and NEON fused-RGB block kernels - the prior test only ever exercised whichever tier the CI/dev host's CPU happened to select. - Re-ran the fast-class RGB comparison against matched ifast+TJFLAG_FASTUPSAMPLE on the two in-order boards plus one out-of-order board for contrast. Found a real loss cell on imx8mp-frdm/CLIC 4:2:0 (0.96x, non-overlapping round spreads) - the same in-order-core effect already documented for the accurate class, published as-is per the review's own option rather than treated as a release blocker. BENCHMARKS.md bumped to 3.14 with the full writeup and changelog entry. Signed-off-by: Sébastien Taylor <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Summary
Addresses
docs/BENCHMARKS_JPEG_REVIEW_v3.11.mdend to end — both the documentation cleanups and the real objections that needed new measurement or implementation work.New feature (
crates/codec): the opt-in fusedset_output_format(Some(Rgb))decode path previously covered only 4:4:4 sources, silently falling back to native NV12 for the dominant real-world subsampling (4:2:0) — exactly the cell a libjpeg-turbo-literate reviewer would expect EdgeFirst to lose on. Implementedwrite_rgb_rows_420: a 2×2 nearest-neighbour (box) chroma upsample fused into the MCU write, reusing the existing SIMDycbcr_to_rgb_rowkernels on the upsampled row. EdgeFirst wins the RGB arm on every board/corpus measured, including the one host (A53) where the YUV arm was a dead heat — with zero fast-class loss cells. Box (not fancy/triangle) upsampling is a deliberate speed tradeoff, measured at 44–50 dB PSNR against a reference decode (comparable to the already-documentedDctMethod::Fastaccuracy cost). No public API change.Benchmark harness: added
EDGEFIRST_WUFFS_FORCE_4BPPtowuffs_benchto isolate Wuffs' 3-byte-swizzle-vs-4-byte-native cost (closes review item 2.3).BENCHMARKS.md — full review response:
Image Codec DecodeJPEG rows; reconciled the nvJPEG June-vs-August conclusions; scoped the "two releases behind" banner to the GL/preprocessing matrix onlyneonfeature is genuinely engaged (not inferred); measured Wuffs' RGB slow-path penalty; published per-board libjpeg-turbo version/compiler provenance; addedmbp-m2-maxto the full 8-arm sweep (largest accurate-class lead of any board, 1.41×)Deferred per explicit product decision: fast-mode mAP impact (after this release + profiler update) and per-queue AWS second-instance runs beyond the headline corpus.
Test plan
cargo test -p edgefirst-codec— all passing, including newwrite_rgb_rows_420correctness tests (matches NV12 decode + box upsample + existing colour kernel, byte-for-byte) and a rejection test for genuinely non-4:2:0 subsamplings (4:2:2)cargo fmt --check,cargo clippy --workspace --all-targets -- -D warnings— cleancargo test --workspace— clean (2 pre-existing macOS GL parallel-test flakes confirmed viagit stash, unrelated to this diff)bench.c) builds clean natively and cross-compiled (zig cc, aarch64), zero warningsdump_rgb420example used to measure real PSNR against a reference decode on 3 test fixtures🤖 Generated with Claude Code