Skip to content

offscreen: Render GPUI views into images on iOS and Android - #33

Merged
madcodelife merged 4 commits into
mainfrom
offscreen-render
Oct 13, 2026
Merged

madcodelife merged 4 commits into
mainfrom
offscreen-render

Conversation

@madcodelife

@madcodelife madcodelife commented Oct 12, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds an offscreen feature for rendering GPUI views into images on iOS and Android — for share cards, long screenshots and similar exports.

  • gpui_mobile::offscreen::OffscreenWindow opens an ordinary GPUI window whose platform side has no UIKit/NDK surface. Layout, text shaping and painting run exactly as on screen; render(size) forces one frame and reads its pixels back through wgpu's headless renderer (image::RgbaImage), and layout(size) paints without reading back so views can measure their content first. max_size() reports the GPU texture limit in logical pixels.
  • IosPlatform::open_window / AndroidPlatform::open_window hand out the offscreen window when OffscreenWindow::open asked for one (a thread-local pending slot, like Android's pending-window lookup).
  • The headless renderer (one device + pipelines) is created on first use and reused on the main thread.

Why a vendored gpui-pre-wgpu

Upstream gpui-pre-wgpu 0.3.8 only builds WgpuHeadlessRenderer with test-support / bench-support (the latter turns on GPUI's per-frame profiler), and its device comes from a Vulkan + GL instance, which has no adapter on iOS. vendor/gpui-pre-wgpu is the unmodified 0.3.8 source (first commit) plus a small patch (second commit, ~100 lines):

  • the headless renderer is also built under a new offscreen feature;
  • WgpuHeadlessRenderer::with_instance(wgpu::Instance) so iOS can pass a Metal instance;
  • inherent render_to_image / sprite_atlas / max_texture_size, which don't need GPUI's test-only PlatformHeadlessRenderer trait.

Applications enabling offscreen must patch gpui-pre-wgpu to this copy (see the README section). The longer-term fix is to offer the same in upstream gpui_wgpu.

Testing

  • cargo check for aarch64-apple-ios, aarch64-apple-ios-sim and aarch64-linux-android with offscreen; host cargo check --all-targets; clippy clean for the new module; cargo fmt --all -- --check.
  • iOS simulator (iPhone 18 Pro, Debug build): a 390pt-wide view with text, a chart and a table at @3x (1170×2773 px) — first window open 1.09s (device + pipeline creation, reused afterwards), layout 12ms, render + readback 64ms (29ms warm). Content taller than the 8192px texture limit was rendered at a reduced scale by the caller.
  • Android: compile-checked only; not run on a device yet.

madcodelife and others added 4 commits October 12, 2026 17:41
…en` feature

Ungate `WgpuHeadlessRenderer` from `test-support` / `bench-support`, add
`WgpuHeadlessRenderer::with_instance` so iOS can pass a Metal instance (the
default instance only enables Vulkan and GL), and inherent
`render_to_image` / `sprite_atlas` / `max_texture_size` that do not need
GPUI's test-only `PlatformHeadlessRenderer` trait.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@madcodelife madcodelife changed the title Add offscreen windows that render GPUI views into images offscreen: Render GPUI views into images on iOS and Android Oct 13, 2026
@madcodelife
madcodelife merged commit 875c3ee into main Oct 13, 2026
5 checks passed
@madcodelife
madcodelife deleted the offscreen-render branch October 13, 2026 02:43
madcodelife added a commit that referenced this pull request Oct 13, 2026
…ws (#37)

## Summary

Follow-up to #33.

- **Release GPU memory after captures.** The headless renderer is kept
for the rest of the process, so after a capture it held the render
target and the path intermediate / MSAA textures sized to the last frame
(for a 1170×16000 image that is roughly 75 MB + 75 MB + 300 MB at 4×
path MSAA), plus every glyph and image tile uploaded to the sprite atlas
the offscreen windows share. GPUI only removes image tiles through
`App::drop_image` on open windows, which never reaches that atlas once
the window is gone, so image tiles accumulated for the life of the
process.
- The vendored `gpui-pre-wgpu` gains two `offscreen`-gated methods on
`WgpuHeadlessRenderer`: `release_render_target` (render target + path
intermediates) and `clear_sprite_atlas` (atlas tiles + the cached bind
groups that keep their textures alive). The readback buffer was already
a per-call local, so there is no staging buffer to release.
- `offscreen` counts live offscreen platform windows and calls both when
GPUI drops the last one (after its view tree), so no open window can
hold a tile that gets cleared. The device and pipelines stay for the
next capture.
- **Close dropped windows.** Dropping an `OffscreenWindow` without
`close` (for example when the task holding it is cancelled) used to
leave the window and its view tree alive forever. It now keeps an
`AsyncApp` and, on drop, spawns the close on the foreground executor,
since `Drop` may run while the `App` is borrowed. `close` disarms it, so
the window is never closed twice.
- README and the `[patch.crates-io]` comment describe the new vendor
methods and the release behaviour.

While an offscreen window stays open, image tiles still accumulate the
same way they do for on-screen windows; the instance buffer, which grows
to the largest frame's instance data, is kept with the pipelines.

## Testing

- `cargo check --features offscreen` for `aarch64-apple-ios`,
`aarch64-apple-ios-sim` and `aarch64-linux-android`
- `cargo check --all-targets`, `cargo test --lib`, `cargo fmt --all --
--check`
- `cargo clippy --features offscreen` for iOS and Android: no new
warnings
- New vendor test `headless_renderer_releases_frame_textures` (Linux,
like the other headless tests); also run locally on macOS against a
Metal instance, where it passes

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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