Skip to content

fast: Present plainly while a composing window holds no extra surface - #90

Open
dtateks wants to merge 2 commits into
longbridge:mainfrom
dtateks:fast-composition-plain-present
Open

dtateks wants to merge 2 commits into
longbridge:mainfrom
dtateks:fast-composition-plain-present

Conversation

@dtateks

@dtateks dtateks commented Oct 26, 2026

Copy link
Copy Markdown

fast: Present plainly while a composing window holds no extra surface

Problem

Once a window enables composition it stays composed for its life, even after its last native, external or extra GPUI surface is removed. With only the default base and overlay surfaces left, every frame still copies and sorts the scene once per layer (composed_scene_layers, ComposedScene::layer_scene, replay_balanced, finish) and presents an empty overlay drawable, and the platform keeps the overlay's view, CAMetalLayer and path textures.

Measured on macOS, a hidden 1600x1000 window, about 5000 retained primitives, 600 frames after a native surface was added and then removed, release build, base and branch alternated, CPU time:

base branch never composed
process CPU per frame 1.28 / 1.35 / 1.30 ms 0.80 / 0.85 / 0.77 ms 0.77 ms
main-thread CPU per frame 0.91 / 0.93 / 0.90 ms 0.56 / 0.57 / 0.52 ms
IOSurface 99 MB 49 MB
total footprint 362-364 MB 296-298 MB

With a surface still held, base and branch are equal. After removal the branch matches a window that never composed.

Change

  • fast/composition.rs: WindowCompositionState::splits() is true when the tree holds a surface beyond the two defaults. present takes the plain PlatformWindow::draw path unless composition is enabled and splits() holds. platform_order (split out of synchronize_platform_order) leaves the overlay out of what the platform is handed while splits() is false, so the platform releases what draws it; adding a surface hands it back.
  • The scene's split markers are recorded in both modes and never read enabled or splits(), so a retained view keeps painting onto its surface across a switch.
  • gpui_linux X11: set_composition_order hides the overlay window when no native or external surface remains (second commit; see below).
  • docs/upstream-sync.md lists the behaviour with the other composition differences. No public API changes; no hook outside fast/.

Per platform (by reading; macOS also measured above)

  • macOS: set_composition_order removes any GPUI view not in the list, so the overlay view, renderer, layer and textures go.
  • Windows: set_order keeps only the overlays in the list, dropping their swap chain, visual and resources.
  • Wayland: set_composition_order destroys the subsurface and renderer of a GPUI surface that left the list.
  • Linux X11: the overlay window exists only while GPUI content sits above a native surface, and only draw_composed hid it. With plain presents that would leave it mapped, so the second commit hides it in set_composition_order. Its renderer is kept as a spare, as before, so X11 releases no memory here.

Costs and risks

  • Adding the first surface after a plain stretch recreates the overlay (view, layer, textures); that one-time cost was not measured.
  • In-flight drawable: command buffers retain their resources. 200 add/remove cycles, 1 to 3 frames apart, gave 0 errors and an IOSurface total of 74 MB.
  • Scene splits: three switches between plain and split frames keep every operation on its surface (test below).
  • Not done: a Metal API validation pass (not run), and any run on Windows or Linux. The X11 commit is not built or run (no Linux target on the host); it is a separate commit so it can be dropped.
  • composition: limit macOS composition overlay input to hitboxes #69 (open) edits present and the composition tests nearby; the two will conflict textually and need a rebase of whichever lands second.

Tests (cargo test -p gpui --lib --features test-support composition, 18 pass)

  • retained_views_keep_their_split_as_surfaces_come_and_go: three add/remove cycles; the platform order is the base alone while unsplit and base, surface, overlay while split; every operation stays on its surface and the retained view is built once. Leaving the overlay in the platform order, or counting one extra surface in splits(), fails it. The plain/composed choice in present itself is not observable on the test platform and is covered by the measurement.
  • script/check-upstream: 0 violations.

Reproduce

No headless run shows the saving: the test platform returns unsupported for enable_window_composition, so a gpui_perf scenario cannot compose, and the cost is mostly the platform's drawable and textures. The table comes from a temporary harness (a hidden window, a native surface added and removed, 600 frames, process and main-thread CPU time plus IOSurface and footprint per frame) that is not part of this PR. The unit test above is the regression guard.

Scope

Composition is a gpui-fast feature (zed#62379 is not merged upstream), so this belongs here under CONTRIBUTING's "needs that are clearly reasonable and pressing": a window that has dropped its last native surface pays for composition on every frame. #54 (gpui_perf --composition) measured this cost and was closed by its author without a recorded reason; this PR removes it.

Commits

  • fast: Present plainly while a composing window holds no extra surface
  • gpui_linux: Hide the X11 overlay window when the last native surface goes

A window that enabled composition presents through PlatformWindow::draw while its tree holds only the base and overlay surfaces, and hands the platform only the base surface, so macOS, Windows and Wayland release the overlay's view or visual, drawable layer and textures. Adding a native, external or extra GPUI surface brings them back and splits the present again; the scene's split markers are recorded in both modes, so a retained view keeps its surface across the switch.

Composition stayed on for the window's whole life after its first surface: every frame copied and sorted the scene once per layer and presented an empty overlay drawable, and the overlay held its textures, about 1.3 ms of process CPU per frame and 50 MB of IOSurface on a 1600x1000 window of 5000 retained primitives.
…goes

set_composition_order destroys the overlay window when no native or external surface remains in the order. Only draw_composed hid it, and a window without a native surface now presents with draw, so an overlay window mapped above the last native surface would have stayed on screen with its last content.

Not built or run: this host has no Linux target, so the change is checked by reading only.

@huacnlee huacnlee left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Static inspection only; no native rendering reproduction or benchmarks were run.

Reviewed by ChatGPT on behalf of @huacnlee.

state.enabled && state.splits()
};
if !splits {
window.platform_window.draw(&window.rendered_frame.scene);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[P2] Preserve base/overlay ordering in the plain presentation path

A root canvas can paint a red quad through with_composition_surface(overlay, ...), then an overlapping opaque blue quad on the restored base surface: composed rendering keeps red above blue, but this raw-scene path paints blue last. Consequently, removing the last extra surface changes the visible stacking of the remaining content. Preserve composition ordering when using one drawable and test overlapping interleaved base/overlay paints; reconstructing split markers alone does not exercise this branch.

Reviewed by ChatGPT on behalf of @huacnlee.

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