Skip to content

Harden Metal presentation lifetimes - #148

Open
x90skysn3k wants to merge 2 commits into
manaflow-ai:mainfrom
x90skysn3k:fix/ghostty-presentation-lifetimes-upstream
Open

Harden Metal presentation lifetimes#148
x90skysn3k wants to merge 2 commits into
manaflow-ai:mainfrom
x90skysn3k:fix/ghostty-presentation-lifetimes-upstream

Conversation

@x90skysn3k

@x90skysn3k x90skysn3k commented Jul 25, 2026

Copy link
Copy Markdown

Fixes verified replay completion safety. Adds renderer lifetime invalidation, generation-based stale-frame rejection, and moves synchronous callback delivery outside the draw mutex while preserving current swap-chain drain and iOS queued behavior.\n\nVerified: zig build test -Drenderer=metal passes.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Hardens Metal presentation lifetimes to prevent stale or late GPU completions from mutating UI or renderer state after resize or teardown. Adds generation-based validation and moves sync presentation callback delivery outside the draw mutex while preserving swap-chain drain and iOS’s queued behavior.

  • Bug Fixes
    • Generation-tracked IOSurfaceLayer lifetime rejects stale frames and cancels after teardown.
    • Surface size is clamped and observed; presentation_generation is passed through frame/present paths.
    • setSurfaceSync validates generation and size; returns false instead of touching invalid state.
    • Sync presentation returns the callback to the caller; delivery occurs after draw_mutex unlocks (async and iOS unchanged).
    • Added tests for lifetime invalidation and stale-generation rejection.

Written for commit d7df42d. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Improved Metal presentation reliability by preventing stale or invalid frame updates from reaching the display.
    • Prevented queued presentation callbacks from affecting detached or inactive surfaces.
    • Added safeguards to reject surfaces whose dimensions do not match the current drawable area.
    • Clamped runtime surface sizes to supported device texture limits.
    • Improved synchronization for frame completion and presentation updates to reduce visual glitches during resizing or rapid frame changes.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Metal presentation now carries a generation token through frame submission, completion, and IOSurface assignment. IOSurfaceLayer validates lifetime, generation, update activity, and surface dimensions before applying synchronous or queued updates.

Changes

Metal presentation generation

Layer / File(s) Summary
Presentation lifetime and generation state
src/renderer/metal/IOSurfaceLayer.zig
IOSurfaceLayer tracks a retained presentation lifetime, invalidates it during teardown, and exposes generation observation, retrieval, and advancement.
Generation-aware surface updates
src/renderer/metal/IOSurfaceLayer.zig
Surface update contexts carry lifetime and generation data; synchronous and queued assignments reject stale, inactive, or incorrectly sized surfaces.
Metal generation propagation
src/renderer/Metal.zig
Metal observes clamped layer sizes, advances generations between frame generations, and passes the active generation into presentation and frame APIs.
Generation-aware frame completion
src/renderer/metal/Frame.zig
Frame completion propagates generations, returns synchronous tokened presentations when accepted, and updates completion tests for invalidated lifetimes.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Metal
  participant Frame
  participant IOSurfaceLayer
  Metal->>IOSurfaceLayer: Observe clamped presentation size
  IOSurfaceLayer-->>Metal: Return presentation generation
  Metal->>Frame: Begin frame with generation
  Frame->>Metal: Present tokened frame with generation
  Metal->>IOSurfaceLayer: Assign surface synchronously
  IOSurfaceLayer-->>Metal: Return assignment result
Loading

Possibly related PRs

Suggested reviewers: azooz2003-bit

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the main change: hardening Metal presentation lifetime handling.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@greptile-apps

greptile-apps Bot commented Jul 25, 2026

Copy link
Copy Markdown

Greptile Summary

Harden Metal frame presentation against stale and late GPU completions.

  • Adds independently retained presentation lifetime state with invalidation and generation-based stale-frame rejection.
  • Propagates presentation generations through frame submission and validates layer size before synchronous or queued assignment.
  • Moves synchronous presentation callback delivery outside the renderer draw mutex while preserving asynchronous iOS delivery.
  • Adds tests covering invalidated renderer lifetimes and stale same-size generations.

Confidence Score: 5/5

The PR appears safe to merge, with presentation ownership, invalidation, and stale-frame rejection consistently applied across the changed Metal paths.

Synchronous frames have one explicit completion path, asynchronous frames retain the state needed by queued callbacks, and both paths reject invalid or mismatched presentation generations before touching layer state.

Important Files Changed

Filename Overview
src/renderer/Metal.zig Threads presentation generations through Metal frame creation and presentation while invalidating layer generations at renderer-lifetime boundaries.
src/renderer/metal/Frame.zig Separates synchronous completion processing from callback delivery and retains asynchronous queued behavior on iOS.
src/renderer/metal/IOSurfaceLayer.zig Introduces ref-counted, mutex-protected presentation lifetime and generation validation for synchronous and queued layer updates.

Sequence Diagram

sequenceDiagram
    participant Renderer
    participant Frame
    participant GPU
    participant Lifetime as PresentationLifetime
    participant Layer as IOSurfaceLayer
    participant Callback

    Renderer->>Lifetime: observe drawable size
    Renderer->>Frame: begin(generation)
    Frame->>GPU: commit command buffer
    GPU-->>Frame: completion
    Frame->>Lifetime: acquire(generation)
    alt lifetime and generation valid
        Frame->>Layer: assign validated IOSurface
        Frame-->>Renderer: return synchronous presentation
        Renderer->>Renderer: unlock draw mutex
        Renderer->>Callback: deliver acknowledgement
    else invalidated or stale
        Lifetime-->>Frame: reject acquisition
        Frame-->>Layer: discard presentation
    end
Loading

Reviews (1): Last reviewed commit: "Merge pull request #1 from x90skysn3k/fi..." | Re-trigger Greptile

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/renderer/metal/IOSurfaceLayer.zig (1)

328-367: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Presentation tokens are dropped on every generation/size rejection path. Both the queued and synchronous presentation paths abandon the FramePresentation when the new generation, activity, or size guard fails, so the embedder never receives the acknowledgement for that token.

  • src/renderer/metal/IOSurfaceLayer.zig#L328-L367: let the guard failures fall through to the delivery block at Lines 362-367 instead of returning from setSurfaceCallback, or document that rejected updates intentionally drop the token.
  • src/renderer/metal/Frame.zig#L175-L186: when presentWithPresentation returns null, still return value so the generic caller delivers it after draw_mutex unlocks (or document the drop).
🤖 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/renderer/metal/IOSurfaceLayer.zig` around lines 328 - 367, Ensure
rejected presentations still deliver their presentation tokens: in
src/renderer/metal/IOSurfaceLayer.zig lines 328-367, change the generation,
activity, and size guard flow so it reaches the existing presentation delivery
block instead of returning from setSurfaceCallback; in
src/renderer/metal/Frame.zig lines 175-186, make the presentWithPresentation
null result return value so the generic caller delivers it after draw_mutex is
unlocked.
🧹 Nitpick comments (3)
src/renderer/Metal.zig (1)

399-409: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Silent drop when setSurfaceSync rejects.

The bool is discarded here, and setSurfaceSync only logs on the size-mismatch path — lease/generation rejection (Line 276 of IOSurfaceLayer.zig) and the inactive-updates path return false with no trace. During a resize storm this presents as frozen output with nothing in the log. A log.debug on the false branch would make it diagnosable.

🤖 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/renderer/Metal.zig` around lines 399 - 409, Add handling to the sync
branch around setSurfaceSync so its returned bool is checked and a log.debug is
emitted when it returns false, covering rejected leases/generations and inactive
updates. Preserve the existing setSurface error propagation and successful sync
behavior.
src/renderer/metal/IOSurfaceLayer.zig (2)

418-422: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

create() hard-codes std.heap.c_allocator.

CompletionLifetime.create(alloc) takes an allocator; this one does not, so allocation is untracked in tests and cannot use the renderer allocator. Consider threading the allocator through IOSurfaceLayer.init() for consistency.

🤖 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/renderer/metal/IOSurfaceLayer.zig` around lines 418 - 422, Update
IOSurfaceLayer.create and the surrounding IOSurfaceLayer.init construction path
to accept and use the caller-provided allocator instead of hard-coding
std.heap.c_allocator. Thread that allocator through all relevant call sites,
preserving existing initialization behavior while enabling renderer and test
allocators to track the allocation.

410-448: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

acquire returns a mutex-owning lease — document the discard hazard.

Live holds owner.mutex until deinit(). Any caller that discards the result (_ = lifetime.acquire(g);) or forgets the defer leaves the lifetime mutex permanently locked, deadlocking every later observeSize/invalidate/acquire. A doc comment on acquire/Live (and ideally defer live.deinit() immediately at each call site, which the current callers do) makes the contract explicit.

🤖 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/renderer/metal/IOSurfaceLayer.zig` around lines 410 - 448, Add
documentation to the Live lease and acquire method stating that a successful
acquire retains owner.mutex and must be paired with live.deinit(), including
when the returned lease is otherwise unused. Verify each current acquire call
site immediately defers deinit on the acquired lease, while preserving the
existing invalid-generation null return behavior.
🤖 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.

Outside diff comments:
In `@src/renderer/metal/IOSurfaceLayer.zig`:
- Around line 328-367: Ensure rejected presentations still deliver their
presentation tokens: in src/renderer/metal/IOSurfaceLayer.zig lines 328-367,
change the generation, activity, and size guard flow so it reaches the existing
presentation delivery block instead of returning from setSurfaceCallback; in
src/renderer/metal/Frame.zig lines 175-186, make the presentWithPresentation
null result return value so the generic caller delivers it after draw_mutex is
unlocked.

---

Nitpick comments:
In `@src/renderer/Metal.zig`:
- Around line 399-409: Add handling to the sync branch around setSurfaceSync so
its returned bool is checked and a log.debug is emitted when it returns false,
covering rejected leases/generations and inactive updates. Preserve the existing
setSurface error propagation and successful sync behavior.

In `@src/renderer/metal/IOSurfaceLayer.zig`:
- Around line 418-422: Update IOSurfaceLayer.create and the surrounding
IOSurfaceLayer.init construction path to accept and use the caller-provided
allocator instead of hard-coding std.heap.c_allocator. Thread that allocator
through all relevant call sites, preserving existing initialization behavior
while enabling renderer and test allocators to track the allocation.
- Around line 410-448: Add documentation to the Live lease and acquire method
stating that a successful acquire retains owner.mutex and must be paired with
live.deinit(), including when the returned lease is otherwise unused. Verify
each current acquire call site immediately defers deinit on the acquired lease,
while preserving the existing invalid-generation null return behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 364521f0-b79b-4c40-ae0b-142f2cf53258

📥 Commits

Reviewing files that changed from the base of the PR and between cfd39f3 and d7df42d.

📒 Files selected for processing (3)
  • src/renderer/Metal.zig
  • src/renderer/metal/Frame.zig
  • src/renderer/metal/IOSurfaceLayer.zig

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.

1 participant