Skip to content

fix: show the window after React's first commit - #691

Merged
Ernxst merged 4 commits into
mainfrom
bb/task-600-first-frame-and-early-appkit-pump-thr_azhjng7mi7
Sep 25, 2026
Merged

Ernxst merged 4 commits into
mainfrom
bb/task-600-first-frame-and-early-appkit-pump-thr_azhjng7mi7

Conversation

@Ernxst

@Ernxst Ernxst commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

Harness: Claude Code
Agent: none
Model: anthropic/claude-opus-5-5

Task statements
  1. Fix GPUIX presents an empty first frame and pumps AppKit late after opening a window #600: pump AppKit straight after init, commit React before the window's first present so the first frame shows content, and install the default menus after the second frame; keep it correct for every entry path and on Windows and Linux. (Working-directory and workflow instructions omitted.)

  2. Changes 1 and 3 are already on main from fix: pump AppKit sooner and install default menus after the first frames #608. Make the approved zed change so macOS displayLayer: frames request presentation, as a fork commit on top of macOS windows start their display link and tab queries late, adding about 25 ms before the first frames #599's pin. Fix the empty first frame on Windows as well if it has one, and say why Linux is unchanged. (Working-directory and workflow instructions omitted.)

Closes #600.

GPUI shows a show: true window while it opens it, before React has committed, so the first frame a GPU-IX window presented was an empty scene. #608 already landed the issue's other two changes: pumping AppKit on the first tick, and installing the default menus after the second frame. This PR covers the first frame.

On macOS and Windows, init() now opens the window hidden, and the first tick()/tickIdle() shows it. render() commits before it starts the frame loop, so the tree is in place when the window appears:

 render()
   renderer.init()          // opens the window hidden
   flushSync(root.render)   // commits the tree
   startFrameLoop()
     tickIdle()
+      reveal the window      // presents the committed tree
       pump AppKit
  • macOS: the reveal marks the view's layer for display, orders the window front, and displays the layer if AppKit didn't. GPUI then draws the committed tree and presents it with the Core Animation transaction that shows the window. gpui: show a window opened hidden with its current frame zed#4 makes GPUI's displayLayer: frame request presentation. Without it, GPUI's inactive-window throttle often dropped that frame, and the window showed the scene it had presented while hidden until the display link drew. The default menus now wait for the shown window's second frame; before, the hidden window's own frame counted as the first.
  • Windows: GPUI showed the window during Window::new, before the root view existed. The window now opens hidden, and the reveal applies GPUI's pending placement through the new gpui_windows::show_window_opened_hidden (gpui: show a window opened hidden with its current frame zed#4), which keeps focus: false from activating it. A hidden window gets no WM_PAINT, so the first frame it presents is drawn after the commit.
  • Linux: unchanged. GPUI's X11 and Wayland backends ignore show: false, so there is no hidden state to reveal from.
  • activateWindow() before the first tick, such as from a mount effect, performs the pending reveal itself, focused, so the window keeps its first frame and deferred default menus. A window opened with show: false is unchanged: activateWindow() still reveals it. A root that throws during the first render terminates before the window is ever shown.

examples/first-frame.test.ts initialises a renderer, commits a tree, pumps once and counts the frames presented in that pump. It covers a focused window, focus: false, and a requestAnimationFrame loop started from an effect:

Case main GPU-IX change, zed unchanged This PR
focused 0 presents 0 or 1 (throttled in some runs) 1
focus: false 0 0 1
rAF from an effect 0 0 or 1 (throttled in some runs) 1
focus: false + rAF 0 0 1

Default menus still install: the menu bar has the app menu (Services, Hide, Hide Others, Show All, Quit with ⌘Q) and a Window menu, and a posted Cmd+Q key event quits the app.

Startup timing, for information: examples/bench/hello-gpuix.tsx, 10 runs each alternating with main (ed9832d3b0) on the same Mac with nothing else building, medians from launch. Present times come from a temporary log in GPUI's present():

From launch to main This PR
first present (any scene) 186.0 ms, empty scene, window visible 177.5 ms, empty scene, window hidden
first present containing the tree 266.3 ms 205.0 ms
the fixture's second requestAnimationFrame 264.3–281.1 ms 280.3–284.1 ms

The tree reaches the screen about 61 ms sooner. It comes with the present that shows the window, not with the display link. The second-requestAnimationFrame marker is 3–16 ms later across two sessions of 10 runs each.

Risk: a custom host that calls createRenderer().init() now sees its window only at its first tick()/tickIdle(). The README and skills/gpuix say to commit before ticking. The Windows path is compiled and tested only by Windows CI. Reverting both commits restores the previous behaviour.

🤖 Generated with Claude Code

@Ernxst
Ernxst force-pushed the bb/task-600-first-frame-and-early-appkit-pump-thr_azhjng7mi7 branch from 8b5308f to a16a7ba Compare September 25, 2026 23:33
Ernxst and others added 4 commits September 26, 2026 00:41
Takes two fork commits on top of the #599 pin: macOS displayLayer:
frames request presentation instead of being throttled for inactive
windows, and gpui_windows can show a window opened hidden without
activating it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GPUI shows a `show: true` window while opening it, before React has
committed, so its first present was an empty scene. On macOS and
Windows, init() now opens the window hidden and the first tick() or
tickIdle() shows it. render() commits before starting the frame loop,
so the first frame the window presents contains the rendered tree.

On macOS the reveal marks the layer for display, orders the window
front and displays the layer if AppKit did not, so GPUI presents the
committed tree with the transaction that shows the window, including
for an unfocused window that opens covered. The default menus now wait
for the shown window's second frame; the hidden window's own frame no
longer counts as one of them. On Windows the reveal applies GPUI's
pending placement, which keeps `focus: false` from activating the
window. GPUI's Linux backends cannot open a window hidden, so Linux is
unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A mount effect runs inside render()'s flushSync, before the frame loop's
first tick. activateWindow() there dropped the pending reveal, and with
it the deferred default-menu install, so a menus-less app lost its menu
bar and Cmd+Q. It now performs the pending reveal itself, focused, which
presents the committed tree and schedules the default menus, then
activates as before.

Adds testHasApplicationMenus() for the regression test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Ernxst
Ernxst force-pushed the bb/task-600-first-frame-and-early-appkit-pump-thr_azhjng7mi7 branch from a16a7ba to 4d04638 Compare September 25, 2026 23:41
@Ernxst
Ernxst merged commit 5f941d4 into main Sep 25, 2026
@Ernxst
Ernxst deleted the bb/task-600-first-frame-and-early-appkit-pump-thr_azhjng7mi7 branch September 25, 2026 23:48
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.

GPUIX presents an empty first frame and pumps AppKit late after opening a window

1 participant