Repository navigation
renderer: a frame the embedder presents itself wakes no main-thread present (sync line) - #76
Conversation
…d present (red) cmux FRAME OWNER S4 (held key resize, 7 moving terminals, cmux24 job c33bd85dbb064376b348d0c2): with GHOSTTY_HOST_PRESENT_EMBEDDER, every frame of a new host size went to the main queue (IOSurfaceLayer.setSurface), where setSurfaceCallback dropped it: the embedder shows that frame itself, with its bounds (Metal.hostFrame). One main-thread wake per moving terminal and frame. HostFrameCounters.main_presents counts the frames Metal.present hands to the main thread's present. Tests (host present:): - a frame of a new host size that the embedder presents: no main present (red: 1), and hostFrame(true) presents it; - a frame for bounds the embedder already set: through main, as before; - a frame of a size the host no longer has, in embedder mode: no main present (red: 1); the same frame by default (renderer mode): through main, as before. (cherry picked from commit 051ecec)
… no longer has, wakes no main-thread present With GHOSTTY_HOST_PRESENT_EMBEDDER, a frame that cannot go to the screen off main went to the main queue (IOSurfaceLayer.setSurface), where setSurfaceCallback dropped it in two cases: - a frame of the host size, kept for the embedder's present (publishFresh), while the layer shows another size in bounds of another size: the embedder shows it with its bounds (hostFrame); - a frame of a size the host no longer has (the host size changes in every refresh of a key resize): the layer never shows it, and the renderer draws the new size next. cmux FRAME OWNER S4 measured one main-thread wake per moving terminal and frame in a held key resize (7 terminals: +0.27 ms of main per motion frame, more missed refreshes with the switch on; cmux24 job c33bd85dbb064376b348d0c2). Metal.present now returns for these frames (Metal.embedderTakesFrame); the check of the shown size and the bounds runs under the Core Animation lock (IOSurfaceLayer.embedderShowsFrame, the same test setSurfaceCallback makes). A first frame (nothing shown), a frame at the shown size (it waits for the embedder's commit) and a frame for bounds the embedder already set still go through main. If the embedder sets the bounds after a frame was kept, the frame waits for its present (READY): ghostty.h says to call again until SHOWN, NONE or HIDDEN, and says so for this case now. Renderer mode is unchanged. (cherry picked from commit bc2ba9d)
… main (Codex review FIX at bc2ba9d, finding 1) The early return for such a frame decided with the host size the completion thread read, without ordering against the main thread's host changes, and the frame is kept nowhere. After A -> B -> A (and the embedder's bounds at A) the main thread's present would have shown it; with the renderer's frames held (synchronized output) nothing else would come for up to the hold's end. In cmux's embedder mode one size is in flight at a time, so these frames are rare and the return saved almost nothing. It is gone: only a frame kept for the embedder's present, checked under the Core Animation lock (IOSurfaceLayer.embedderShowsFrame), skips the main-queue block. The test of that case is now a guard (it expects the main present, in both modes). ghostty.h (finding 6): the frame that waits for a present is one whose completion left its present to the embedder; after giving bounds without a present, call with present true when it returns READY. (cherry picked from commit 3d20928)
…till this one (Codex review FIX at 3d20928) `published` recorded that publishFresh ran, not that the frame is still kept: a host change after it (A -> B -> A) drops the kept frame, and a skip then left the frame nowhere. The skip is now decided in Metal.embedderShowsKeptFrame, under the Core Animation lock and then fresh.mutex (the order the main thread takes them in hostFrame's lock span, so there is no inversion): the layer cannot show the frame in its bounds now (IOSurfaceLayer.embedderShowsFrameLocked) and the kept fresh frame is this surface, at its present sequence and host size generation. A drop after that decision is a host change, whose new size the renderer draws. A declined publish (a newer frame kept) fails the check too. New test: a kept frame that a host change dropped since its publish still goes through main (embedderShowsKeptFrame false after the host went to 160 and back to 120). (cherry picked from commit 17ccbcf)
…e rule; test the new size's frame replaces it Codex review FIX at 17ccbcf: after the completion skipped the main-thread present of a kept frame, a host change A -> B -> A drops it, and the old queued block could have shown that frame for the second A. The fork's rule for kept frames (setHostDrawableSize) says no frame kept from before a host change is the new size's frame, even at an equal size; the renderer draws a new size at once while visible (Thread: resize sets `resized`, drawsNow), and while it holds its frames the layer keeps its last frame (ready-or-keep). embedderShowsKeptFrame's comment and ghostty.h now say so. New test: after A -> B -> A and the embedder's A bounds without a present, hostFrame(false) is PENDING (the layer keeps its last frame, no stale frame), and the renderer's frame of the second A presents itself through main. (cherry picked from commit cd11dc5)
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the
|
|
Review (independent subagent): SHIP at c7d97b5 I found no correctness defect. This PR is the change of #75 on the sync line. The sync line does not change my analysis of #75 (#75 (comment)). Scope. I compared the two patch series. I also compared the two lines in each file that my #75 analysis uses. I did not build or run tests. I did not read the cmux side. 1. The patch is the same as #75
2. The sync line does not change the analysis
3. Evidence
Findings (not blocking; the same as on #75, with the line numbers of this head)
Review (independent subagent): SHIP at c7d97b5 |
Summary
release/cmux-9ce9b7d0-e2), cherry-picked with-xontorelease/cmux-sync-20261104(the upstream-sync line that cmux PR ghostty-next: sync to upstream Ghostty c7f68a7f4 (Zig 0.17.0), fixes the CVDisplayLinkStop freeze cmux#35478 pins). The ghostty-next pin carries the change whichever cmux PR moves it first.GHOSTTY_HOST_PRESENT_EMBEDDER, a frame of the host size kept for the embedder's present, which the layer cannot show in its bounds now, goes to no main-queue block (setSurfaceCallbackdropped it there). The skip is decided under the Core Animation lock and thenfresh.mutex, only while the kept frame is still that frame (Metal.embedderShowsKeptFrame). All other frames still go through main. See renderer: a frame the embedder presents itself wakes no main-thread present #75 for the full description, the kept-frame rule after a host change, and the four Codex rounds.IOSurfaceLayer.zigis the same file on both lines;Metal.zig,ghostty.handNEXT.mddiffer only outside the changed parts.Testing
scratch/embedder-wake-sync-greenonly narrows the workflow's test filter tohost present:, never merged): https://github.com/manaflow-ai/ghostty-next/actions/runs/41920331766: 109/109 and lib-vt 97/97.Observed / Not run
zig fmt --checkandzig ast-checkwith Zig 0.17.0.Changelog
none
Open in [code]smith · Need help on this PR? Tag
@codesmith-botwith what you need.Summary by cubic
Cherry-picks #75 onto
release/cmux-sync-20261104so the ghostty-next pin carries the fix whichever cmux PR moves it first. WhenGHOSTTY_HOST_PRESENT_EMBEDDERis on, a frame of the host size kept for the embedder's present no longer wakes the main thread:setSurfaceCallbackpreviously dropped it after dispatching to the main queue.fresh.mutex, and only while the kept frame is still that frame; a host change drops it, so a skipped frame never shows for a later equal size (A->B->A).Written for commit c7d97b5. Summary will update on new commits.