Skip to content

perf(gpui): streaming images that reuse one texture per window - #2852

Open
richiemcilroy wants to merge 4 commits into
mainfrom
perf/gpui-streaming-image
Open

richiemcilroy wants to merge 4 commits into
mainfrom
perf/gpui-streaming-image

Conversation

@richiemcilroy

@richiemcilroy richiemcilroy commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Phase 1 of the gpui streaming-image design. This adds a new gpui patch, zed-streaming-image.patch, plus tests and a CI job. No Cap call site is switched yet, so the app behaves exactly as before.

What the patch adds

  • gpui::StreamingImage: a Send + Sync handle carrying a frame generation. Methods: set_frame (returns the previous buffer for reuse once nothing else holds it), copy_frame (double-buffers internally, so same-size frames don't allocate), current, clear. Strides are supported, and the formats are Bgra8 and Rgba8. Bad frames (empty, a stride that is short or not 4-aligned, a short buffer, or overflow) return Err and don't panic.
  • AtlasTextureKind::Stream plus two required trait methods, PlatformAtlas::get_or_update_stream and remove_stream. They are implemented for Metal, wgpu, DirectX, TestAtlas and HeadlessAtlas.
    • Stream textures live in a separate list in each atlas. The packer, remove and the Windows unreferenced-texture cache never see them.
    • Textures are exactly the frame size and are reallocated only when the size or format changes.
    • Texture indices are never reused, so a stale scene can't sample another stream's texture.
  • Window::paint_streaming_image(_fitted):
    • Shares a private insert_polychrome_tile helper with paint_image_fitted. The helper was extracted mechanically, so crop UVs, radii, opacity and edge fade are identical.
    • A frame is uploaded at most once per generation per window.
    • At the start of each draw, the window releases textures for streams that are dropped or cleared.
  • Renderers check the kind and skip a stream batch whose texture is gone. They never unwrap.
  • Metal: each frame is written into a slot that no command buffer is reading. Draws lease the slot through CommandBufferResources, so the lease is released when the GPU completes. There are at most 2 slots (1 for frames over 32 MB). The spare slot is freed after 120 uploads without contention, and uploads never wait on the GPU. COMET_GPU_STATS also reports stream textures, MB and how many were created.
  • wgpu: painting records the newest frame per stream (an Arc clone, no copy). before_frame writes it with write_texture straight from the frame's buffer, at the same point the atlas flushes its uploads. This is a deliberate change from the design's "write during paint": on llvmpipe GL, a write_texture issued during paint added 12-24% to draw time in the in-app A/B (perf(gpui): stream the editor preview into one texture (default on Linux) #2854). Bgra input is swizzled into a scratch buffer kept per stream, and only when the atlas format is Rgba8. Rgba input always uploads without conversion.
  • D3D11: one DEFAULT texture plus an SRV per stream, written with UpdateSubresource (the stride is the row pitch). The stream is dropped on device loss, and the next paint uploads it again.
  • Every AtlasTextureKind match was audited. Arms that can only ever see packed kinds now name Stream as unreachable!(), and no _ => arm covers the new kind.
  • Fixes a pre-existing compile error in gpui_macos's own test module: the Quad helper was missing fade.

Tests (all skip cleanly when there is no device)

  • Handle: generation, buffer reuse, validation, clear, Send/Sync.
  • Window sweep: with TestAtlas swapped in, a frame painted twice uploads once, a new generation uploads once more, and clearing or dropping the handle calls remove_stream exactly once.
  • Metal:
    • same-size reuse (1 texture created over 9 frames), size and format realloc, image/stream isolation, removal, oversize rejection
    • ordering: a blit is left uncommitted while g2 uploads, and it must still read g1. This runs for Shared and Managed storage; a mutation test confirmed it fails if the in-flight check is removed.
    • spare-slot release; single slot above 32 MB
    • renderer: a headless render of a stream sprite is pixel-identical to the atlas image (Bgra and padded-stride Rgba); a removed stream batch is skipped
  • wgpu: reuse, realloc, isolation, writes waiting for before_frame (only the newest pending frame is written), the bytes held match the atlas upload path for both atlas formats, Rgba/Bgra/stride parity, write-copy-write-copy ordering, recovery after clear(), oversize rejection.
  • D3D (WARP): reuse, realloc, image removal with the unreferenced cache leaving streams alone, device loss then re-upload, CopyResource-into-staging ordering, Rgba/Bgra/stride parity, oversize rejection.

CI

New job GPUI patch tests (macOS, Windows WARP, Linux lavapipe + llvmpipe). It runs only when apps/desktop-gpui/patches/** or the prepare script changes. It runs the tests above from the prepared ../zed-cap checkout without --locked: the existing patch stack adds wayland-client to gpui without updating zed's lockfile, so --locked fails.

Merge order with #2776

Both PRs append a patch. The patches touch disjoint hunks and apply cleanly in either order, with identical results (verified). Whichever merges second gets a trivial textual conflict in patch_paths and the fixture test.

RetriggerView in GreptileConfidence Score: 4/5

The PR appears safe to merge, with non-blocking feedback about GPU tests silently skipping in CI.

Findings

  1. P2 GPU tests can silently skip ▶
Fix with agent prompt
### Issue 1
apps/desktop-gpui/patches/zed-streaming-image.patch:2101-2103
If adapter or device creation fails on a CI runner, `stream_test` returns `None`, and every stream test returns successfully. The new GPU job can therefore pass without checking uploads or draw ordering. Installing Mesa does not check that either requested backend works.

Add a CI-only requirement that fails when the requested device is unavailable, while keeping the intended skips for local runs.

---

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

Summary

Adds StreamingImage and separate stream textures for Metal, wgpu, and DirectX without switching Cap callers.

  • GPUI can paint changing images without repacking every frame.
  • CI runs GPUI patch tests on the platforms that use each renderer.

Reviews (1) · Last reviewed commit: "perf(gpui): write wgpu stream frames in ..." · Reviewed by Greptile

@vercel

vercel Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
cap-status Ready Ready Preview Oct 21, 2026 8:29pm UTC

@richiemcilroy

Copy link
Copy Markdown
Member Author

Atlas A/B micro-benchmark (scratch branch ci/streaming-image-ab, run 39652303765). Each row is 600 frames of one producer frame per iteration.

  • old: a new Vec and a new ImageId, then get_or_insert_with + remove of the previous frame (+ before_frame and a submit on wgpu).
  • stream: copy_frame + get_or_update_stream (+ the same submit).

Large allocs are Rust heap allocations of 64 KiB or more.

backend size old p50/p95 µs stream p50/p95 µs p50 speedup large allocs/frame old→stream textures created (600 frames) old→stream
d3d11-warp 960x540 366 / 400 222 / 266 1.7x 1.00 → 0.00 2 → 1
d3d11-warp 1248x702 447 / 637 372 / 423 1.2x 1.00 → 0.00 2 → 1
d3d11-warp 1920x1080 892 / 1020 899 / 1079 1.0x 1.00 → 0.00 2 → 1
d3d11-warp 3840x2160 9139 / 10873 4444 / 5992 2.1x 1.00 → 0.00 2 → 1
d3d11-warp 5120x2880 16032 / 34944 8182 / 10005 2.0x 1.00 → 0.00 2 → 1
metal-shared(Apple Paravirtual device) 960x540 449 / 634 78 / 121 5.8x 1.00 → 0.00 600 → 1
metal-shared(Apple Paravirtual device) 1248x702 655 / 826 142 / 181 4.6x 1.00 → 0.00 600 → 1
metal-shared(Apple Paravirtual device) 1920x1080 1536 / 1940 320 / 465 4.8x 1.00 → 0.00 600 → 1
metal-shared(Apple Paravirtual device) 3840x2160 6718 / 8015 1494 / 1986 4.5x 1.00 → 0.00 600 → 1
metal-shared(Apple Paravirtual device) 5120x2880 12333 / 14796 2578 / 3463 4.8x 1.00 → 0.00 600 → 1
wgpu-Dx12(Microsoft Basic Render Driver,Bgra8Unorm) 960x540 9408 / 17204 429 / 592 22.0x 2.00 → 0.00 600 → 1
wgpu-Dx12(Microsoft Basic Render Driver,Bgra8Unorm) 1248x702 1660 / 2336 601 / 861 2.8x 2.00 → 0.00 600 → 1
wgpu-Dx12(Microsoft Basic Render Driver,Bgra8Unorm) 1920x1080 3739 / 5059 1363 / 2481 2.7x 2.00 → 0.00 600 → 1
wgpu-Gl(llvmpipe ,Bgra8Unorm) 960x540 1848 / 3022 576 / 670 3.2x 2.00 → 0.00 600 → 1
wgpu-Gl(llvmpipe ,Bgra8Unorm) 1248x702 2430 / 3305 991 / 1164 2.5x 2.00 → 0.00 600 → 1
wgpu-Gl(llvmpipe ,Bgra8Unorm) 1920x1080 8694 / 11056 5713 / 7522 1.5x 2.00 → 0.00 600 → 1
wgpu-Gl(llvmpipe ,Bgra8Unorm) 3840x2160 39831 / 50102 21904 / 25201 1.8x 2.00 → 0.00 600 → 1
wgpu-Gl(llvmpipe ,Bgra8Unorm) 5120x2880 93216 / 113909 41834 / 52660 2.2x 2.00 → 0.00 600 → 1
wgpu-Metal(Apple Paravirtual device,Bgra8Unorm) 960x540 634 / 901 274 / 437 2.3x 2.00 → 0.00 600 → 1
wgpu-Metal(Apple Paravirtual device,Bgra8Unorm) 1248x702 1375 / 2974 741 / 1129 1.9x 2.00 → 0.00 600 → 1
wgpu-Metal(Apple Paravirtual device,Bgra8Unorm) 1920x1080 5784 / 7093 2808 / 3393 2.1x 2.00 → 0.00 600 → 1
wgpu-Metal(Apple Paravirtual device,Bgra8Unorm) 3840x2160 22294 / 26566 9450 / 12458 2.4x 2.00 → 0.00 600 → 1
wgpu-Metal(Apple Paravirtual device,Bgra8Unorm) 5120x2880 26474 / 37148 12053 / 18155 2.2x 2.00 → 0.00 600 → 1
wgpu-Vulkan(llvmpipe ,Bgra8Unorm) 960x540 310 / 538 82 / 96 3.8x 2.00 → 0.00 600 → 1
wgpu-Vulkan(llvmpipe ,Bgra8Unorm) 1248x702 796 / 1375 210 / 442 3.8x 2.00 → 0.00 600 → 1
wgpu-Vulkan(llvmpipe ,Bgra8Unorm) 1920x1080 2734 / 4447 2012 / 3323 1.4x 2.00 → 0.00 600 → 1
wgpu-Vulkan(llvmpipe ,Bgra8Unorm) 3840x2160 34084 / 43677 15107 / 18136 2.3x 2.00 → 0.00 600 → 1
wgpu-Vulkan(llvmpipe ,Bgra8Unorm) 5120x2880 64111 / 89510 27678 / 52470 2.3x 2.00 → 0.00 600 → 1

Notes:

  • D3D11 on WARP gains least, as the design predicted: the unreferenced-texture cache already avoids per-frame textures. At 1080p it is flat (p50 +0.8%, p95 +5.8%); every other size is 1.2x to 2.1x faster.
  • The wgpu-on-DX12 run hit OOM in the old path at 4K. Cap doesn't use wgpu on Windows.
  • The macOS Intel (Managed) job is still queued on macos-26-intel.

@blacksmith-sh

This comment has been minimized.

@richiemcilroy

Copy link
Copy Markdown
Member Author

The macos-26-intel A/B finished. Metal uses Managed storage there (Paravirtual device, no Apple GPU family):

backend size old p50/p95 µs stream p50/p95 µs p50 speedup textures old→stream
metal-managed 960x540 1967 / 2461 437 / 573 4.5x 600 → 1
metal-managed 1248x702 2609 / 4653 777 / 934 3.4x 600 → 1
metal-managed 1920x1080 7015 / 8245 2370 / 2731 3.0x 600 → 1
metal-managed 3840x2160 27307 / 30646 8826 / 10344 3.1x 600 → 1
metal-managed 5120x2880 51342 / 59135 15690 / 18787 3.3x 600 → 1

wgpu-on-Metal on that VM is very noisy, but stream still wins at every size. Large allocs per frame drop from 2 to 0.

@richiemcilroy

Copy link
Copy Markdown
Member Author

@greptileai review

@capy-ai capy-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Capy found 1 potential issue (1 medium).

View 2 other findings in Capy.

Open in Capy

Comment on lines +1908 to +1910
+ Some(position) => {
+ let index = self.streams[position].index;
+ self.streams[position] = self.new_stream(id, index, size, format, swizzle);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Stream resizing changes textures beneath existing sprite coordinates

Medium Severity

If a producer changes a shared stream from 64×64 to 128×128 between two paint_streaming_image calls in one draw, the first sprite retains its 64×64 tile bounds, but this replacement makes the same texture ID resolve to the 128×128 texture. The shaders divide those stored bounds by the current texture dimensions, so the first sprite stretches the top-left quarter instead of displaying the whole frame. GPUI's Window::reuse_paint can also preserve these stale bounds when another consumer updates the stream. Metal's update_stream and DirectX's update_stream reuse the index on resize as well. Keep frame dimensions consistent across a draw and invalidate cached consumers on resize, or represent stream source coordinates independently of the previous texture dimensions.

Open in Capy

Comment on lines +2101 to +2103
+ Err(error) => {
+ eprintln!("skipping: {error}");
+ return None;

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 GPU tests can silently skip

If adapter or device creation fails on a CI runner, stream_test returns None, and every stream test returns successfully. The new GPU job can therefore pass without checking uploads or draw ordering. Installing Mesa does not check that either requested backend works.

Add a CI-only requirement that fails when the requested device is unavailable, while keeping the intended skips for local runs.

Knowledge Base Used: Build and release automation

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/desktop-gpui/patches/zed-streaming-image.patch
Line: 2101-2103

Comment:
**GPU tests can silently skip**

If adapter or device creation fails on a CI runner, `stream_test` returns `None`, and every stream test returns successfully. The new GPU job can therefore pass without checking uploads or draw ordering. Installing Mesa does not check that either requested backend works.

Add a CI-only requirement that fails when the requested device is unavailable, while keeping the intended skips for local runs.

**Knowledge Base Used:** [Build and release automation](https://app.greptile.com/cap/-/custom-context/knowledge-base/capsoftware/cap/-/docs/build-and-release-automation.md)

---

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

@blacksmith-sh

blacksmith-sh Bot commented Oct 22, 2026

Copy link
Copy Markdown

Found 2 test failures on Blacksmith runners:

Failures

Test View Logs
crash/recorder-kill/inproc/static-screen/static-screen View Logs
crash/recorder-kill/oop/static-screen/static-screen View Logs

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

This branch had an error being deployed

1 failed and 1 active deployments
Preview – cap-web — a6fc0072 Deployed Oct 21, 2026 by vercel[bot]
Preview – cap-status — a6fc0072 Deployed Oct 21, 2026 by vercel[bot]
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.

1 participant