renderer + embedded: the embedder presents a new host size with its bounds (set_host_present, host_frame)

Merged
LA
2d ago
+1124-14

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, and setSurfaceCallback takes 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). setSurfaceCallback refuses 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 to GHOSTTY_HOST_PRESENT_RENDERER, a frame drawn for the host size shows at once.
    • ghostty_surface_host_frame(surface, present) returns ghostty_host_frame_e: NONE, HIDDEN, SHOWN, READY, PRESENTED, PENDING or HELD. With present, 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 in host_present_calls, host_fresh_presents and host_late_presents.
  • publishFresh keeps 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 pin 34e73ba8b + #69), so cmux can pin it without the v1.4.0 / Zig 0.17 merge on main. A port to main is 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 branch scratch/host-present-red. That branch only narrows the test filters to host present: .
  • Green: fix 62df0adde, run https://github.com/manaflow-ai/ghostty-next/actions/runs/41611130888 on scratch/host-present-green. That branch adds host present: , ghostty.h HostPresent and ghostty.h HostFrame to the full patch filter list.
  • The tests run on macOS with a real IOSurfaceLayer and 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 commit e999522d3, run https://github.com/manaflow-ai/ghostty-next/actions/runs/41615130156 (3 of 3 new tests fail; 92 other tests pass). Green: fix 5a9907c45, 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 (TerminalHostFramePresenter with this API, 120 Hz films, typing under load). That is the next cmux PR, after this one is pinned.

Changelog

none