Skip to content

Optimize and expose WebP encoding - #2

Merged
taskylizard merged 1 commit into
taskylizard:trunkfrom
xirreal:perf/expose-fast-webp-encoder
Aug 12, 2026
Merged

Optimize and expose WebP encoding#2
taskylizard merged 1 commit into
taskylizard:trunkfrom
xirreal:perf/expose-fast-webp-encoder

Conversation

@xirreal

@xirreal xirreal commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • replace the wrapper-based WebP path with a validated libwebp-sys encoder and expose bounded-memory streaming plus high-throughput batch APIs from maple-render-core
  • accelerate batch animations with dirty rectangles, parallel frame encoding, memory-aware scheduling, and mux assembly while preserving lossless pixels and timing
  • harden input, timestamp, duration, keyframe, dimension, and native initialization handling; add integration tests, fuzzing, CodSpeed coverage, native/MSRV CI, and publishing checks
  • publish the reusable core API as maple-render-core 0.3.0 and re-export it from Maple on native targets

Performance

On the included generated 640x360x16 workload (lossy quality 85, method 0), the optimized build measured:

Workload Streaming Batch Speedup Output
full-frame changes 88.8 ms 7.83 ms 11.3x 1,257,170 B vs 1,257,360 B
dirty-region changes 15.0 ms 5.89 ms 2.5x 88,374 B vs 87,976 B

The batch API intentionally trades retained input frames and independent rectangle compression for throughput. The streaming API remains the bounded-memory default.

Alternative evaluated

zenwebp 0.4.4 has promising pure-Rust, SIMD, mux, animation, and no_std support, but it is not suitable here today:

  • it is AGPL-3.0-only or commercially licensed, incompatible with Maple's MIT distribution without a commercial agreement
  • it requires Rust 1.93, above Maple's tested Rust 1.88 MSRV
  • its high-level animation encoder is sequential, while this change's main consumer requirement is parallel method-0 batch throughput
  • its published encoder comparisons cover methods 4-6, not Maple/Blossom's latency-oriented method-0 workload

Verification

  • cargo fmt --all -- --check
  • cargo clippy --locked --all-targets -- -D warnings
  • root and standalone core tests, including 10 unit and 6 WebP integration tests
  • default and no-default-feature checks/tests
  • wasm32-unknown-unknown library check
  • Rust 1.88 root/core checks
  • maple-render-core package verification
  • CodSpeed WebP benchmark build
  • fuzz target build

@codspeed-hq

codspeed-hq Bot commented Aug 12, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by 11.17%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
✅ 19 untouched benchmarks
🆕 2 new benchmarks

Performance Changes

Benchmark BASE HEAD Efficiency
webp_single_frame_lossless 1.8 s 1.6 s +11.17%
🆕 webp_batch_serialize_fast N/A 156.8 ms N/A
🆕 webp_serialize_fast N/A 350 ms N/A

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing xirreal:perf/expose-fast-webp-encoder (fbccc8b) with trunk (5d9a6df)

Open in CodSpeed

@taskylizard

Copy link
Copy Markdown
Owner

@codspeedbot explain why is this faster?

@taskylizard
taskylizard merged commit ffa1095 into taskylizard:trunk Aug 12, 2026
7 checks passed
@codspeed-hq

codspeed-hq Bot commented Aug 12, 2026

Copy link
Copy Markdown

@codspeedbot explain why is this faster?

Why webp_single_frame_lossless got ~11% faster

Short answer: it's the libwebp backend swap, not the Rust-side changes. The benchmark body is unchanged (same 1 frame, same WebpOptions { lossless: true, .. }), and the whole call stack is still encode_single → add_frame → WebPAnimEncoderAdd → VP8LEncodeImage. What changed is which native library that stack lands in:

BASE (trunk) HEAD
Native dep webp-animation 0.10libwebp-sys2 0.2.0 libwebp-sys =0.14.4
Lossless entropy kernel CombinedShannonEntropy_SSE2 (dsp/lossless_enc_sse2.c) CombinedShannonEntropy_AVX2 (dsp/lossless_enc_avx2.c)
LZ77 match scan VectorMismatch_SSE2 VectorMismatch_AVX2
Total 1.8 s 1.6 s

1. AVX2 lossless-encoder kernels (the main win)

The bundled libwebp in libwebp-sys 0.14.4 ships src/dsp/lossless_enc_avx2.c, which the older vendored copy behind webp-animation 0.10 didn't have at all — so on the same hardware class the base run could only dispatch to SSE2. Both callers of the entropy kernel got cheaper:

  • CombinedShannonEntropy: 401.6 ms (SSE2) → 361.1 ms (AVX2)
  • inside it, the pack step alone: _mm_packs_epi32 85.1 ms → _mm256_packs_epi32 62.8 ms (2× wider vectors, half the ops)

Rolled up to the three top-level lossless stages:

Stage BASE HEAD Δ
VP8LGetHistoImageSymbols 682.9 ms 608.6 ms −74.3 ms
VP8LResidualImage 586.0 ms 513.3 ms −72.7 ms
VP8LColorSpaceTransform 247.0 ms 225.7 ms −21.3 ms

That's ~168 ms of the ~200 ms total delta.

2. Fewer entropy evaluations in the newer histogram code

GetEntropyUnrefined_C goes from 6 calls / 73.7 ms to 2 calls / 32.2 ms — the extra PopulationCost invocations present in the old version are gone. GetCombinedEntropyUnrefined_C also drops 395 ms → 372.7 ms. This is an upstream algorithmic change in the newer libwebp, on top of the SIMD win.

3. What did not move the needle here

Dropping the webp-animation wrapper removes one full-frame memcpy on the way into the encoder (visible in the base flamegraph as encode_single → __memcpy_avx_unaligned_erms), but the Rust layer is only ~0.2–0.3% of this benchmark — 99.6%+ of the time is inside VP8LEncodeStream either way. The wrapper removal matters for API shape and for the new batch path, not for this number.

Consistent with that: webp_single_frame (lossy) is unchanged, because the lossy VP8 encoder doesn't go through the lossless AVX2 kernels at all.

One caveat on the number

CodSpeed flagged an environment difference on this comparison: base ran on an EPYC 9V74, head on an EPYC 7763. In Simulation mode the cache model is derived from the physical CPU, so a few percent of the 11.17% may be runner-attributable. The direction and mechanism are real though — the SSE2→AVX2 dispatch and the reduced call counts are structural properties of the two libwebp versions, visible in the profiles regardless of runner.

The 11.3× figure in the PR description is a separate story: that's the new batch API (webp_batch_serialize_fast, 156.8 ms vs webp_serialize_fast, 350 ms in these runs), driven by dirty rectangles and parallel frame encoding rather than by the backend swap.

No code changes were needed for this question, so nothing has been pushed.

Summary
  • Warming up the performance wizard...
  • Investigating the WebP speedup — Root-caused the 11.17% improvement to the libwebp backend swap (AVX2 lossless kernels + cheaper histogram cost path)
  • Handling comment — Comment handling finished successfully

Was this response helpful? React with 👍 or 👎 to help us improve CodSpeedBot.

@taskylizard

Copy link
Copy Markdown
Owner

@codspeedbot can you create a new PR? Just asking.

@codspeed-hq

codspeed-hq Bot commented Aug 12, 2026

Copy link
Copy Markdown

@codspeedbot can you create a new PR? Just asking.

Not from here, no — when I'm triggered from a PR comment I work on that PR's branch (perf/expose-fast-webp-encoder) and push commits to it. I can't open a separate, unrelated pull request.

What I can do on this PR:

  • make and commit changes to benchmarks, the CodSpeed workflow, or the CodSpeed setup
  • investigate performance results (like the WebP speedup breakdown above)

If you want a change landed on a different branch, the usual path is to open the PR yourself and mention me there.

Summary
  • Warming up the performance wizard...
  • Handling comment — Comment handling finished successfully

Was this response helpful? React with 👍 or 👎 to help us improve CodSpeedBot.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants