@@ -100,60 +100,13 @@ public NSRect(double x, double y, double width, double height)
100100
101101 private delegate void dispatch_function_t ( IntPtr context ) ;
102102
103- // CFRunLoop API - supports explicit RunLoop modes
104- [ DllImport ( "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation" ) ]
105- private static extern IntPtr CFRunLoopGetMain ( ) ;
106-
107- [ StructLayout ( LayoutKind . Sequential ) ]
108- private struct CFRunLoopTimerContext
109- {
110- public IntPtr version ;
111- public IntPtr info ;
112- public IntPtr retain ;
113- public IntPtr release ;
114- public IntPtr copyDescription ;
115- }
116-
117- [ DllImport ( "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation" ) ]
118- private static extern IntPtr CFRunLoopTimerCreate (
119- IntPtr allocator ,
120- double fireDate ,
121- double interval ,
122- ulong flags ,
123- long order ,
124- CFRunLoopTimerCallBack callout ,
125- ref CFRunLoopTimerContext context ) ;
126-
127- [ DllImport ( "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation" ) ]
128- private static extern void CFRunLoopAddTimer ( IntPtr rl , IntPtr timer , IntPtr mode ) ;
129-
130- [ DllImport ( "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation" ) ]
131- private static extern double CFAbsoluteTimeGetCurrent ( ) ;
132-
133- [ DllImport ( "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation" ) ]
134- private static extern void CFRelease ( IntPtr cf ) ;
135-
136- [ DllImport ( "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation" ) ]
137- private static extern void CFRunLoopWakeUp ( IntPtr rl ) ;
138-
139- private delegate void CFRunLoopTimerCallBack ( IntPtr timer , IntPtr info ) ;
140-
141- // Static delegate reference to prevent GC collection
142- private static readonly CFRunLoopTimerCallBack TimerCallbackDelegate = TimerCallback ;
143-
144- // kCFRunLoopCommonModes constant (CFStringRef)
145- // This mode set contains both NSDefaultRunLoopMode AND NSEventTrackingRunLoopMode
146- // So our callback runs even when a dropdown menu is open (tracking mode)
147- private static readonly IntPtr kCFRunLoopCommonModes ;
148-
149- // dlopen/dlsym for loading framework constants and dispatch queue
103+ // dlopen/dlsym for loading the GCD main queue symbol
150104 [ DllImport ( "/usr/lib/libSystem.B.dylib" ) ]
151105 private static extern IntPtr dlopen ( string path , int mode ) ;
152106
153107 [ DllImport ( "/usr/lib/libSystem.B.dylib" ) ]
154108 private static extern IntPtr dlsym ( IntPtr handle , string symbol ) ;
155109
156- private const int RTLD_LAZY = 1 ;
157110 private const int RTLD_DEFAULT = - 2 ; // Special handle for default search
158111
159112 static NativeWindowMac ( )
@@ -164,28 +117,23 @@ static NativeWindowMac()
164117 if ( mainQueueSymbol == IntPtr . Zero )
165118 throw new InvalidOperationException ( "Failed to load _dispatch_main_q symbol." ) ;
166119 _mainQueue = mainQueueSymbol ;
167-
168- // Load kCFRunLoopCommonModes from CoreFoundation. It is a CFStringRef* exported
169- // as a data symbol, so we dereference the pointer to get the CFStringRef value.
170- IntPtr cfHandle = dlopen ( "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation" , RTLD_LAZY ) ;
171- if ( cfHandle != IntPtr . Zero )
172- {
173- IntPtr symbolPtr = dlsym ( cfHandle , "kCFRunLoopCommonModes" ) ;
174- if ( symbolPtr != IntPtr . Zero )
175- kCFRunLoopCommonModes = Marshal . ReadIntPtr ( symbolPtr ) ;
176- }
177-
178- if ( kCFRunLoopCommonModes == IntPtr . Zero )
179- throw new InvalidOperationException ( "Failed to load kCFRunLoopCommonModes from CoreFoundation." ) ;
180120 }
181121
182122 private static bool IsMainThread ( ) => pthread_main_np ( ) != 0 ;
183123
184124 /// <summary>
185- /// Asynchronously executes an operation on the macOS main thread.
186- /// Uses CFRunLoopTimer with kCFRunLoopCommonModes, which ensures
187- /// that the callback runs even when a dropdown menu is in tracking mode.
188- /// This solves VST plugin editor freezing on macOS.
125+ /// Asynchronously executes an operation on the macOS main thread via GCD.
126+ ///
127+ /// Uses dispatch_async_f instead of CFRunLoopTimer to avoid a deadlock:
128+ /// CFRunLoopTimer with kCFRunLoopCommonModes fires during NSEventTrackingRunLoopMode
129+ /// (NSMenu popup tracking). A .NET P/Invoke callback executing inside that tracking
130+ /// event loop can deadlock with AppKit on Apple Silicon macOS 12+, causing the
131+ /// plugin editor UI to freeze permanently when a dropdown is clicked.
132+ ///
133+ /// GCD dispatch_async_f uses the main queue's own dispatch source, which is separate
134+ /// from the RunLoop timer infrastructure and does not exhibit this interaction.
135+ /// The _idlePending throttle in VstEditorController ensures at most one pending
136+ /// callback at a time, so no accumulation occurs during tracking mode.
189137 /// </summary>
190138 private static void DispatchToMainAsync ( Action action )
191139 {
@@ -195,50 +143,8 @@ private static void DispatchToMainAsync(Action action)
195143 return ;
196144 }
197145
198- // Create GCHandle - ensures the Action is not garbage collected
199146 var handle = GCHandle . Alloc ( action ) ;
200-
201- // Create CFRunLoopTimerContext with the GCHandle as info
202- var context = new CFRunLoopTimerContext
203- {
204- version = IntPtr . Zero , // version 0
205- info = GCHandle . ToIntPtr ( handle ) , // our GCHandle
206- retain = IntPtr . Zero , // no retain callback
207- release = IntPtr . Zero , // no release callback
208- copyDescription = IntPtr . Zero // no description callback
209- } ;
210-
211- // Create one-shot timer that fires immediately on the main RunLoop
212- // fireDate: current time (immediately)
213- // interval: 0 (one-shot, does not repeat)
214- double fireDate = CFAbsoluteTimeGetCurrent ( ) ;
215- IntPtr timer = CFRunLoopTimerCreate (
216- IntPtr . Zero , // allocator (default)
217- fireDate , // fireDate (now, immediately)
218- 0 , // interval (0 = one-shot)
219- 0 , // flags
220- 0 , // order
221- TimerCallbackDelegate , // callback (use static delegate to prevent GC)
222- ref context ) ; // context struct
223-
224- if ( timer == IntPtr . Zero )
225- {
226- handle . Free ( ) ;
227- throw new InvalidOperationException ( "Failed to create CFRunLoopTimer" ) ;
228- }
229-
230- // Add timer to main RunLoop with kCFRunLoopCommonModes
231- // This guarantees that the timer fires even when:
232- // - NSDefaultRunLoopMode is active (normal operation)
233- // - NSEventTrackingRunLoopMode is active (dropdown menu, scrolling, etc.)
234- IntPtr mainRunLoop = CFRunLoopGetMain ( ) ;
235- CFRunLoopAddTimer ( mainRunLoop , timer , kCFRunLoopCommonModes ) ;
236-
237- // Wake up RunLoop if it's waiting
238- CFRunLoopWakeUp ( mainRunLoop ) ;
239-
240- // Release timer - the RunLoop owns it until it fires
241- CFRelease ( timer ) ;
147+ dispatch_async_f ( _mainQueue , GCHandle . ToIntPtr ( handle ) , DispatchCallback ) ;
242148 }
243149
244150 /// <summary>
@@ -258,26 +164,7 @@ private static void DispatchToMainSync(Action action)
258164 }
259165
260166 /// <summary>
261- /// CFRunLoopTimer callback - runs on the main thread (RunLoop context).
262- /// Unwraps the Action from the GCHandle and executes it.
263- /// The 'timer' parameter is the CFRunLoopTimer reference, 'info' is the context (GCHandle pointer).
264- /// </summary>
265- private static void TimerCallback ( IntPtr timer , IntPtr info )
266- {
267- var handle = GCHandle . FromIntPtr ( info ) ;
268- try
269- {
270- var action = ( Action ) handle . Target ! ;
271- action ( ) ;
272- }
273- finally
274- {
275- handle . Free ( ) ;
276- }
277- }
278-
279- /// <summary>
280- /// GCD callback (used for dispatch_sync) - runs on the main thread.
167+ /// GCD callback - runs on the main thread (dispatch_sync and dispatch_async).
281168 /// Unwraps the Action from the GCHandle and executes it.
282169 /// </summary>
283170 private static void DispatchCallback ( IntPtr context )
0 commit comments