Skip to content

perf(gpui): SIMD vectorize rgba_to_rgb and eliminate image conversion overhead - #2672

Open
nikolas-sapa wants to merge 2 commits into
CapSoftware:mainfrom
nikolas-sapa:perf/gpui-simd-rgba-to-rgb
Open

nikolas-sapa wants to merge 2 commits into
CapSoftware:mainfrom
nikolas-sapa:perf/gpui-simd-rgba-to-rgb

Conversation

@nikolas-sapa

@nikolas-sapa nikolas-sapa commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Vectorizes rgba_to_rgb in apps/desktop-gpui/kernels/src/codec.rs using aarch64 NEON SIMD (vld4q_u8 + vst3q_u8), processing 16 pixels (64 bytes in -> 48 bytes out) per iteration.
  • Adds an unrolled 4-pixel chunk portable fallback for non-aarch64 targets or trailing buffers.
  • Eliminates iterator and intermediate vector reallocation overhead from chunks_exact(4).flat_map(...).collect().
  • Updates encode_jpeg_rgba_as_rgb to call rgba_to_rgb directly instead of rgba.convert(), saving memory allocation and redundant pixel traversal during JPEG thumbnail and export encoding.
  • Includes bit-exact tests across all pixel boundaries and a benchmark harness in benches/rgba_to_rgb.rs.

Benchmark Results (Apple Silicon M-series)

Workload Pixels Baseline Median Optimized Median Speedup Correctness
thumbnail-320x180 57,600 81.8 µs 5.8 µs 14.1x Bit-exact
preset-216x122 26,352 36.5 µs 3.1 µs 11.8x Bit-exact
screen-720p 921,600 1,157 µs 130 µs 8.9x Bit-exact
screen-1080p 2,073,600 2,576 µs 417 µs 6.2x Bit-exact
screen-retina 5,939,136 7,293 µs 934 µs 7.8x Bit-exact
screen-4k 8,294,400 10,087 µs 1,145 µs 8.8x Bit-exact

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 rgba_to_rgb

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge, with a non-blocking benchmark correction.

Findings

  1. P2 Benchmark measures the wrong baseline ▶
Fix with agent prompt
### Issue 1
apps/desktop-gpui/kernels/benches/rgba_to_rgb.rs:5-9
`baseline_rgba_to_rgb` does not match the code this PR replaces. The base version already allocates the output once and fills it with `chunks_exact(4).zip(...)`; this benchmark instead measures `flat_map(...).collect()`. Its `before_ns` column therefore cannot show the speedup over the previous production code.

Copy the base implementation into the benchmark, and keep the iterator version as a separately named comparison if useful.

```suggestion
fn baseline_rgba_to_rgb(rgba: &[u8]) -> Vec<u8> {
    let len = rgba.len() / 4 * 3;
    let mut rgb = vec![0u8; len];
    for (src, dst) in rgba.chunks_exact(4).zip(rgb.chunks_exact_mut(3)) {
        dst[0] = src[0];
        dst[1] = src[1];
        dst[2] = src[2];
    }
    rgb
}
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Adds aarch64 NEON conversion, portable four-pixel loops, and direct RGB conversion before JPEG encoding.

  • RGBA pixels become RGB through grouped channel copying.
  • JPEG encoding from RGBA images uses the direct RGB byte path.

Reviews (2) · Last reviewed commit: "test(gpui): assert JPEG output against i..." · Reviewed by Greptile

@vercel

vercel Bot commented Oct 17, 2026

Copy link
Copy Markdown
Contributor

@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.

Comment thread apps/desktop-gpui/kernels/src/codec.rs Outdated
}
}

// Portable 4-pixel chunk unrolled fallback

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Comment repeats the code

The new // Portable 4-pixel chunk unrolled fallback comment only describes the loop below it. The repository guide explicitly bans comments that narrate what the code does. Remove it before merging to meet that requirement.

Suggested change
// Portable 4-pixel chunk unrolled fallback

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/codec.rs
Line: 45

Comment:
**Comment repeats the code**

The new `// Portable 4-pixel chunk unrolled fallback` comment only describes the loop below it. The repository guide explicitly bans comments that narrate what the code does. Remove it before merging to meet that requirement.

```suggestion

```

**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!

Comment on lines +127 to +128
let rgb = rgba_to_rgb(rgba.as_raw());
encode_jpeg_rgb(&rgb, rgba.width(), rgba.height(), quality)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 JPEG test compares identical paths

Routing encode_jpeg_rgba_as_rgb through encode_jpeg_rgb makes jpeg_paths_agree_on_the_same_pixels compare the same conversion and encoder calls with themselves. It no longer checks that this replacement preserves the old JPEG output. Keep an independent reference in the test using ConvertBuffer and encode_image, then compare it with the new path.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/kernels/src/codec.rs
Line: 127-128

Comment:
**JPEG test compares identical paths**

Routing `encode_jpeg_rgba_as_rgb` through `encode_jpeg_rgb` makes `jpeg_paths_agree_on_the_same_pixels` compare the same conversion and encoder calls with themselves. It no longer checks that this replacement preserves the old JPEG output. Keep an independent reference in the test using `ConvertBuffer` and `encode_image`, then compare it with the new path.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@richiemcilroy

Copy link
Copy Markdown
Member

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 @greptileai review to re-run it. Once it is 5/5 we will run the full verification suite on it.

@nikolas-sapa
nikolas-sapa force-pushed the perf/gpui-simd-rgba-to-rgb branch from 53ddcbd to ea2a513 Compare October 19, 2026 12:12
@nikolas-sapa

Copy link
Copy Markdown
Contributor Author

Rebased onto current main (a05ee24f7) — the branch was 256 commits behind. Two conflicts, both resolved by keeping both sides:

  • kernels/Cargo.toml — same as the others: upstream's frame_export entry plus this PR's rgba_to_rgb.
  • src/codec.rs — upstream has since added flatten_rgba_to_rgb_over_white immediately after rgba_to_rgb, and this PR replaced rgba_to_rgb with the NEON version. Kept both functions. Likewise kept both tests: upstream's fused_flatten_to_rgb_matches_two_step and this PR's rgba_to_rgb_matches_reference_across_lengths.

That pairing is a useful cross-check rather than just coexistence — upstream's test calls rgba_to_rgb(...) against this PR's NEON path across all 256x256 alpha/channel combinations, so it independently exercises the vectorised conversion.

On Greptile's two findings: both were already addressed in 53ddcbdaf, pushed ~25 minutes after Greptile commented but without re-triggering a review, so the comments were stale rather than open. For the record:

  • Comment repeats the code — the // Portable 4-pixel chunk unrolled fallback comment is gone.
  • JPEG test compares identical paths — jpeg_paths_agree_on_the_same_pixels now builds an independent reference via image::buffer::ConvertBuffer + JpegEncoder::encode_image, and asserts both the new encode_jpeg_rgba_as_rgb path and the old encode_jpeg_rgb(rgba_to_rgb(..)) path against it, rather than comparing them to each other.

Verification: cargo test -p cap-gpui-kernels -> 40 passed, 0 failed, 1 ignored.

@nikolas-sapa

Copy link
Copy Markdown
Contributor Author

@greptileai review

@nikolas-sapa

Copy link
Copy Markdown
Contributor Author

@richiemcilroy addressed — details in my summary above. Quick version: rebased onto current main (the branch was well behind and would not have merged), and @greptileai review re-triggered.

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 @greptileai review, so the comments were stale rather than open. #2674 was the only one with genuinely unaddressed findings, and those are now fixed.

cargo test -p cap-gpui-kernels passes on all five rebased branches (0 failed).

Comment on lines +5 to +9
fn baseline_rgba_to_rgb(rgba: &[u8]) -> Vec<u8> {
rgba.chunks_exact(4)
.flat_map(|px| [px[0], px[1], px[2]])
.collect()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Benchmark measures the wrong baseline

baseline_rgba_to_rgb does not match the code this PR replaces. The base version already allocates the output once and fills it with chunks_exact(4).zip(...); this benchmark instead measures flat_map(...).collect(). Its before_ns column therefore cannot show the speedup over the previous production code.

Copy the base implementation into the benchmark, and keep the iterator version as a separately named comparison if useful.

Suggested change
fn baseline_rgba_to_rgb(rgba: &[u8]) -> Vec<u8> {
rgba.chunks_exact(4)
.flat_map(|px| [px[0], px[1], px[2]])
.collect()
}
fn baseline_rgba_to_rgb(rgba: &[u8]) -> Vec<u8> {
let len = rgba.len() / 4 * 3;
let mut rgb = vec![0u8; len];
for (src, dst) in rgba.chunks_exact(4).zip(rgb.chunks_exact_mut(3)) {
dst[0] = src[0];
dst[1] = src[1];
dst[2] = src[2];
}
rgb
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/kernels/benches/rgba_to_rgb.rs
Line: 5-9

Comment:
**Benchmark measures the wrong baseline**

`baseline_rgba_to_rgb` does not match the code this PR replaces. The base version already allocates the output once and fills it with `chunks_exact(4).zip(...)`; this benchmark instead measures `flat_map(...).collect()`. Its `before_ns` column therefore cannot show the speedup over the previous production code.

Copy the base implementation into the benchmark, and keep the iterator version as a separately named comparison if useful.

```suggestion
fn baseline_rgba_to_rgb(rgba: &[u8]) -> Vec<u8> {
    let len = rgba.len() / 4 * 3;
    let mut rgb = vec![0u8; len];
    for (src, dst) in rgba.chunks_exact(4).zip(rgb.chunks_exact_mut(3)) {
        dst[0] = src[0];
        dst[1] = src[1];
        dst[2] = src[2];
    }
    rgb
}
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

@richiemcilroy

Copy link
Copy Markdown
Member

Thanks! The NEON rgba_to_rgb is correct (lane order, bounds, tail), but after #2529 merged, rgba_to_rgb has no hot production caller left: the only new one is encode_jpeg_rgba_as_rgb, which runs once per library thumbnail. The 6-14x is also measured against a flat_map().collect() baseline rather than main's implementation. If you'd like, cut this down to just the encode_jpeg_rgba_as_rgb simplification and we'll take that; otherwise we'll pass on the SIMD part.

This branch has not been deployed

No deployments
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