renderer: an off-main present begins a Core Animation transaction only for a frame it shows (sync line)

Merged
LA
13h ago
+121-13

Summary

The same two commits as manaflow-ai/ghostty-next#79 (cherry-picked with -x), for the sync line release/cmux-sync-20261104 that cmux #35478 pins. #79 has the independent review receipt (SHIP at 7f2dcbf36) and the red and green fork runs; the layer file is identical on both lines.

IOSurfaceLayer.setSurfaceOffMain began and committed a Core Animation transaction before its checks. So every frame that it did not show still committed an empty transaction on the GPU completion thread:

  • frames that go through main: a new size while the embedder presents host sizes, a frame the embedder has not committed, or another size than the shown one;
  • frames that a newer frame superseded.

cmux FRAME OWNER REFACTOR S4 measured this with Time Profiler (cmux step 4b41c42c, held keys-stress, 14 terminals, motion.frameOwner.terminals on): 405 ms of CA::Context::commit_transaction under the GPU completion queue in one round. Presents per round went from 84 (switch off) to about 850 (switch on). The new IOSurface per size cost only about +100 ms in the same round, and WindowServer's create_surface was the same with the switch off and on.

The checks now run first, read-only, under the Core Animation lock (offMainDecisionLocked). Only a frame that it shows begins a transaction. The transaction begins under the lock (a begin commits nothing), takes the contents, and commits after the unlock. So the invariant "no commit while holding the lock" still holds. Renderer mode (the default) gets the same change: a superseded frame no longer commits.

No C API change. No change to which frame is shown, or when.

Testing

  • Red first: 694f7ffb7 adds host present: an off-main present that does not show its frame commits no transaction. It counts the presenting thread's commits with the existing CommitOrder swizzle, for each refusal (no host size, another size than the host's, a layer that shows another size, a present the embedder has not committed, an older frame), and expects one commit, outside the lock, for a frame that it shows.
  • Fork CI, host present: filter only (scratch branches):
    • Red at 694f7ffb7, run 41953548703: 110/111 tests pass. Only the new test fails, at IOSurfaceLayer.zig:862, expected 0, found 1 (a layer that shows another size: the off-main attempt committed an empty transaction).
    • Green at 7f2dcbf36, run 41953551466: 111/111 tests pass, and lib-vt 97/97.
  • After merge: release build, cmux pin PR, then the 8-row gate A/B and a profile on cmux24-perf, switch off and on.

Autofix with [code]smith
Open in [code]smith · Need help on this PR? Tag @codesmith-bot with what you need.


Summary by cubic

Fixes IOSurfaceLayer.setSurfaceOffMain so an off-main present only begins a Core Animation transaction for a frame it actually shows. Previously every frame that went through main (new size, uncommitted embedder present, size mismatch) or was superseded still committed an empty transaction on the GPU completion thread; now the read-only checks run first under the Core Animation lock, and the transaction commits only after the unlock, preserving the no-commit-while-locked invariant. Adds a test asserting refusals commit nothing on the presenting thread while a shown frame commits exactly once, outside the lock.