diff --git a/patches/README.txt b/patches/README.txt index 5c7b5d2..5f6b4f8 100644 --- a/patches/README.txt +++ b/patches/README.txt @@ -3,7 +3,7 @@ EMACS NS VOICEOVER ACCESSIBILITY PATCH patch: 0001-ns-implement-AXBoundsForRange-for-macOS-Zoom-cursor-.patch author: Martin Sukany files: src/nsterm.h (+108 lines) - src/nsterm.m (+2561 ins, -140 del, +2421 net) + src/nsterm.m (+2588 ins, -140 del, +2448 net) OVERVIEW @@ -159,6 +159,30 @@ THREADING MODEL return result; } + Async notification posting (deadlock prevention): + + NSAccessibilityPostNotification may synchronously invoke VoiceOver + callbacks from a private AX server thread. Those callbacks call + AX getters which dispatch_sync back to the main queue. If the + main thread is still inside the notification-posting method (e.g., + postAccessibilityUpdates called from ns_update_end), the + dispatch_sync deadlocks: the main thread waits for VoiceOver to + finish processing the notification, while VoiceOver's thread waits + for the main queue to become available. + + To break this cycle, all notification posting goes through two + static inline wrappers: + + ns_ax_post_notification(element, name) + ns_ax_post_notification_with_info(element, name, info) + + These wrappers defer the actual NSAccessibilityPostNotification + call via dispatch_async(dispatch_get_main_queue(), ^{ ... }). + The current method returns first, freeing the main queue, so + VoiceOver's dispatch_sync calls can proceed without deadlock. + Block captures retain ObjC objects (element, info dictionary) + for the lifetime of the deferred block. + Cached data written on main thread and read from any thread: - cachedText (NSString *): written by ensureTextCache on main. - visibleRuns (ns_ax_visible_run *): written by ensureTextCache. @@ -171,7 +195,11 @@ THREADING MODEL NOTIFICATION STRATEGY --------------------- - Notifications are posted from -postAccessibilityNotificationsForFrame: + All notifications are posted asynchronously via + ns_ax_post_notification / ns_ax_post_notification_with_info + (dispatch_async wrappers -- see THREADING MODEL for rationale). + + Notifications are generated by -postAccessibilityNotificationsForFrame: which runs on the main thread after every redisplay cycle. The method detects three mutually exclusive events: @@ -443,6 +471,18 @@ KEY DESIGN DECISIONS calls ns_update_end -> postAccessibilityUpdates. The BOOL flag breaks this recursion. + 6a. Async notification posting (dispatch_async wrappers). + NSAccessibilityPostNotification can synchronously trigger + VoiceOver queries from a background AX server thread. Those + queries dispatch_sync to the main queue. If the main thread + is still inside postAccessibilityUpdates (or windowDidBecomeKey, + or setAccessibilityFocused:), the dispatch_sync deadlocks. + All 14 notification sites use ns_ax_post_notification / _with_info + wrappers that defer posting via dispatch_async, freeing the main + queue before VoiceOver's callbacks arrive. This follows the same + pattern used by WebKit's AXObjectCacheMac (deferred posting via + performSelector:withObject:afterDelay:0). + 7. lispWindow (Lisp_Object) instead of raw struct window *. struct window pointers can become dangling after delete-window. Storing the Lisp_Object and using WINDOW_LIVE_P + XWINDOW at the @@ -554,10 +594,15 @@ TESTING CHECKLIST 22. Delete a window with C-x 0. No crash should occur. 23. Switch buffers with C-x b. VoiceOver should read new buffer. + Deadlock regression (async notifications): + 24. With VoiceOver on: M-x, type partial command, M-v to + *Completions*, Tab to a candidate, Enter to execute, then + C-x o to switch windows. Emacs must not hang. + Stress test: - 24. Open a large file (>5000 lines). Navigate with C-v / M-v. + 26. Open a large file (>5000 lines). Navigate with C-v / M-v. Verify no significant lag in VoiceOver speech response. - 25. Open an org-mode file with many folded sections. Verify that + 27. Open an org-mode file with many folded sections. Verify that folded (invisible) text is not announced during navigation. -- end of README --