Skip to content

renderer: only start and stop the CVDisplayLink on the main thread - #14691

Merged
mitchellh merged 1 commit into
mainfrom
macos-displaylink
Oct 13, 2026
Merged

mitchellh merged 1 commit into
mainfrom
macos-displaylink

Conversation

@mitchellh

Copy link
Copy Markdown
Contributor

Fixes #14150

The render thread started and stopped its CVDisplayLink directly. Turns out this is NOT SAFE TO DO!

I actually couldn't find any docs or anything to point this out to me and I had to rely on Fable-assistance disassembling CoreVideo frameworks to confirm the behavior. I read the disassembly myself and can confirm the reading matches what I'd see.

CoreVideo performs the very same stop and start on the main thread inside its display reconfiguration callback, which fires on display sleep and wake, resolution changes, etc.

CVDisplayLinkStop clears the link's running flag and then waits, once and unconditionally, for the IO thread to notice and exit. The link's mutex is released during that wait. If CVDisplayLinkStart lands in that window, the flag is set again and a second IO thread is created. The first IO thread wakes, sees the flag set, and keeps looping, so the waiting stop is never signaled. Whichever side lost was stuck in CVDisplayLinkStop forever.

In the reports that was sometimes our render thread and sometimes CoreVideo's own call on the main thread. Either way the main thread eventually waited on the renderer (focus change, tab close, quit) and the whole app froze with no "Not Responding" indication. The spindumps all show one CVDisplayLink object with two live IO threads, which is exactly this problem.

Fixes #14150

The render thread started and stopped its CVDisplayLink directly.
Turns out this is NOT SAFE TO DO!

I actually couldn't find any docs or anything to point this out to me
and I had to rely on Fable-assistance disassembling CoreVideo frameworks
to confirm the behavior. I read the disassembly myself and can confirm
the reading matches what I'd see.

CoreVideo performs the very same stop and start on the main thread 
inside its display reconfiguration callback, which fires on display sleep 
and wake, resolution changes, etc.

CVDisplayLinkStop clears the link's running flag and then waits, once
and unconditionally, for the IO thread to notice and exit. The link's
mutex is released during that wait. If CVDisplayLinkStart lands in
that window, the flag is set again and a second IO thread is created.
The first IO thread wakes, sees the flag set, and keeps looping, so
the waiting stop is never signaled. Whichever side lost was stuck in
CVDisplayLinkStop forever. 

In the reports that was sometimes our render thread and sometimes 
CoreVideo's own call on the main thread. Either way the main thread 
eventually waited on the renderer (focus change, tab close, quit) and the 
whole app froze with no "Not Responding" indication. The spindumps all show 
one CVDisplayLink object with two live IO threads, which is exactly this 
problem.
@mitchellh mitchellh added this to the 1.4.0 milestone Oct 13, 2026
@mitchellh
mitchellh requested review from a team as code owners October 13, 2026 22:02
@ghostty-bot ghostty-bot Bot added the renderer label Oct 13, 2026
@mitchellh
mitchellh merged commit 8159838 into main Oct 13, 2026
211 of 215 checks passed
@mitchellh
mitchellh deleted the macos-displaylink branch October 13, 2026 23:00
@erickochen

Copy link
Copy Markdown

Thank you for fixing @mitchellh

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants