From 30383d6462d6c9413359cd6d30fac6185b28ab0d Mon Sep 17 00:00:00 2001 From: Andras Laszlo Date: Fri, 16 Oct 2026 16:55:22 +0200 Subject: [PATCH] AppKit/X11: Robust multi-monitor mouse boundary confinement and clean stale Darling/Riot state --- patches/cocotron.patch | 87 +++++++++++++++++++++++++++------ tools/ensure-darling-runtime.py | 23 +++++++++ 2 files changed, 94 insertions(+), 16 deletions(-) diff --git a/patches/cocotron.patch b/patches/cocotron.patch index 9f8a153..6fd9936 100644 --- a/patches/cocotron.patch +++ b/patches/cocotron.patch @@ -2305,7 +2305,7 @@ index 18189404..5b3a34bc 100644 +- (NSRect) frameForRootRect: (NSRect) rect; @end diff --git a/AppKit/X11.backend/X11Display.m b/AppKit/X11.backend/X11Display.m -index 5ae29bf4..5105e7d8 100644 +index 5ae29bf4..5403cfa3 100644 --- a/AppKit/X11.backend/X11Display.m +++ b/AppKit/X11.backend/X11Display.m @@ -48,12 +48,271 @@ @@ -3355,7 +3355,38 @@ index 5ae29bf4..5105e7d8 100644 [self postEvent: event atStart: NO]; break; -@@ -1164,45 +1834,82 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1160,49 +1830,113 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { + // ev->xmotion.x, ev->xmotion.y, ev->xmotion.x_root, + // ev->xmotion.y_root); + ++ /* darling-lol: Coalesce pending MotionNotify events in the X11 queue to eliminate input lag. */ ++ XEvent nextEv; ++ while (XCheckTypedWindowEvent(_display, ev->xmotion.window, MotionNotify, &nextEv)) { ++ *ev = nextEv; ++ } ++ ++ if (_confinedWindow == [window windowHandle]) { ++ XWindowAttributes attrs; ++ if (XGetWindowAttributes(_display, _confinedWindow, &attrs)) { ++ int clampedX = ev->xmotion.x; ++ int clampedY = ev->xmotion.y; ++ BOOL needWarp = NO; ++ if (clampedX < 0) { clampedX = 0; needWarp = YES; } ++ if (clampedX >= attrs.width) { clampedX = attrs.width - 1; needWarp = YES; } ++ if (clampedY < 0) { clampedY = 0; needWarp = YES; } ++ if (clampedY >= attrs.height) { clampedY = attrs.height - 1; needWarp = YES; } ++ if (needWarp) { ++ XWarpPointer(_display, None, _confinedWindow, 0, 0, 0, 0, clampedX, clampedY); ++ XFlush(_display); ++ ev->xmotion.x = clampedX; ++ ev->xmotion.y = clampedY; ++ } ++ } ++ } else if (!_relativeMouse.active && window != nil) { ++ [self updateMouseConfinementForWindow: window]; ++ } ++ + CGPoint lastMotionPos = [window mouseLocationOutsideOfEventStream]; pos = [window transformPoint: NSMakePoint(ev->xmotion.x, ev->xmotion.y)]; @@ -3464,7 +3495,7 @@ index 5ae29bf4..5105e7d8 100644 mouseEventWithType: type location: pos modifierFlags: -@@ -1211,28 +1918,49 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1211,28 +1945,61 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { clickCount: 1 deltaX: deltaX deltaY: deltaY]; @@ -3499,6 +3530,18 @@ index 5ae29bf4..5105e7d8 100644 case LeaveNotify: NSLog(@"LeaveNotify"); ++ if (_confinedWindow == [window windowHandle]) { ++ Window win = [window windowHandle]; ++ XWindowAttributes attrs; ++ if (XGetWindowAttributes(_display, win, &attrs)) { ++ int cx = ev->xcrossing.x < 0 ? 0 : (ev->xcrossing.x >= attrs.width ? attrs.width - 1 : ev->xcrossing.x); ++ int cy = ev->xcrossing.y < 0 ? 0 : (ev->xcrossing.y >= attrs.height ? attrs.height - 1 : ev->xcrossing.y); ++ XWarpPointer(_display, None, win, 0, 0, 0, 0, cx, cy); ++ XFlush(_display); ++ } ++ [self updateMouseConfinementForWindow: window]; ++ break; ++ } + if (!_relativeMouse.active && ev->xcrossing.detail != NotifyInferior) { + [window setLastKnownCursorPosition: + [window transformPoint: NSMakePoint(ev->xcrossing.x, ev->xcrossing.y)]]; @@ -3516,7 +3559,7 @@ index 5ae29bf4..5105e7d8 100644 NSLog(@"FocusIn"); if ([delegate attachedSheet]) { [[delegate attachedSheet] makeKeyAndOrderFront: delegate]; -@@ -1243,28 +1971,55 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1243,28 +2010,67 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { checkForAppDeactivation: NO]; lastFocusedWindow = nil; } @@ -3544,6 +3587,18 @@ index 5ae29bf4..5105e7d8 100644 + rclolFocusDetailName(ev->xfocus.detail), _relativeRequested, _relativeMouse.active, + (unsigned long) _confinedWindow, + [NSApp isActive]); ++ if (ev->xfocus.detail == NotifyPointer || ev->xfocus.mode == NotifyGrab || ev->xfocus.mode == NotifyUngrab) { ++ /* Pointer boundary crossing or grab transition in XWayland: do NOT release confinement or deactivate window */ ++ break; ++ } ++ { ++ NSString *procName = [[NSProcessInfo processInfo] processName]; ++ BOOL isGame = (procName && [procName rangeOfString:@"League" options:NSCaseInsensitiveSearch].location != NSNotFound); ++ if (isGame && [NSApp isActive]) { ++ /* While League is the active app, do not drop confinement on transient XWayland focus events */ ++ break; ++ } ++ } NSLog(@"FocusOut"); + [self reconcileModifierKeys:NULL window:delegate]; + _lastKeySym = 0; @@ -3574,7 +3629,7 @@ index 5ae29bf4..5105e7d8 100644 O2Rect rect = NSMakeRect(ev->xexpose.x, ev->xexpose.y, ev->xexpose.width, ev->xexpose.height); -@@ -1301,11 +2056,23 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1301,11 +2107,23 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { break; case UnmapNotify: @@ -3602,7 +3657,7 @@ index 5ae29bf4..5105e7d8 100644 break; case MapRequest: -@@ -1321,6 +2088,7 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1321,6 +2139,7 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { [delegate platformWindow: window frameChanged: [window frame] didSize: YES]; @@ -3610,7 +3665,7 @@ index 5ae29bf4..5105e7d8 100644 break; case ConfigureRequest: -@@ -1344,6 +2112,8 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1344,6 +2163,8 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { break; case PropertyNotify: @@ -3619,7 +3674,7 @@ index 5ae29bf4..5105e7d8 100644 if ([window respondsToSelector: @selector(propertyNotify:)]) { [window propertyNotify: &ev->xproperty]; } -@@ -1399,7 +2169,9 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1399,7 +2220,9 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { break; default: @@ -3630,7 +3685,7 @@ index 5ae29bf4..5105e7d8 100644 [self _invalidateRRCache]; break; } -@@ -1433,6 +2205,10 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1433,6 +2256,10 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { NSLog(@"Request code: %d:%d", errorEvent->request_code, errorEvent->minor_code); NSLog(@"Error code: %d", errorEvent->error_code); @@ -3641,7 +3696,7 @@ index 5ae29bf4..5105e7d8 100644 return 0; } -@@ -1476,54 +2252,271 @@ void CGNativeBorderFrameWidthsForStyle(NSUInteger styleMask, CGFloat *top, +@@ -1476,54 +2303,271 @@ void CGNativeBorderFrameWidthsForStyle(NSUInteger styleMask, CGFloat *top, return frame; } @@ -3687,8 +3742,8 @@ index 5ae29bf4..5105e7d8 100644 - position.x, position.y); - XSync(_display, False); + [self warpPointerToRootX: x y: y]; -+} -+ + } + +/* Confinement preserves absolute motion and edge panning. Relative mode holds + * its own grab and is left alone while requested. */ +- (void) updateMouseConfinementForWindow:(X11Window *)window { @@ -3803,7 +3858,7 @@ index 5ae29bf4..5105e7d8 100644 + } + if (getenv("RCLOL_INPUT_TRACE")) fprintf(stderr,"CONFINEMENT focus=%lu target=%lu ancestor=%lu fullscreen=%d\n",focus,win,ancestor,fullscreen); + -+ BOOL isFocused = (ancestor == win || focus == win || (isGame && (focus == PointerRoot || focus == None || focus == DefaultRootWindow(_display)))); ++ BOOL isFocused = (ancestor == win || focus == win || (isGame && ([NSApp isActive] || focus == PointerRoot || focus == None || focus == DefaultRootWindow(_display)))); + if ((!fullscreen && !isGame) || attrs.map_state != IsViewable || !isFocused) { + if (rclolTraceOn()) + rclolTraceRated(&rclolConfineChannel, "no confinement window=%lu fullscreen=%d ewmh=%d titled=%d size=%dx%d map_state=%d %s", @@ -3826,9 +3881,9 @@ index 5ae29bf4..5105e7d8 100644 + rclolTraceRated(&rclolConfineChannel, "confine window=%lu fullscreen=%d ewmh=%d size=%dx%d XGrabPointer=%d(%s)", + (unsigned long) win, fullscreen, rclolEwmhFullscreen, attrs.width, attrs.height, status, + rclolGrabResultName(status)); -+ if (status == GrabSuccess) _confinedWindow=win; - } - ++ if (status == GrabSuccess || status == AlreadyGrabbed) _confinedWindow=win; ++} ++ +/* Takes the relative-mode pointer grab on the key (else main) window, starting with the virtual cursor at the + * current pointer position. Returns the XGrabPointer result, or -1 without a window. */ +- (int) beginRelativeMouseWithWindow: (NSWindow **) outWindow pick: (const char **) outPick { diff --git a/tools/ensure-darling-runtime.py b/tools/ensure-darling-runtime.py index a7a6339..a34ff7f 100644 --- a/tools/ensure-darling-runtime.py +++ b/tools/ensure-darling-runtime.py @@ -22,3 +22,26 @@ except (OSError, StopIteration): if dead: os.replace(marker, prefix / f'.init.pid.stale-{pid}-{time.time_ns()}') print(f'Recovered stale Darling server marker for PID {pid}', flush=True) + +# Clean up stale Riot temporary files and shared memory frames from previous crashes +try: + tmp_dir = prefix / 'private/tmp' + if tmp_dir.is_dir(): + for f in tmp_dir.glob('.com.riotgames.*'): + try: + f.unlink(missing_ok=True) + except OSError: + pass +except OSError: + pass + +try: + shm_dir = Path('/dev/shm') + if shm_dir.is_dir(): + for f in shm_dir.glob('darling-*'): + try: + f.unlink(missing_ok=True) + except OSError: + pass +except OSError: + pass