Skip to content

Commit f33be45

Browse files
Merge pull request #2836 from CapSoftware/perf/gpui-screenshot-editor-copies
perf(desktop-gpui): convert screenshot editor frames in one pass and skip stale ones
2 parents e76c7ad + 9fd2d73 commit f33be45

2 files changed

Lines changed: 146 additions & 46 deletions

File tree

‎apps/desktop-gpui/src/screenshot_annotations.rs‎

Lines changed: 75 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2242,7 +2242,7 @@ impl ScreenshotEditorWindow {
22422242
// -- Mask overlays ---------------------------------------------------------
22432243

22442244
fn mask_requests(&self) -> Vec<MaskRequest> {
2245-
if self.frame_rgba.is_none() {
2245+
if self.frame.is_none() {
22462246
return Vec::new();
22472247
}
22482248
let size = (
@@ -2312,14 +2312,16 @@ impl ScreenshotEditorWindow {
23122312
}
23132313

23142314
let previous = self.annotation_state.masks.clone();
2315-
let Some(rgba) = self.frame_rgba.clone() else {
2315+
let Some(frame) = self.frame.clone() else {
23162316
return;
23172317
};
23182318
self.annotation_state.mask_pending = Some(requests.clone());
23192319
self.annotation_state.mask_task = Some(cx.spawn_in(window, async move |this, cx| {
23202320
let overlays = cx
23212321
.background_executor()
2322-
.spawn(async move { build_mask_overlays(&rgba, &previous, &requests) })
2322+
.spawn(async move {
2323+
build_mask_overlays(frame.as_bytes(0).unwrap_or_default(), &previous, &requests)
2324+
})
23232325
.await;
23242326
this.update_in(cx, |this, window, cx| {
23252327
let overlays = finish_mask_build(&this.annotation_state.masks, overlays);
@@ -3952,22 +3954,26 @@ fn checker_quarters(size: f32) -> Vec<AnyElement> {
39523954
/// `renderMaskOverlays` + `blurRegion` (`Preview.tsx:537-687`): resample the
39533955
/// frame's own pixels inside each mask and hand back one image per region.
39543956
/// Runs on the background executor -- the regions are small, but a 50-block
3955-
/// pixelate over a 4K screenshot is still not main-thread work.
3957+
/// pixelate over a 4K screenshot is still not main-thread work. The filter
3958+
/// treats every channel alike, so it runs on the atlas's BGRA as-is and the
3959+
/// overlays come out already in atlas order.
39563960
fn build_mask_overlays(
3957-
rgba: &[u8],
3961+
bgra: &[u8],
39583962
previous: &[MaskEntry<MaskOverlay>],
39593963
requests: &[MaskRequest],
39603964
) -> Vec<MaskEntry<MaskOverlay>> {
39613965
update_masks(previous, requests, |request| {
3962-
let processed = request.filter(rgba)?;
3966+
let processed = request.filter(bgra)?;
39633967
Some(MaskOverlay {
39643968
rect: Rect {
39653969
x: request.region.0 as f64,
39663970
y: request.region.1 as f64,
39673971
width: processed.width() as f64,
39683972
height: processed.height() as f64,
39693973
},
3970-
image: crate::library::rgba_to_render_image(processed),
3974+
image: Arc::new(RenderImage::new(smallvec::smallvec![image::Frame::new(
3975+
processed
3976+
)])),
39713977
})
39723978
})
39733979
}
@@ -4523,6 +4529,68 @@ mod tests {
45234529
assert_eq!(mask_region(&away, rect, (200, 200)), None);
45244530
}
45254531

4532+
#[test]
4533+
fn mask_overlays_from_bgra_match_filtering_rgba_then_swapping() {
4534+
let (width, height) = (37u32, 23u32);
4535+
let stride = (width as usize * 4).next_multiple_of(256);
4536+
let padded: Vec<u8> = (0..stride * height as usize)
4537+
.map(|index| (index * 31 + index / 7) as u8)
4538+
.collect();
4539+
let rgba = cap_gpui_kernels::frame::unpad_rows(
4540+
&padded,
4541+
width as usize * 4,
4542+
stride,
4543+
height as usize,
4544+
)
4545+
.unwrap();
4546+
let bgra = cap_gpui_kernels::frame::unpad_rgba_to_bgra(
4547+
&padded,
4548+
width as usize,
4549+
stride,
4550+
height as usize,
4551+
)
4552+
.unwrap();
4553+
let frame = MaskFrame {
4554+
revision: 1,
4555+
size: (width, height),
4556+
content: [0., 0., f64::from(width), f64::from(height)].map(f64::to_bits),
4557+
padding: 0_f64.to_bits(),
4558+
crop: None,
4559+
};
4560+
let mut masks = Vec::new();
4561+
for (index, (mask_type, level, region)) in [
4562+
(MaskType::Blur, 4., (0., 0., 37., 23.)),
4563+
(MaskType::Blur, 9., (3., 1., 17., 11.)),
4564+
(MaskType::Blur, 16., (20., 5., 17., 18.)),
4565+
(MaskType::Blur, 33., (1., 2., 35., 21.)),
4566+
(MaskType::Pixelate, 2., (0., 0., 37., 23.)),
4567+
(MaskType::Pixelate, 7., (5., 3., 13., 9.)),
4568+
(MaskType::Pixelate, 20., (11., 7., 26., 16.)),
4569+
]
4570+
.into_iter()
4571+
.enumerate()
4572+
{
4573+
let mut mask = shape(AnnotationType::Mask, region.0, region.1, region.2, region.3);
4574+
mask.id = index.to_string();
4575+
mask.mask_type = Some(mask_type);
4576+
mask.mask_level = Some(level);
4577+
masks.push(mask);
4578+
}
4579+
let requests: Vec<_> = masks
4580+
.iter()
4581+
.filter_map(|mask| mask_request(mask, frame))
4582+
.collect();
4583+
assert_eq!(requests.len(), masks.len());
4584+
4585+
let overlays = build_mask_overlays(&bgra, &[], &requests);
4586+
for (request, entry) in requests.iter().zip(&overlays) {
4587+
let expected = crate::library::rgba_to_render_image(request.filter(&rgba).unwrap());
4588+
let overlay = entry.value.as_ref().unwrap();
4589+
assert_eq!(overlay.image.size(0), expected.size(0));
4590+
assert_eq!(overlay.image.as_bytes(0), expected.as_bytes(0));
4591+
}
4592+
}
4593+
45264594
/// The resample itself: one overlay per mask, at the region's own size,
45274595
/// with the row slicing staying inside the frame buffer.
45284596
#[test]

‎apps/desktop-gpui/src/screenshot_editor.rs‎

Lines changed: 71 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -912,12 +912,10 @@ pub struct ScreenshotEditorWindow {
912912
/// renders.
913913
exporting: bool,
914914
export_status: ExportStatus,
915-
/// The latest GPU frame, already BGRA in gpui's atlas format.
916-
frame: Option<Arc<RenderImage>>,
917-
/// The same frame as tight RGBA. `Preview.tsx` reads the preview canvas
918-
/// back to blur/pixelate mask regions (`:537-687`); the mask overlay
919-
/// resamples this instead of the atlas image.
920-
pub(crate) frame_rgba: Option<Arc<Vec<u8>>>,
915+
/// The latest GPU frame, already BGRA in gpui's atlas format. `Preview.tsx`
916+
/// reads the preview canvas back to blur/pixelate mask regions
917+
/// (`:537-687`); the mask overlay resamples these same pixels.
918+
pub(crate) frame: Option<Arc<RenderImage>>,
921919
pub(crate) mask_frame_revision: u64,
922920
pub(crate) frame_size: (f32, f32),
923921
/// `originalImageSize` -- the source PNG's dimensions, which the crop
@@ -1003,7 +1001,6 @@ impl ScreenshotEditorWindow {
10031001
exporting: false,
10041002
export_status: ExportStatus::Idle,
10051003
frame: None,
1006-
frame_rgba: None,
10071004
mask_frame_revision: 0,
10081005
frame_size: (1., 1.),
10091006
image_size: None,
@@ -1126,15 +1123,13 @@ impl ScreenshotEditorWindow {
11261123
pub fn frame_arrived(
11271124
&mut self,
11281125
image: Arc<RenderImage>,
1129-
rgba: Arc<Vec<u8>>,
11301126
size: (u32, u32),
11311127
window: &mut Window,
11321128
cx: &mut Context<Self>,
11331129
) {
11341130
let next = (size.0.max(1) as f32, size.1.max(1) as f32);
11351131
self.rescale_annotations_for(next);
11361132
self.frame_size = next;
1137-
self.frame_rgba = Some(rgba);
11381133
self.mask_frame_revision = self
11391134
.mask_frame_revision
11401135
.checked_add(1)
@@ -4747,26 +4742,10 @@ fn render_input_key(project: &mut ProjectConfiguration) -> Option<Vec<u8>> {
47474742
Some(key)
47484743
}
47494744

4750-
/// One rendered frame, in both the forms the window needs: tight RGBA (what a
4751-
/// mask overlay resamples) and the BGRA copy gpui's atlas takes. The rows are
4752-
/// copied once and the swap runs over a clone, so the padded GPU buffer is
4753-
/// walked a single time.
4754-
fn frame_buffers(frame: &RenderedFrame) -> Option<(Arc<Vec<u8>>, Arc<RenderImage>)> {
4755-
let rgba = cap_gpui_kernels::frame::unpad_rows(
4756-
&frame.data,
4757-
frame.width as usize * 4,
4758-
frame.padded_bytes_per_row as usize,
4759-
frame.height as usize,
4760-
)?;
4761-
let mut bgra = rgba.clone();
4762-
cap_gpui_kernels::frame::swap_red_blue(&mut bgra);
4763-
let buffer = image::RgbaImage::from_raw(frame.width, frame.height, bgra)?;
4764-
Some((
4765-
Arc::new(rgba),
4766-
Arc::new(RenderImage::new(smallvec::smallvec![image::Frame::new(
4767-
buffer
4768-
)])),
4769-
))
4745+
/// Only the newest queued frame is ever shown, so older ones are dropped
4746+
/// before the copy rather than converted and replaced.
4747+
fn latest_frame<T>(frame: T, frames: &flume::Receiver<T>) -> T {
4748+
frames.drain().last().unwrap_or(frame)
47704749
}
47714750

47724751
/// Read the bundle, start the still renderer on tokio, pump frames back --
@@ -4837,13 +4816,14 @@ pub fn load_screenshot_project(
48374816
return;
48384817
}
48394818

4840-
while let Ok((frame, revision)) = frame_rx.recv_async().await {
4819+
while let Ok(frame) = frame_rx.recv_async().await {
4820+
let (frame, revision) = latest_frame(frame, &frame_rx);
48414821
let size = (frame.width, frame.height);
4842-
let buffers = cx
4822+
let image = cx
48434823
.background_executor()
4844-
.spawn(async move { frame_buffers(&frame) })
4824+
.spawn(async move { crate::editor_window::frame_image(&frame) })
48454825
.await;
4846-
let Some((rgba, image)) = buffers else {
4826+
let Some(image) = image else {
48474827
tracing::warn!("a rendered screenshot frame could not be converted for display");
48484828
continue;
48494829
};
@@ -4855,7 +4835,7 @@ pub fn load_screenshot_project(
48554835
);
48564836
if handle
48574837
.update(cx, |view, window, cx| {
4858-
view.frame_arrived(image, rgba, size, window, cx)
4838+
view.frame_arrived(image, size, window, cx)
48594839
})
48604840
.is_err()
48614841
{
@@ -4887,6 +4867,56 @@ mod tests {
48874867
assert_eq!(CHECKER_TILE % (CHECKER_CELL * 2), 0);
48884868
}
48894869

4870+
#[test]
4871+
fn the_displayed_frame_matches_unpadding_then_swapping_a_copy() {
4872+
for (width, height) in [(1u32, 1u32), (3, 5), (7, 2), (33, 9), (1001, 3)] {
4873+
let row_bytes = width * 4;
4874+
for stride in [row_bytes, row_bytes + 4, row_bytes.next_multiple_of(256)] {
4875+
let frame = RenderedFrame {
4876+
data: Arc::new(
4877+
(0..stride as usize * height as usize)
4878+
.map(|index| (index * 7 + index / 251) as u8)
4879+
.collect(),
4880+
),
4881+
width,
4882+
height,
4883+
padded_bytes_per_row: stride,
4884+
frame_number: 0,
4885+
target_time_ns: 0,
4886+
};
4887+
let rgba = cap_gpui_kernels::frame::unpad_rows(
4888+
&frame.data,
4889+
row_bytes as usize,
4890+
stride as usize,
4891+
height as usize,
4892+
)
4893+
.unwrap();
4894+
let mut bgra = rgba.clone();
4895+
cap_gpui_kernels::frame::swap_red_blue(&mut bgra);
4896+
4897+
let image = crate::editor_window::frame_image(&frame).unwrap();
4898+
assert_eq!(
4899+
image.size(0),
4900+
gpui::size(
4901+
gpui::DevicePixels::from(width),
4902+
gpui::DevicePixels::from(height)
4903+
)
4904+
);
4905+
assert_eq!(image.as_bytes(0).unwrap(), bgra.as_slice());
4906+
}
4907+
}
4908+
}
4909+
4910+
#[test]
4911+
fn only_the_newest_queued_frame_is_converted() {
4912+
let (frame_tx, frame_rx) = flume::bounded(2);
4913+
assert_eq!(latest_frame(1, &frame_rx), 1);
4914+
frame_tx.send(2).unwrap();
4915+
frame_tx.send(3).unwrap();
4916+
assert_eq!(latest_frame(1, &frame_rx), 3);
4917+
assert!(frame_rx.is_empty());
4918+
}
4919+
48904920
/// `PaddingPopover`'s guard (`context.tsx:30-42`).
48914921
#[test]
48924922
fn an_invisible_background_is_one_with_nothing_to_show() {
@@ -5182,7 +5212,7 @@ mod tests {
51825212
));
51835213
setup_rx.recv_async().await.unwrap().unwrap();
51845214
let (first, _) = frame_rx.recv_async().await.unwrap();
5185-
assert!(frame_buffers(&first).is_some());
5215+
assert!(crate::editor_window::frame_image(&first).is_some());
51865216

51875217
let renders = Arc::new(std::sync::atomic::AtomicUsize::new(0));
51885218
let render_ms = Arc::new(std::sync::Mutex::new(Vec::new()));
@@ -5194,10 +5224,12 @@ mod tests {
51945224
(renders.clone(), render_ms.clone(), sent_at.clone());
51955225
tokio::spawn(async move {
51965226
while let Ok((frame, revision)) = frame_rx.recv_async().await {
5197-
let buffers = tokio::task::spawn_blocking(move || frame_buffers(&frame))
5198-
.await
5199-
.unwrap();
5200-
assert!(buffers.is_some());
5227+
let image = tokio::task::spawn_blocking(move || {
5228+
crate::editor_window::frame_image(&frame)
5229+
})
5230+
.await
5231+
.unwrap();
5232+
assert!(image.is_some());
52015233
if let Some(sent) = sent_at.lock().unwrap().remove(&revision) {
52025234
render_ms
52035235
.lock()

0 commit comments

Comments
 (0)