renderer + embedded: the embedder presents a new host size with its bounds (set_host_present, host_frame)
Merged
LA
2d ago+1124+1124+0123456789012345678901234567890123456789-14-14-01234567890123456789
Summary
- cmux-next (frame owner refactor, stage S4 "terminals ready-or-keep") needs this rule for every terminal in every refresh: the view's bounds and the layer's pixels change together, or neither changes. With the renderer showing new host sizes itself, a new-size frame goes to main by
dispatch_async, andsetSurfaceCallbacktakes any frame of the host size whatever the view's bounds. In cmux's 120 Hz films the new-size frame landed in the old bounds 2-4 refreshes after the geometry, and two terminals landed 2 refreshes apart. The waiting present (ghostty_surface_present_host_frame, cmux G1) fixed the order but cost main 4-9 ms per drag frame. - New, opt-in, macOS host-sized surfaces only (the default does not change; cmux does not call it until its own PR):
ghostty_surface_set_host_present(surface, GHOSTTY_HOST_PRESENT_EMBEDDER): a new host size asks for no display pass (a first frame still does), and a display pass never waits. The renderer still draws each new size at once with cells rebuilt for it and keeps that frame (Metal.fresh, now with its present sequence).setSurfaceCallbackrefuses a frame of another size than the shown one, unless the layer's bounds already have its size. Frames at the shown size still go off main. Back toGHOSTTY_HOST_PRESENT_RENDERER, a frame drawn for the host size shows at once.ghostty_surface_host_frame(surface, present)returnsghostty_host_frame_e:NONE,HIDDEN,SHOWN,READY,PRESENTED,PENDINGorHELD. Withpresent, a READY frame becomes the layer's contents in the caller's Core Animation transaction (IOSurfaceLayer.setSurfacePresented), ordered by the present sequence under the CA lock: a frame that reaches main later never replaces it. It never waits and never draws. It is counted inhost_present_calls,host_fresh_presentsandhost_late_presents.
publishFreshkeeps only the newest frame by present sequence. A host size already on screen disarms publishing, so nothing publishes per frame at rest.- Base:
release/cmux-9ce9b7d0-e2(cmux's pin34e73ba8b+ #69), so cmux can pin it without the v1.4.0 / Zig 0.17 merge onmain. A port tomainis a follow-up. - Chromium does the same at a resize: the browser compositor shows a new size only with a frame the renderer submitted for it, and keeps the old surface until then.
Testing
- Red: test commit
30b8490a1, run https://github.com/manaflow-ai/ghostty-next/actions/runs/41611127503 on scratch branchscratch/host-present-red. That branch only narrows the test filters tohost present:. - Green: fix
62df0adde, run https://github.com/manaflow-ai/ghostty-next/actions/runs/41611130888 onscratch/host-present-green. That branch addshost present:,ghostty.h HostPresentandghostty.h HostFrameto the full patch filter list. - The tests run on macOS with a real
IOSurfaceLayerand real IOSurfaces, no GPU. Controls (the default mode, a first frame, an off-main frame at the shown size, a frame that fits bounds the embedder set, the switch back to the renderer) pass on both commits. - Review fixes (independent review of 62df0adde): an off-main frame no longer commits while the layer shows an embedder present that may not be committed yet; a refused present re-arms publishing;
ghostty_surface_present_host_frame(_ready)follow the ordered present in the embedder mode. Red: test commite999522d3, run https://github.com/manaflow-ai/ghostty-next/actions/runs/41615130156 (3 of 3 new tests fail; 92 other tests pass). Green: fix5a9907c45, run https://github.com/manaflow-ai/ghostty-next/actions/runs/41615133466 (ghostty-test 233/233, lib-vt 475/475, build, no-gettext and link smoke pass). - The workflow's test filter list is not changed in this PR (no new CI test step). The scratch branches are never merged.
- Not run: the cmux side (
TerminalHostFramePresenterwith this API, 120 Hz films, typing under load). That is the next cmux PR, after this one is pinned.
Changelog
none