Repository navigation
perf(gpui): SIMD vectorize PNG adaptive filters and eliminate buffer copies - #2674
nikolas-sapa wants to merge 2 commits into
Conversation
|
@nikolas-sapa is attempting to deploy a commit to the Cap Software Inc Team on Vercel. A member of the Team first needs to authorize it. |
| for i in 0..len { | ||
| let diff = tail_c[i].wrapping_sub(tail_p[i]); | ||
| tail_o[i] = diff; | ||
| sum += u64::from((diff as i8).unsigned_abs()); |
There was a problem hiding this comment.
Loops violate required Rust style
The new loops index slices with a range variable. The repository instructions require slice iteration or .iter().enumerate() instead. Use zipped iterators for these equal-length slices. The same pattern appears at lines 353–356 and 835–837, and elsewhere in the added filters. This repository requirement must be satisfied before merging.
Context Used: AGENTS.md (source)
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/kernels/src/png.rs
Line: 282-285
Comment:
**Loops violate required Rust style**
The new loops index slices with a range variable. The repository instructions require slice iteration or `.iter().enumerate()` instead. Use zipped iterators for these equal-length slices. The same pattern appears at lines 353–356 and 835–837, and elsewhere in the added filters. This repository requirement must be satisfied before merging.
**Context Used:** AGENTS.md ([source](https://github.com/capsoftware/cap/blob/main/AGENTS.md))
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| color, | ||
| } | ||
| }) | ||
| .collect() |
There was a problem hiding this comment.
Image batches retain every buffer
The CLI branch now decodes every image into targets before benchmarking the first one. Previously, it decoded and released one image at a time. Large screenshot batches now need memory for all decoded images at once, which can cause swapping, distort timings, or stop the benchmark with an out-of-memory failure. Keep CLI targets lazy and decode each image just before benchmarking it.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/kernels/benches/png_encode.rs
Line: 93
Comment:
**Image batches retain every buffer**
The CLI branch now decodes every image into `targets` before benchmarking the first one. Previously, it decoded and released one image at a time. Large screenshot batches now need memory for all decoded images at once, which can cause swapping, distort timings, or stop the benchmark with an out-of-memory failure. Keep CLI targets lazy and decode each image just before benchmarking it.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.|
|
||
| let mut ref_out = vec![0u8; len]; | ||
| apply_filter(2, bpp, &prev, &curr, &mut ref_out); | ||
| let ref_cost = sum_abs(&ref_out); |
There was a problem hiding this comment.
Cost tests share their implementation
The expected cost uses sum_abs, which this PR also replaces with NEON on aarch64. Both sides therefore use the same vector absolute-value and sum operations, so a shared cost bug could pass every assertion. Compute the expected cost with a scalar iterator in this test, including explicit 0x80 values, to check the new cost code independently.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/kernels/src/png.rs
Line: 842
Comment:
**Cost tests share their implementation**
The expected cost uses `sum_abs`, which this PR also replaces with NEON on `aarch64`. Both sides therefore use the same vector absolute-value and sum operations, so a shared cost bug could pass every assertion. Compute the expected cost with a scalar iterator in this test, including explicit `0x80` values, to check the new cost code independently.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
|
Thanks @nikolas-sapa! Greptile is at 4/5 here. We only merge once Greptile reaches 5/5, so please address its findings in the summary above (or reply to them if you think one is wrong), then comment |
Replace the range-indexed loops in the new NEON filter tails/heads with
zipped slice iterators, matching the AGENTS.md rule that bans
`for i in 0..v.len() { v[i] ... }`. The pre-existing scalar `apply_filter`
fallback is left untouched.
Decode CLI benchmark targets one at a time instead of collecting every
decoded image up front. Peak RSS on eight 1200x900 targets drops from
59.6 MB to 35.7 MB.
Compute expected filter costs with a scalar i16 fold rather than
`sum_abs`, which this PR also gives a NEON path built from the same
vabsq_s8/vaddlvq_u8 pair the filters use. Seed 0x80 bytes into the test
inputs so the saturating-abs boundary is actually exercised.
|
Addressed all three Greptile findings in 1. Measured peak RSS over eight 1200x900 targets:
~23.9 MB saved, which is the ~25.9 MB of decoded buffers that were being held simultaneously. Wall time was unchanged (1.30 s -> 1.22 s), so this is a memory fix, not a timing one. 2.
3. I checked this test is not vacuous: mutating the NEON abs to the saturating Verification: |
|
@greptileai review |
|
@richiemcilroy addressed — details in my summary above. Quick version: rebased onto current One thing worth knowing across these PRs: Greptile's findings on #2668, #2671, #2672 and #2673 were already fixed in commits pushed shortly after it reviewed — nobody had posted
|
| // Expected costs must not come from `sum_abs`: this PR gives it a NEON path | ||
| // built from the same `vabsq_s8`/`vaddlvq_u8` pair the filters use, so a wrong | ||
| // absolute-value or reduction would cancel out on both sides of the assert. | ||
| fn scalar_filter_cost(buf: &[u8]) -> u64 { |
There was a problem hiding this comment.
scalar_filter_cost is compiled for tests on every platform, but its only callers are inside the aarch64-only test. Tests on x86_64 therefore gain an unused-function warning. Add #[cfg(target_arch = "aarch64")] to the helper so it follows the test's platform limit.
| fn scalar_filter_cost(buf: &[u8]) -> u64 { | |
| #[cfg(target_arch = "aarch64")] | |
| fn scalar_filter_cost(buf: &[u8]) -> u64 { |
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/kernels/src/png.rs
Line: 709
Comment:
**Non-ARM tests gain a warning**
`scalar_filter_cost` is compiled for tests on every platform, but its only callers are inside the `aarch64`-only test. Tests on x86_64 therefore gain an unused-function warning. Add `#[cfg(target_arch = "aarch64")]` to the helper so it follows the test's platform limit.
```suggestion
#[cfg(target_arch = "aarch64")]
fn scalar_filter_cost(buf: &[u8]) -> u64 {
```
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.|
Thanks! I checked this for output equivalence and it matches main: the vabsq_s8 0x80 wrap, the per-vector sum range, the floor average and Paeth tie rules all agree with the scalar adaptive_filter, and the tail handling is right. Before we merge:
|
Summary
• Vectorizes all 4 PNG adaptive filter encoders (Up, Sub, Avg, Paeth) in
apps/desktop-gpui/kernels/src/png.rsusing ARM NEON on aarch64 with unrolled 64-byte block processing.• Zero-copy Up filter: Filter 2 (the initial filter evaluated by
adaptive_filter) filters directly into the output row buffer while accumulating the absolute sum in vector registers, eliminating the intermediate candidate buffer write and subsequent memcpy.• Vectorized Average filter (
vhaddq_u8): evaluates floor halving addition and subtraction in vector instructions with zero scalar division or bitwise overhead.• Vectorized Paeth filter: evaluates parallel 8-lane 16-bit absolute distance comparisons and bitselect (
vbslq_u16), eliminating all per-pixel scalar branches.• Fast SIMD
sum_abs: replaces the double-nested.chunks(32).map().sum()iterator loop with NEON vector absolute and horizontal pairwise addition (vabsq_s8+vaddlvq_u8).• Unit tests: adds
neon_filters_match_scalar_referenceverifying all 4 filters and their cost calculations match the scalar reference byte-for-byte across RGB and RGBA formats and varied row lengths.• Benchmark harness: adds default synthetic screen workloads (1080p, Retina, 4K) to
benches/png_encode.rswhen executed without CLI file arguments.Benchmark Results (Apple Silicon M-series)
Test Plan
• Run
cargo test --manifest-path apps/desktop-gpui/kernels/Cargo.toml• Run
cargo bench --manifest-path apps/desktop-gpui/kernels/Cargo.toml --bench png_encode• Run
cargo clippy --manifest-path apps/desktop-gpui/kernels/Cargo.toml

Confidence Score: 5/5
The PR appears safe to merge, with one non-blocking cleanup for an unused test helper.Findings
Fix with agent prompt
Summary
This PR adds ARM NEON PNG filters and default synthetic benchmark images.
Reviews (2) · Last reviewed commit: "fix(gpui): address Greptile findings on ..." · Reviewed by Greptile