Repository navigation
perf(gpui): optimize screenshot alpha checks, flattening, and premultiplication #2673
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3ce4963
2913e91
b4b8603
7be067f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,349 @@ | ||
| use std::{hint::black_box, time::Instant}; | ||
|
|
||
| mod baseline { | ||
| pub fn is_opaque(rgba: &[u8]) -> bool { | ||
| const ALPHA_MASK: u128 = 0xff000000ff000000ff000000ff000000; | ||
| let mut blocks = rgba.chunks_exact(16); | ||
| blocks | ||
| .by_ref() | ||
| .all(|block| u128::from_le_bytes(block.try_into().unwrap()) & ALPHA_MASK == ALPHA_MASK) | ||
| && blocks | ||
| .remainder() | ||
| .iter() | ||
| .skip(3) | ||
| .step_by(4) | ||
| .all(|&alpha| alpha == 255) | ||
| } | ||
|
|
||
| pub fn has_translucent_pixel(rgba: &[u8]) -> bool { | ||
| rgba.iter().skip(3).step_by(4).any(|&alpha| alpha != 255) | ||
| } | ||
|
|
||
| pub fn flatten_onto_white(rgba: &[u8]) -> Vec<u8> { | ||
| let mut rgba = rgba.to_vec(); | ||
| for pixel in rgba.chunks_exact_mut(4) { | ||
| let alpha = u32::from(pixel[3]); | ||
| for channel in &mut pixel[..3] { | ||
| *channel = ((u32::from(*channel) * alpha + 255 * (255 - alpha) + 127) / 255) as u8; | ||
| } | ||
| pixel[3] = 255; | ||
| } | ||
| rgba | ||
| } | ||
|
|
||
| pub fn premultiply(rgba: &mut [u8]) { | ||
| for pixel in rgba.chunks_exact_mut(4) { | ||
| let alpha = u16::from(pixel[3]); | ||
| if alpha == 255 { | ||
| continue; | ||
| } | ||
| for channel in &mut pixel[..3] { | ||
| *channel = ((u16::from(*channel) * alpha + 127) / 255) as u8; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| pub fn demultiply(rgba: &mut [u8]) { | ||
| for pixel in rgba.chunks_exact_mut(4) { | ||
| let alpha = u32::from(pixel[3]); | ||
| if alpha == 255 || alpha == 0 { | ||
| continue; | ||
| } | ||
| for channel in &mut pixel[..3] { | ||
| let value = (u32::from(*channel) * 255 + alpha / 2) / alpha; | ||
| *channel = value.min(255) as u8; | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| use cap_gpui_kernels::screenshot as candidate; | ||
|
|
||
| fn main() { | ||
| println!("benchmark,workload,pixels,sample,before_ns,after_ns"); | ||
|
|
||
| for (name, width, height) in [ | ||
| ("1080p", 1920u32, 1080u32), | ||
| ("retina", 3024, 1964), | ||
| ("4k", 3840, 2160), | ||
| ] { | ||
| let num_pixels = (width * height) as usize; | ||
| let mut opaque = Vec::with_capacity(num_pixels * 4); | ||
| let mut mixed = Vec::with_capacity(num_pixels * 4); | ||
|
|
||
| for i in 0..num_pixels { | ||
| let r = (i * 7) as u8; | ||
| let g = (i * 13) as u8; | ||
| let b = (i * 31) as u8; | ||
| opaque.extend_from_slice(&[r, g, b, 255]); | ||
| let a = if i % 100 == 0 { (i % 256) as u8 } else { 255 }; | ||
| mixed.extend_from_slice(&[r, g, b, a]); | ||
|
Comment on lines
+79
to
+80
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The Add a mostly opaque workload with a translucent final pixel for Prompt To Fix With AIThis is a comment left during a code review.
Path: apps/desktop-gpui/kernels/benches/screenshot_alpha.rs
Line: 79-80
Comment:
**Late transparency goes unmeasured**
The `mixed` buffer always starts with alpha zero, so the new `is_opaque` checks stop at the first block. The benchmarks miss the costly case where transparency near the end makes a conversion scan almost the whole buffer before running its original loop.
Add a mostly opaque workload with a translucent final pixel for `flatten_onto_white`, `premultiply`, and `demultiply`. Also time `demultiply` on translucent input; its only current workload returns early. This would show the added scan cost rather than only the best-case fallback.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly. |
||
| } | ||
|
|
||
| let mut late_translucent = opaque.clone(); | ||
| if let Some(alpha) = late_translucent.last_mut() { | ||
| *alpha = 128; | ||
| } | ||
|
|
||
| { | ||
| let b = baseline::is_opaque(&opaque); | ||
| let c = candidate::is_opaque(&opaque); | ||
| assert_eq!(b, c); | ||
| for sample in 0..20 { | ||
| let start_o = Instant::now(); | ||
| black_box(baseline::is_opaque(black_box(&opaque))); | ||
| let old_t = start_o.elapsed().as_nanos(); | ||
|
|
||
| let start_n = Instant::now(); | ||
| black_box(candidate::is_opaque(black_box(&opaque))); | ||
| let new_t = start_n.elapsed().as_nanos(); | ||
| if sample >= 5 { | ||
| println!( | ||
| "is_opaque_solid,{name},{num_pixels},{},{old_t},{new_t}", | ||
| sample - 5 | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| { | ||
| let b = baseline::has_translucent_pixel(&opaque); | ||
| let c = candidate::has_translucent_pixel(&opaque); | ||
| assert_eq!(b, c); | ||
| for sample in 0..20 { | ||
| let start_o = Instant::now(); | ||
| black_box(baseline::has_translucent_pixel(black_box(&opaque))); | ||
| let old_t = start_o.elapsed().as_nanos(); | ||
|
|
||
| let start_n = Instant::now(); | ||
| black_box(candidate::has_translucent_pixel(black_box(&opaque))); | ||
| let new_t = start_n.elapsed().as_nanos(); | ||
| if sample >= 5 { | ||
| println!( | ||
| "has_translucent_solid,{name},{num_pixels},{},{old_t},{new_t}", | ||
| sample - 5 | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| { | ||
| let b = baseline::flatten_onto_white(&opaque); | ||
| let c = candidate::flatten_onto_white(&opaque); | ||
| assert_eq!(b, c); | ||
| for sample in 0..20 { | ||
| let start_o = Instant::now(); | ||
| black_box(baseline::flatten_onto_white(black_box(&opaque))); | ||
| let old_t = start_o.elapsed().as_nanos(); | ||
|
|
||
| let start_n = Instant::now(); | ||
| black_box(candidate::flatten_onto_white(black_box(&opaque))); | ||
| let new_t = start_n.elapsed().as_nanos(); | ||
| if sample >= 5 { | ||
| println!( | ||
| "flatten_white_solid,{name},{num_pixels},{},{old_t},{new_t}", | ||
| sample - 5 | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| { | ||
| let b = baseline::flatten_onto_white(&mixed); | ||
| let c = candidate::flatten_onto_white(&mixed); | ||
| assert_eq!(b, c); | ||
| for sample in 0..20 { | ||
| let start_o = Instant::now(); | ||
| black_box(baseline::flatten_onto_white(black_box(&mixed))); | ||
| let old_t = start_o.elapsed().as_nanos(); | ||
|
|
||
| let start_n = Instant::now(); | ||
| black_box(candidate::flatten_onto_white(black_box(&mixed))); | ||
| let new_t = start_n.elapsed().as_nanos(); | ||
| if sample >= 5 { | ||
| println!( | ||
| "flatten_white_mixed,{name},{num_pixels},{},{old_t},{new_t}", | ||
| sample - 5 | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| { | ||
| let b = baseline::flatten_onto_white(&late_translucent); | ||
| let c = candidate::flatten_onto_white(&late_translucent); | ||
| assert_eq!(b, c); | ||
| for sample in 0..20 { | ||
| let start_o = Instant::now(); | ||
| black_box(baseline::flatten_onto_white(black_box(&late_translucent))); | ||
| let old_t = start_o.elapsed().as_nanos(); | ||
|
|
||
| let start_n = Instant::now(); | ||
| black_box(candidate::flatten_onto_white(black_box(&late_translucent))); | ||
| let new_t = start_n.elapsed().as_nanos(); | ||
| if sample >= 5 { | ||
| println!( | ||
| "flatten_white_late_translucent,{name},{num_pixels},{},{old_t},{new_t}", | ||
| sample - 5 | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| { | ||
| let mut buf_b = opaque.clone(); | ||
| let mut buf_c = opaque.clone(); | ||
| baseline::premultiply(&mut buf_b); | ||
| candidate::premultiply(&mut buf_c); | ||
| assert_eq!(buf_b, buf_c); | ||
| for sample in 0..20 { | ||
| buf_b.copy_from_slice(&opaque); | ||
| buf_c.copy_from_slice(&opaque); | ||
|
|
||
| let start_o = Instant::now(); | ||
| baseline::premultiply(black_box(&mut buf_b)); | ||
| let old_t = start_o.elapsed().as_nanos(); | ||
|
|
||
| let start_n = Instant::now(); | ||
| candidate::premultiply(black_box(&mut buf_c)); | ||
| let new_t = start_n.elapsed().as_nanos(); | ||
| if sample >= 5 { | ||
| println!( | ||
| "premultiply_solid,{name},{num_pixels},{},{old_t},{new_t}", | ||
| sample - 5 | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| { | ||
| let mut buf_b = mixed.clone(); | ||
| let mut buf_c = mixed.clone(); | ||
| baseline::premultiply(&mut buf_b); | ||
| candidate::premultiply(&mut buf_c); | ||
| assert_eq!(buf_b, buf_c); | ||
| for sample in 0..20 { | ||
| buf_b.copy_from_slice(&mixed); | ||
| buf_c.copy_from_slice(&mixed); | ||
|
|
||
| let start_o = Instant::now(); | ||
| baseline::premultiply(black_box(&mut buf_b)); | ||
| let old_t = start_o.elapsed().as_nanos(); | ||
|
|
||
| let start_n = Instant::now(); | ||
| candidate::premultiply(black_box(&mut buf_c)); | ||
| let new_t = start_n.elapsed().as_nanos(); | ||
| if sample >= 5 { | ||
| println!( | ||
| "premultiply_mixed,{name},{num_pixels},{},{old_t},{new_t}", | ||
| sample - 5 | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| { | ||
| let mut buf_b = late_translucent.clone(); | ||
| let mut buf_c = late_translucent.clone(); | ||
| baseline::premultiply(&mut buf_b); | ||
| candidate::premultiply(&mut buf_c); | ||
| assert_eq!(buf_b, buf_c); | ||
| for sample in 0..20 { | ||
| buf_b.copy_from_slice(&late_translucent); | ||
| buf_c.copy_from_slice(&late_translucent); | ||
|
|
||
| let start_o = Instant::now(); | ||
| baseline::premultiply(black_box(&mut buf_b)); | ||
| let old_t = start_o.elapsed().as_nanos(); | ||
|
|
||
| let start_n = Instant::now(); | ||
| candidate::premultiply(black_box(&mut buf_c)); | ||
| let new_t = start_n.elapsed().as_nanos(); | ||
| if sample >= 5 { | ||
| println!( | ||
| "premultiply_late_translucent,{name},{num_pixels},{},{old_t},{new_t}", | ||
| sample - 5 | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| { | ||
| let mut buf_b = opaque.clone(); | ||
| let mut buf_c = opaque.clone(); | ||
| baseline::demultiply(&mut buf_b); | ||
| candidate::demultiply(&mut buf_c); | ||
| assert_eq!(buf_b, buf_c); | ||
| for sample in 0..20 { | ||
| buf_b.copy_from_slice(&opaque); | ||
| buf_c.copy_from_slice(&opaque); | ||
|
|
||
| let start_o = Instant::now(); | ||
| baseline::demultiply(black_box(&mut buf_b)); | ||
| let old_t = start_o.elapsed().as_nanos(); | ||
|
|
||
| let start_n = Instant::now(); | ||
| candidate::demultiply(black_box(&mut buf_c)); | ||
| let new_t = start_n.elapsed().as_nanos(); | ||
| if sample >= 5 { | ||
| println!( | ||
| "demultiply_solid,{name},{num_pixels},{},{old_t},{new_t}", | ||
| sample - 5 | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| { | ||
| let mut buf_b = mixed.clone(); | ||
| let mut buf_c = mixed.clone(); | ||
| baseline::demultiply(&mut buf_b); | ||
| candidate::demultiply(&mut buf_c); | ||
| assert_eq!(buf_b, buf_c); | ||
| for sample in 0..20 { | ||
| buf_b.copy_from_slice(&mixed); | ||
| buf_c.copy_from_slice(&mixed); | ||
|
|
||
| let start_o = Instant::now(); | ||
| baseline::demultiply(black_box(&mut buf_b)); | ||
| let old_t = start_o.elapsed().as_nanos(); | ||
|
|
||
| let start_n = Instant::now(); | ||
| candidate::demultiply(black_box(&mut buf_c)); | ||
| let new_t = start_n.elapsed().as_nanos(); | ||
| if sample >= 5 { | ||
| println!( | ||
| "demultiply_mixed,{name},{num_pixels},{},{old_t},{new_t}", | ||
| sample - 5 | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| { | ||
| let mut buf_b = late_translucent.clone(); | ||
| let mut buf_c = late_translucent.clone(); | ||
| baseline::demultiply(&mut buf_b); | ||
| candidate::demultiply(&mut buf_c); | ||
| assert_eq!(buf_b, buf_c); | ||
| for sample in 0..20 { | ||
| buf_b.copy_from_slice(&late_translucent); | ||
| buf_c.copy_from_slice(&late_translucent); | ||
|
|
||
| let start_o = Instant::now(); | ||
| baseline::demultiply(black_box(&mut buf_b)); | ||
| let old_t = start_o.elapsed().as_nanos(); | ||
|
|
||
| let start_n = Instant::now(); | ||
| candidate::demultiply(black_box(&mut buf_c)); | ||
| let new_t = start_n.elapsed().as_nanos(); | ||
| if sample >= 5 { | ||
| println!( | ||
| "demultiply_late_translucent,{name},{num_pixels},{},{old_t},{new_t}", | ||
| sample - 5 | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
baseline::flatten_onto_whitedrops the original shortcuts for alpha255and0, so it does color math even for fully opaque pixels. All three workloads are mostly or fully opaque, making the timing comparison misleading. The output equality checks cannot catch this difference in work. Copy the original implementation into the baseline so the benchmark measures this PR's actual improvement.Prompt To Fix With AI