Skip to content

perf: separable triangle resample for screenshot blur masks - #2979

Merged
richiemcilroy merged 3 commits into
CapSoftware:mainfrom
kyzer1023:perf/screenshot-blur-mask
Oct 24, 2026
Merged

richiemcilroy merged 3 commits into
CapSoftware:mainfrom
kyzer1023:perf/screenshot-blur-mask

Conversation

@kyzer1023

@kyzer1023 kyzer1023 commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Screenshot blur masks scale a region down with a triangle filter and then scale it back up. This used image::imageops::resize twice. A separable resampler takes the same samples and produces byte-identical output, and it is 4x to 12x faster. Pixelate masks are unchanged.

Change

mask_filter in apps/desktop-gpui/kernels/src/screenshot.rs still uses pixelate_region when pixelate is set. The blur branch now resamples the region directly: a vertical pass into f32, then a horizontal pass that rounds to u8. The downsample factor is still (level / 4).round().max(2), and a second resample restores the original size. The weights follow the triangle kernel in image 0.25.10, including the scaled support and the division by the weight sum. Rounding truncates x + 0.5 and steps back by one just below a midpoint. That matches f32::round on [0, 255] without the libm call.

Benchmarks

Command: cargo bench --bench screenshot_blur

Release profile with lto = "thin" and codegen-units = 1. Each time is the median of alternating before and after runs, after 3 discarded warmup iterations. The bench asserts the full buffers are equal before it records times, and passes each finished image to black_box so no pixel work can be optimized away. Toolchain: rustc 1.95.0.

Machine: Intel Xeon, family 6 model 207, 4 vCPU, x86_64, KVM.

Region Level n Before After Speedup
400x300 16 21 3.048 ms 0.700 ms 4.35x
400x300 8 21 3.691 ms 0.689 ms 5.36x
400x300 40 21 2.532 ms 0.448 ms 5.65x
1920x1080 16 21 65.25 ms 10.18 ms 6.41x
1920x1080 8 21 72.95 ms 11.23 ms 6.50x
1920x1080 40 21 49.72 ms 7.77 ms 6.40x
3840x2160 16 11 550.5 ms 51.2 ms 10.74x
3840x2160 8 11 697.7 ms 87.0 ms 8.02x
3840x2160 40 11 523.0 ms 40.8 ms 12.81x

Max absolute difference is 0, and so is the mean.

Tests

  • cargo test --release on cap-gpui-kernels: 65 passed, 0 failed, 1 ignored. The ignored test is the existing waveform benchmark.
  • Screenshot module tests after the review fixes: 13 passed, 0 failed.
  • blur_matches_triangle_resize_for_full_byte_range uses frames whose channels include 0 and 255 (including opaque alpha), plus solid 0, solid 255 and a sub-rectangle, and compares byte for byte with the two image Triangle resizes.
  • quantize_channel_matches_clamped_round compares quantize_channel with value.clamp(0.0, 255.0).round() as u8 for every half-integer from 0.5 to 254.5 and its neighboring floats, the integers 0 to 255, negatives down to negative infinity, and values above 255 up to positive infinity.
  • blur_matches_triangle_resize_across_sizes_levels_and_odd_dimensions compares the new blur with two image Triangle resizes. It covers odd sizes, sub-rectangles, empty edges, levels from 0.5 to 100, and 400x300 at levels 8, 16 and 40.
  • pixelate_matches_two_nearest_resizes still passes.
  • cargo fmt, cargo check and cargo clippy --all-targets -- -D warnings are clean.

Note: the cloud machine could not resolve the workspace gpui patch, so these ran from a standalone copy of apps/desktop-gpui/kernels with the same sources and image pinned to 0.25.10. CI is the first full-workspace build.

Test plan

  • cargo test -p cap-gpui-kernels --release
  • cargo bench -p cap-gpui-kernels --bench screenshot_blur
  • In the desktop app, add a blur mask to a screenshot at a few levels and confirm it looks the same as on main. Then confirm a pixelate mask is unchanged.

Risk

The blur bytes match the triangle resize in image 0.25.10 for every size covered here. Blur no longer calls imageops::resize, so later image releases will not change this path. The horizontal loops read through pointers that stay inside the precomputed tap window. Pixelate is untouched.

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; both previous findings are addressed and no new blocking issue was found.

Summary

Replaces screenshot blur's two Triangle resizes with a separable resampler. Pixelate remains unchanged.

  • Screenshot blur masks use a faster Triangle resampler.

Reviews (2) · Last reviewed commit: "fix: cover screenshot blur bench output ..." · Reviewed by Greptile

@vercel

vercel Bot commented Oct 23, 2026

Copy link
Copy Markdown
Contributor

@kyzer1023 is attempting to deploy a commit to the Cap Software Inc Team on Vercel.

A member of the Team first needs to authorize it.

)
};
let elapsed = start.elapsed().as_nanos();
black_box(image.unwrap().into_raw().len());

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 Timings may skip pixel work

time_call passes only the output length to black_box. That length does not depend on the computed pixels, so an optimized build may remove pixel calculations and distort the speedup comparison. Pass the whole image or its pixel buffer to black_box instead.

Suggested change
black_box(image.unwrap().into_raw().len());
black_box(image.unwrap());
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/kernels/benches/screenshot_blur.rs
Line: 58

Comment:
**Timings may skip pixel work**

`time_call` passes only the output length to `black_box`. That length does not depend on the computed pixels, so an optimized build may remove pixel calculations and distort the speedup comparison. Pass the whole image or its pixel buffer to `black_box` instead.

```suggestion
    black_box(image.unwrap());
```

---

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

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in f28779a. time_call now passes the finished image to black_box. Reran with the same profile: 1080p level 16 is 65.25 ms vs 10.18 ms (6.41x), 4K level 16 is 550.5 ms vs 51.2 ms (10.74x).

Comment on lines +744 to +745
let rgba: Vec<u8> = (0..width * height * 4)
.map(|i| (i.wrapping_mul(37).wrapping_add(19) % 251) as u8)

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 Rounding boundaries lack direct tests

The new blur tests generate every channel modulo 251, so they never test 255, including the fully opaque alpha used by ordinary screenshots. They also do not directly test the midpoint correction in quantize_channel.

Add full-range image cases and a direct test comparing quantize_channel with value.clamp(0.0, 255.0).round() as u8 at each half-integer and the adjacent representable values. This would catch rounding changes that the current image cases may miss.

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

Comment:
**Rounding boundaries lack direct tests**

The new blur tests generate every channel modulo 251, so they never test 255, including the fully opaque alpha used by ordinary screenshots. They also do not directly test the midpoint correction in `quantize_channel`.

Add full-range image cases and a direct test comparing `quantize_channel` with `value.clamp(0.0, 255.0).round() as u8` at each half-integer and the adjacent representable values. This would catch rounding changes that the current image cases may miss.

---

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!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in f28779a. blur_matches_triangle_resize_for_full_byte_range covers frames with 0 and 255 in every channel (including opaque alpha), solid 0, solid 255 and a sub-rectangle, compared byte for byte with the two Triangle resizes. quantize_channel_matches_clamped_round checks every half-integer from 0.5 to 254.5 and its neighboring floats, 0 to 255, negatives and values above 255 (including infinities) against value.clamp(0.0, 255.0).round() as u8.

@richiemcilroy

Copy link
Copy Markdown
Member

Thanks @kyzer1023, this looks great: we checked the blur output is byte-identical to the previous image resize (same weights, order and rounding), and the equality test passes in CI. Before we merge, could you pick up the two Greptile notes so the review comes back clean?

  1. apps/desktop-gpui/kernels/benches/screenshot_blur.rs:58: pass the image (or its pixel buffer) to black_box rather than its length, so the optimizer can't drop the pixel work being timed.
  2. apps/desktop-gpui/kernels/src/screenshot.rs tests: the generated channels are all % 251, so 255 (the opaque alpha every screenshot has) is never exercised. Please add a full-range case including 255, and a direct test of quantize_channel against value.clamp(0.0, 255.0).round() as u8 at the half-integer points.

Pass the whole blur image to black_box. Add a full-range byte match and a direct quantize_channel test.

Copy link
Copy Markdown
Contributor Author

Thanks @richiemcilroy. Both notes are addressed in f28779a: the bench passes the whole image to black_box, and there are new full-range (0 and 255) and direct quantize_channel tests. Output is still byte-identical, speedups are unchanged (6.4x at 1080p, 10.7x at 4K level 16), and the description is updated.

@greptileai review

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