Skip to content

Expose exact iOS render presentation tokens - #116

Merged
lawrencecchen merged 4 commits into
mainfrom
feat/ios-render-present-token-main
Jul 22, 2026
Merged

Expose exact iOS render presentation tokens#116
lawrencecchen merged 4 commits into
mainfrom
feat/ios-render-present-token-main

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Jul 15, 2026

Copy link
Copy Markdown

Adds a tokened render callback that reports the IOSurface identity and presentation only after the matching Metal submission reaches the renderer layer.

cmux uses this to keep last-good terminal pixels visible until an atomic replay has verified the exact rendered frame.

Tracks manaflow-ai/cmux#7160

Verification: cmux iOS VerifiedReplaySubmissionTests and tagged macOS/iOS builds.


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Summary by cubic

Adds a tokened render-present callback on iOS that fires only after the exact IOSurface is assigned to the CALayer on the main thread. Enables cmux to keep last-good pixels until replay verifies the presented frame, addressing cmux issue ghostty-org#7160 across Metal and OpenGL.

  • New Features

    • C API: adds ghostty_render_presented_cb, render_presented_cb and render_presented_userdata in ghostty_surface_config_s, plus ghostty_surface_render_now_with_token(surface, token).
    • Embedded API: adds Surface.Options.render_presented_cb, render_presented_userdata, and Surface.renderNowWithToken(token).
    • Renderer: introduces FramePresentation; adds drawFrameWithPresentation and thread renderNowWithPresentation; beginFrameWithPresentation for Metal/OpenGL; Metal presentWithPresentation; iOS IOSurfaceLayer.setSurfaceWithPresentation.
    • Behavior: iOS invokes the callback on main only after layer size checks and IOSurface assignment; other platforms call after present (or immediately when sync). Tokens propagate across backends.
  • Migration

    • Optional: set render_presented_cb and use ghostty_surface_render_now_with_token (or Surface.renderNowWithToken).
    • No callback if the render fails or is dropped due to a size mismatch. Existing paths are unchanged.

Written for commit 24284c3. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added token-based “render presented” acknowledgment for embedded/forced renders.
    • Introduced a new callback (with caller token + user data) that fires after the presented IOSurface is assigned to the renderer layer.
    • Added a new API to trigger an immediate forced render tied to a token, using tokenized presentation on iOS while preserving prior behavior on other platforms.
  • Tests
    • Updated embedded surface ABI/layout validation for the expanded surface options.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The embedded rendering API adds tokenized forced renders and presentation callbacks. Tokens flow through renderer frame creation and platform presentation paths, reaching completion callbacks after successful surface presentation.

Changes

Render presentation flow

Layer / File(s) Summary
Public callback and forced-render API
include/ghostty.h, src/apprt/embedded.zig
Adds callback configuration, stores it during surface initialization, updates the options size test, and exposes tokenized forced-render APIs.
Presentation-aware render pipeline
src/renderer.zig, src/renderer/Thread.zig, src/renderer/generic.zig
Defines FramePresentation and propagates it through synchronous rendering, frame drawing, and graphics frame initialization.
Metal frame completion propagation
src/renderer/Metal.zig, src/renderer/metal/Frame.zig
Carries presentation metadata through Metal frame creation and selects presentation-aware or legacy presentation paths.
IOSurface presentation acknowledgment
src/renderer/metal/IOSurfaceLayer.zig
Passes callback metadata to main-thread IOSurface assignment and invokes the callback after layer contents are set.
OpenGL presentation propagation
src/renderer/OpenGL.zig, src/renderer/opengl/Frame.zig
Passes optional presentation metadata through OpenGL frame creation and invokes the callback after successful presentation.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Embedder
  participant SurfaceAPI
  participant RenderThread
  participant Renderer
  participant Metal
  participant IOSurfaceLayer
  Embedder->>SurfaceAPI: ghostty_surface_render_now_with_token(token)
  SurfaceAPI->>RenderThread: renderNowWithPresentation
  RenderThread->>Renderer: drawFrameWithPresentation(token)
  Renderer->>Metal: beginFrameWithPresentation
  Metal->>IOSurfaceLayer: setSurfaceWithPresentation
  IOSurfaceLayer-->>Embedder: render_presented_cb(userdata, token)
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: adding tokened render-presentation support for iOS and propagating it through the render stack.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ios-render-present-token-main

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/apprt/embedded.zig (1)

912-921: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Fallback to standard render if the callback is missing.

If renderNowWithToken is called but the embedder forgot to configure the presentation callback, returning early silently skips the render altogether. Since the caller explicitly invoked a render_now function, falling back to a standard render is much safer and avoids a silent no-op.

♻️ Proposed fallback
     pub fn renderNowWithToken(self: *Surface, token: u64) void {
-        const callback = self.render_presented_cb orelse return;
+        const callback = self.render_presented_cb orelse {
+            self.renderNow();
+            return;
+        };
         self.core_surface.applyPendingResizeIfNeeded();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/apprt/embedded.zig` around lines 912 - 921, Update
Surface.renderNowWithToken so a missing render_presented_cb falls back to the
standard renderNow path instead of returning without rendering. Preserve the
existing presentation callback flow, resize handling, userdata, and token
behavior when the callback is configured.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/renderer/Metal.zig`:
- Around line 275-291: Update presentWithPresentation so non-iOS calls with sync
== false route target.surface and presentation through
setSurfaceWithPresentation, allowing the callback to run on the main thread only
after successful assignment. Preserve the existing self.present(target, sync)
behavior for sync == true, but do not invoke presentation.callback directly in
the asynchronous path.

---

Nitpick comments:
In `@src/apprt/embedded.zig`:
- Around line 912-921: Update Surface.renderNowWithToken so a missing
render_presented_cb falls back to the standard renderNow path instead of
returning without rendering. Preserve the existing presentation callback flow,
resize handling, userdata, and token behavior when the callback is configured.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 87840465-2221-437a-a044-caa62f02ddc5

📥 Commits

Reviewing files that changed from the base of the PR and between 67b388b and d303f9c.

📒 Files selected for processing (8)
  • include/ghostty.h
  • src/apprt/embedded.zig
  • src/renderer.zig
  • src/renderer/Metal.zig
  • src/renderer/Thread.zig
  • src/renderer/generic.zig
  • src/renderer/metal/Frame.zig
  • src/renderer/metal/IOSurfaceLayer.zig

Comment thread src/renderer/Metal.zig
@greptile-apps

greptile-apps Bot commented Jul 15, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds tokened render presentation callbacks for embedded surfaces. The main changes are:

  • New C and Zig callback fields for rendered-token completion.
  • A forced render API that carries an opaque token through the renderer thread.
  • Metal/iOS presentation plumbing that calls back after the IOSurface reaches the layer.
  • Generic renderer changes to pass optional presentation data into backend frame creation.

Confidence Score: 4/5

The tokened render API needs a backend fallback fix before merging.

  • The iOS Metal flow preserves the token until layer assignment.
  • Backends without beginFrameWithPresentation can complete the draw while silently dropping the callback.
  • Embedders waiting for the token can remain stuck on stale terminal pixels.

src/renderer/generic.zig

Important Files Changed

Filename Overview
include/ghostty.h Adds the public C callback typedef, surface config fields, and tokened render API declaration.
src/apprt/embedded.zig Stores presentation callback state and exports the tokened render entry point.
src/renderer.zig Introduces the shared FramePresentation payload used by renderer backends.
src/renderer/Thread.zig Adds a forced render path that drains pending work and submits a frame with presentation metadata.
src/renderer/generic.zig Threads optional presentation metadata through frame creation, but drops it for backends without the new backend hook.
src/renderer/Metal.zig Adds Metal-specific frame and presentation helpers for tokened callbacks.
src/renderer/metal/Frame.zig Copies presentation callback data into the Metal command-buffer completion path.
src/renderer/metal/IOSurfaceLayer.zig Calls the presentation callback after the main-thread layer size guard and contents assignment.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Embedder
    participant Surface as embedded.Surface
    participant Thread as renderer.Thread
    participant Generic as generic.Renderer
    participant Metal as Metal Frame
    participant Layer as IOSurfaceLayer

    Embedder->>Surface: ghostty_surface_render_now_with_token(token)
    Surface->>Thread: renderNowWithPresentation(callback, userdata, token)
    Thread->>Generic: "drawFrameWithPresentation(sync=true, presentation)"
    Generic->>Metal: beginFrameWithPresentation(...)
    Metal-->>Metal: command buffer completion
    Metal->>Layer: setSurfaceWithPresentation(surface, presentation)
    Layer-->>Layer: main-thread size guard and contents assignment
    Layer->>Embedder: render_presented_cb(userdata, token)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Embedder
    participant Surface as embedded.Surface
    participant Thread as renderer.Thread
    participant Generic as generic.Renderer
    participant Metal as Metal Frame
    participant Layer as IOSurfaceLayer

    Embedder->>Surface: ghostty_surface_render_now_with_token(token)
    Surface->>Thread: renderNowWithPresentation(callback, userdata, token)
    Thread->>Generic: "drawFrameWithPresentation(sync=true, presentation)"
    Generic->>Metal: beginFrameWithPresentation(...)
    Metal-->>Metal: command buffer completion
    Metal->>Layer: setSurfaceWithPresentation(surface, presentation)
    Layer-->>Layer: main-thread size guard and contents assignment
    Layer->>Embedder: render_presented_cb(userdata, token)
Loading

Reviews (1): Last reviewed commit: "Add tokened render presentation callback..." | Re-trigger Greptile

Comment thread src/renderer/generic.zig
Comment on lines +1718 to +1724
var frame_ctx = if (presentation) |value|
if (@hasDecl(GraphicsAPI, "beginFrameWithPresentation"))
try self.api.beginFrameWithPresentation(self, &frame.target, value)
else
try self.api.beginFrame(self, &frame.target)
else
try self.api.beginFrame(self, &frame.target);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Presentation Token Gets Dropped

When ghostty_surface_render_now_with_token reaches a renderer backend without beginFrameWithPresentation, this branch still draws the frame but falls back to beginFrame and discards the callback payload. The caller sees a successful tokened render request, but no render_presented_cb can fire after presentation, so embedders waiting for that exact token can hang or keep stale pixels visible.

@lawrencecchen
lawrencecchen merged commit ae37964 into main Jul 22, 2026
151 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants