Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion NEXT.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ thread. New tab and split surfaces do not inherit the IO fields.
| embedded: a host-size present that never waits | `ghostty_surface_present_host_frame_ready` is `ghostty_surface_present_host_frame` without the bounded wait (`Metal.presentHostSizedWait(false)`): it presents the renderer thread's frame at the host size in the caller's Core Animation transaction only if that frame is already published, otherwise it keeps the last frame (counted in `host_late_presents`) and the new frame lands through `setSurface` when it completes. cmux-next measured the waiting present at up to 4 ms of main per drag frame (main p95 20.5 -> 24.1 ms, more dropped frames), so it calls only this one, from one observer just before the commit. |
| renderer: a content-only frame goes to the screen without the main thread | A frame of a host-sized surface whose drawable size is the host size and equals the frame the layer shows (no geometry change pending) is presented from the GPU completion thread in its own `CATransaction` (`IOSurfaceLayer.setSurfaceOffMain`), not by a `dispatch_async` to main: a terminal echo reaches the screen while the app's main thread is busy (cmux-next G9). A frame for a new size still goes through main with its geometry. Frames are numbered in completion order (`Metal.present_seq`) and both paths check the number under the Core Animation lock, so a late main-thread present never replaces a newer frame. Counted in `host_off_main_presents`. Covered by cmux-next `TerminalEchoOffMainTests`. |
| input: one compiled link regex per pattern for the whole process | `input/link_regex.zig`: surfaces and their renderers share each compiled link regex, counted by its users; up to 16 unused patterns stay compiled. A new surface compiled the URL regex twice on the main thread (Surface `DerivedConfig` and the renderer `link.Set`, about 0.5 ms each: cmux workspace switch trace, 6.3 ms of `onig_compile` per switch). Oniguruma 6.9.9 search never writes to a `regex_t`, so one regex serves every thread. |
| renderer + embedded: the embedder presents a new host size with its bounds | `ghostty_surface_set_host_present(surface, GHOSTTY_HOST_PRESENT_EMBEDDER)` (macOS, host-sized surfaces; default `GHOSTTY_HOST_PRESENT_RENDERER`): the renderer still draws each new host size at once with cells rebuilt for it and keeps that frame (`Metal.fresh`, with its present sequence), but never shows a frame of another size than the shown one itself: `setHostDrawableSize` asks for no display pass, a display pass never waits (`displayWhileEmbedderPresents`), and `setSurfaceCallback` refuses such a frame unless the layer's bounds already have its size. `ghostty_surface_host_frame(surface, present)` (`Metal.hostFrame`) answers 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, so a late main-thread frame never replaces it). cmux-next (frame owner refactor S4) changes a terminal view's bounds only in the transaction that shows the frame at the new size, or keeps both: before, a new-size frame landed in the old bounds 2-4 refreshes after the geometry. Frames at the shown size still go off main (an off-main present never commits while it holds the Core Animation lock: that deadlocked against main's commit in cmux's S4 films), except from the mode's start and after a main-thread change of the shown size until the embedder reports its commit (`ghostty_surface_host_frame_committed`: Ghostty cannot see the main thread's commit); a first frame presents itself; `ghostty_surface_present_host_frame(_ready)` follow the same ordered present in this mode. Tests: `host present:` in `renderer/Metal.zig` and `renderer/metal/IOSurfaceLayer.zig` (real layer and IOSurfaces, no GPU). |
| renderer + embedded: the embedder presents a new host size with its bounds | `ghostty_surface_set_host_present(surface, GHOSTTY_HOST_PRESENT_EMBEDDER)` (macOS, host-sized surfaces; default `GHOSTTY_HOST_PRESENT_RENDERER`): the renderer still draws each new host size at once with cells rebuilt for it and keeps that frame (`Metal.fresh`, with its present sequence), but never shows a frame of another size than the shown one itself: `setHostDrawableSize` asks for no display pass, a display pass never waits (`displayWhileEmbedderPresents`), and `setSurfaceCallback` refuses such a frame unless the layer's bounds already have its size. `ghostty_surface_host_frame(surface, present)` (`Metal.hostFrame`) answers 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, so a late main-thread frame never replaces it). cmux-next (frame owner refactor S4) changes a terminal view's bounds only in the transaction that shows the frame at the new size, or keeps both: before, a new-size frame landed in the old bounds 2-4 refreshes after the geometry. Frames at the shown size still go off main (an off-main present never commits while it holds the Core Animation lock: that deadlocked against main's commit in cmux's S4 films), except from the mode's start and after a main-thread change of the shown size until the embedder reports its commit (`ghostty_surface_host_frame_committed`: Ghostty cannot see the main thread's commit); a first frame presents itself; `ghostty_surface_present_host_frame(_ready)` follow the same ordered present in this mode. A frame that the embedder shows itself (one of the host size, kept for its present, while the view keeps bounds of another size; checked under the Core Animation lock and then `fresh.mutex`, while the kept frame is still this one, `Metal.embedderShowsKeptFrame`) goes to no main-queue block: `setSurfaceCallback` dropped it there, one main-thread wake per moving terminal and frame in cmux-next's held key resize. A first frame, a frame for bounds the embedder already set and a frame of a size the host no longer has (not kept anywhere; main rechecks the host size) still go through main. Tests: `host present:` in `renderer/Metal.zig` and `renderer/metal/IOSurfaceLayer.zig` (real layer and IOSurfaces, no GPU). |

Next in the stack (tracked in the design): presentation callbacks for
frame-exact acknowledgment, Kitty image replay after a snapshot, and a
Expand Down
15 changes: 11 additions & 4 deletions include/ghostty.h
Original file line number Diff line number Diff line change
Expand Up @@ -1596,10 +1596,17 @@ typedef enum {
// replaces a newer one. Never waits and never draws. Counted in
// host_present_calls (present true), host_fresh_presents (PRESENTED) and
// host_late_presents (PENDING or HELD with present true). A view whose
// bounds were given without a present shows the new frame when it is drawn;
// until then, or while the bounds stay old, call again until it returns
// SHOWN, NONE or HIDDEN. READY does not reserve the frame: a hide can drop it
// before the present (HIDDEN then).
// bounds were given without a present shows a frame that completes after that
// by itself; a frame whose completion already left its present to the
// embedder (it completed while the bounds were old) waits for a present: after
// giving bounds without a present, call with present true when it returns
// READY. Until the frame shows, or while the bounds stay old, call again until
// it returns SHOWN, NONE or HIDDEN. READY does not reserve the frame: a hide can
// drop it before the present (HIDDEN then), and a change of the host size
// drops it too, even back to an equal size after another one (A->B->A): a
// frame kept for a present before that change is not presented for the new
// size (the renderer draws a new size at once; while it holds its frames, the
// layer keeps its last frame).
// Call it with present true, and give the view its bounds, in one explicit
// [CATransaction lock] span. An open transaction, a layout pass or a display
// pass alone is not such a span. No off-main present runs its checks or sets
Expand Down
Loading