diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index c04aa46..75b1c4d 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -230,6 +230,10 @@ These are host settings, not game settings. Run on AC power, and use a performan The X11 backend previously called `XSync` after every cursor-shape update, waiting for an X server reply on the input thread. It now uses `XFlush` to send the change immediately without waiting. This removes an avoidable round trip during cursor updates. A local manual retest on 2026-10-16 found that the mouse problem no longer occurred with VSync enabled. Input-to-display latency has not been measured, and confirmation from the original reporter is still pending. Rebuild and install with `./scripts/update.sh` after exiting the game and both clients. +A later input trace showed League setting the cursor about 190 times and unhiding it about 100 times per second, each time sending the same cursor to the X server. The backend now skips the update when the window already has that cursor. The effect on latency has not been measured. + +The backend's relative mouse mode (`CGAssociateMouseAndMouseCursorPosition(false)`) previously warped the pointer to the window center, treated the game's own cursor warps as motion and reported `deltaY` upside down. It now follows macOS: the cursor stays where it is, events carry the physical motion as deltas (positive `deltaY` downwards, also for ordinary motion), the game's warps move the cursor without a delta, and `+[NSEvent mouseLocation]` returns that cursor. On Xwayland a warp takes effect only while the cursor is hidden, so with a visible cursor the pointer is not held in place. `tools/test-x11-relative-mouse.c` checks this logic on the host (`cc tools/test-x11-relative-mouse.c && ./a.out` after `scripts/prepare.py`). League on the tested Hyprland setup never enters this mode, so it has not been confirmed in game. + For a manual comparison, keep the scene, FPS cap, display refresh and compositor settings the same. Record whether the delay affects the cursor itself, camera movement, or actions after clicking, and compare the in-game VSync setting on and off. Include the desktop/compositor, X11 or Xwayland session, GPU/driver, monitor refresh and fullscreen/windowed mode in the report. Do not automate game input. The fullscreen cursor-shape problem below is a separate known symptom. ## Cursor stops changing shape in fullscreen diff --git a/patches/cocotron.patch b/patches/cocotron.patch index 5ef6273..d946286 100644 --- a/patches/cocotron.patch +++ b/patches/cocotron.patch @@ -515,10 +515,10 @@ index 7361d09a..4bdd88cf 100644 return [self CGColor]; } diff --git a/AppKit/NSCursor.m b/AppKit/NSCursor.m -index 4cd74633..9be5239a 100644 +index 4cd74633..4098fd28 100644 --- a/AppKit/NSCursor.m +++ b/AppKit/NSCursor.m -@@ -365,12 +365,9 @@ static NSMutableArray *_cursorStack = nil; +@@ -365,10 +365,7 @@ static NSMutableArray *_cursorStack = nil; } + (void) setHiddenUntilMouseMoves: (BOOL) flag { @@ -530,10 +530,8 @@ index 4cd74633..9be5239a 100644 } - initWithImage: (NSImage *) image - foregroundColorHint: (NSColor *) foregroundHint - backgroundColorHint: (NSColor *) backgroundHint diff --git a/AppKit/NSDisplay.m b/AppKit/NSDisplay.m -index cacd8999..13faf0f7 100644 +index cacd8999..9422d869 100644 --- a/AppKit/NSDisplay.m +++ b/AppKit/NSDisplay.m @@ -43,6 +43,10 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ @@ -547,8 +545,7 @@ index cacd8999..13faf0f7 100644 - (instancetype) init { if ([self class] != [NSDisplay class]) { // Initializing a concrete subclass. -@@ -135,6 +139,26 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ - - (void) unhideCursor { +@@ -136,6 +140,26 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ NSInvalidAbstractInvocation(); } @@ -574,6 +571,7 @@ index cacd8999..13faf0f7 100644 + // Arrow, IBeam, HorizontalResize, VerticalResize - (id) cursorWithName: (NSString *) name { + NSInvalidAbstractInvocation(); @@ -146,28 +170,46 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ NSInvalidAbstractInvocation(); } @@ -628,7 +626,7 @@ index cacd8999..13faf0f7 100644 } } -@@ -193,7 +215,8 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +@@ -193,7 +235,8 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ } while (--count >= 0) { @@ -638,7 +636,7 @@ index cacd8999..13faf0f7 100644 [_eventQueue removeObjectAtIndex: count]; } } -@@ -310,6 +333,10 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +@@ -310,6 +353,10 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ return 0; } @@ -835,6 +833,37 @@ index 2251b642..8b29725d 100644 - (NSRect) boundingRectForFont { return CTFontGetBoundingBox(_ctFont); } +diff --git a/AppKit/NSGeometryTrace.h b/AppKit/NSGeometryTrace.h +new file mode 100644 +index 00000000..a51a9241 +--- /dev/null ++++ b/AppKit/NSGeometryTrace.h +@@ -0,0 +1,25 @@ ++/* Opt-in runtime geometry diagnostics; never log view text or client data. */ ++#ifndef DARLING_NS_GEOMETRY_TRACE_H ++#define DARLING_NS_GEOMETRY_TRACE_H ++#include ++#include ++#include ++ ++static inline void DarlingGeometryTrace(const char *format, ...) { ++ if (!getenv("DARLING_UI_GEOMETRY_TRACE")) ++ return; ++ va_list args, copy; ++ va_start(args, format); ++ va_copy(copy, args); ++ vfprintf(stderr, format, args); ++ va_end(args); ++ /* Clients may redirect stderr to a non-log descriptor. Keep a copy in ++ * the guest's temporary directory so their resize events remain visible. */ ++ FILE *file = fopen("/tmp/darling-ui-geometry.log", "a"); ++ if (file) { ++ vfprintf(file, format, copy); ++ fclose(file); ++ } ++ va_end(copy); ++} ++#endif diff --git a/AppKit/NSImage.m b/AppKit/NSImage.m index d2ac8e05..a4164f0a 100644 --- a/AppKit/NSImage.m @@ -2039,6 +2068,112 @@ index d545120a..b2845478 100644 } + (BOOL) allowsAutomaticWindowTabbing { +diff --git a/AppKit/NSWorkspace.m b/AppKit/NSWorkspace.m +index ad5e4f9d..dbd997aa 100644 +--- a/AppKit/NSWorkspace.m ++++ b/AppKit/NSWorkspace.m +@@ -20,9 +20,58 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + #import + #import + #import ++#include + + NSString *const NSWorkspaceApplicationKey = @"NSWorkspaceApplicationKey"; + ++/* Electron's shell.openExternal() uses -openURL:configuration:completionHandler: (macOS 10.15) ++ * with a configured NSWorkspaceOpenConfiguration, so the configuration needs real properties. ++ * The URL itself is opened by -openURL: (LaunchServices in MacWorkspace). */ ++@interface NSWorkspaceOpenConfiguration () { ++ BOOL _activates, _hides, _hidesOthers, _addsToRecentItems, _createsNewApplicationInstance; ++ BOOL _allowsRunningApplicationSubstitution, _forPrinting, _promptsUserIfNeeded, _requiresUniversalLinks; ++ NSAppleEventDescriptor *_appleEvent; ++ NSArray *_arguments; ++ NSDictionary *_environment; ++ cpu_type_t _architecture; ++} ++@end ++ ++@implementation NSWorkspaceOpenConfiguration ++@synthesize activates = _activates, hides = _hides, hidesOthers = _hidesOthers; ++@synthesize addsToRecentItems = _addsToRecentItems, createsNewApplicationInstance = _createsNewApplicationInstance; ++@synthesize allowsRunningApplicationSubstitution = _allowsRunningApplicationSubstitution; ++@synthesize forPrinting = _forPrinting, promptsUserIfNeeded = _promptsUserIfNeeded; ++@synthesize requiresUniversalLinks = _requiresUniversalLinks; ++@synthesize appleEvent = _appleEvent, arguments = _arguments, environment = _environment; ++@synthesize architecture = _architecture; +++ (instancetype) configuration { ++ NSWorkspaceOpenConfiguration *c = [[[self alloc] init] autorelease]; ++ c->_activates = YES; ++ c->_addsToRecentItems = YES; ++ c->_allowsRunningApplicationSubstitution = YES; ++ return c; ++} ++- (id) copyWithZone: (NSZone *) zone { ++ NSWorkspaceOpenConfiguration *c = [[[self class] allocWithZone: zone] init]; ++ c->_activates = _activates; c->_hides = _hides; c->_hidesOthers = _hidesOthers; ++ c->_addsToRecentItems = _addsToRecentItems; ++ c->_createsNewApplicationInstance = _createsNewApplicationInstance; ++ c->_allowsRunningApplicationSubstitution = _allowsRunningApplicationSubstitution; ++ c->_forPrinting = _forPrinting; c->_promptsUserIfNeeded = _promptsUserIfNeeded; ++ c->_requiresUniversalLinks = _requiresUniversalLinks; c->_architecture = _architecture; ++ c->_appleEvent = [_appleEvent retain]; ++ c->_arguments = [_arguments copy]; c->_environment = [_environment copy]; ++ return c; ++} ++- (void) dealloc { ++ [_appleEvent release]; ++ [_arguments release]; ++ [_environment release]; ++ [super dealloc]; ++} ++@end ++ + NSString *const NSWorkspaceWillPowerOffNotification = + @"NSWorkspaceWillPowerOffNotification"; + +@@ -161,6 +210,24 @@ NSNotificationName NSWorkspaceWillUnmountNotification = @"NSWorkspaceWillUnmount + return 0; + } + ++- (void) openURL: (NSURL *) url ++ configuration: (NSWorkspaceOpenConfiguration *) configuration ++ completionHandler: (void (^)(id application, NSError *error)) completionHandler ++{ ++ // Opening may wait on another process; keep it off the caller's thread. ++ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ ++ BOOL opened = [self openURL: url]; ++ if (completionHandler != NULL) { ++ NSError *error = nil; ++ if (!opened) ++ error = [NSError errorWithDomain: NSCocoaErrorDomain code: 256 userInfo: nil]; ++ dispatch_async(dispatch_get_main_queue(), ^{ ++ completionHandler(nil, error); ++ }); ++ } ++ }); ++} ++ + - (BOOL) selectFile: (NSString *) path + inFileViewerRootedAtPath: (NSString *) rootedAtPath + { +@@ -317,16 +384,3 @@ NSNotificationName NSWorkspaceWillUnmountNotification = @"NSWorkspaceWillUnmount + } + + @end +- +-static dispatch_once_t _initOnceNsWorkspaceOpenConfig; +-static NSWorkspaceOpenConfiguration* _singletonNsWorkspaceOpenConfig; +- +-@implementation NSWorkspaceOpenConfiguration +-+ (instancetype)configuration { +- dispatch_once(&_initOnceNsWorkspaceOpenConfig, ^{ +- _singletonNsWorkspaceOpenConfig = [[NSWorkspaceOpenConfiguration alloc] init]; +- }); +- return _singletonNsWorkspaceOpenConfig; +-} +- +-@end +\ No newline at end of file diff --git a/AppKit/X11.backend/CarbonKeys.h b/AppKit/X11.backend/CarbonKeys.h index e999cbf2..14f9cb6a 100644 --- a/AppKit/X11.backend/CarbonKeys.h @@ -2107,10 +2242,17 @@ index bdc97bdf..139c91eb 100644 _cursor = XcursorLibraryLoadCursor(display, "left_ptr"); return self; diff --git a/AppKit/X11.backend/X11Display.h b/AppKit/X11.backend/X11Display.h -index 18189404..dd6215c9 100644 +index 18189404..0eff2187 100644 --- a/AppKit/X11.backend/X11Display.h +++ b/AppKit/X11.backend/X11Display.h -@@ -16,7 +16,7 @@ +@@ -10,13 +10,14 @@ + #import + #import + #import ++#import "X11RelativeMouse.h" + + #ifdef DARLING + #import #import #endif @@ -2119,10 +2261,21 @@ index 18189404..dd6215c9 100644 @interface X11Display : NSDisplay { Display *_display; -@@ -37,7 +37,10 @@ +@@ -36,8 +37,20 @@ + NSTimeInterval lastClickTimeStamp; int clickCount; X11Cursor *_blankCursor, *_defaultCursor; - BOOL _cursorGrabbed; +- BOOL _cursorGrabbed; ++ /* Relative mouse mode: requested by CGAssociateMouseAndMouseCursorPosition(false); _relativeMouse.active ++ * while its pointer grab on _relativeWindow is held (released on focus loss, retaken on focus). */ ++ BOOL _relativeRequested; ++ Window _relativeWindow; ++ X11RelativeMouse _relativeMouse; ++ BOOL _isXwayland; ++ /* The cursor last defined by setCursor:, retained so its X id cannot be freed and reused while cached. */ ++ Window _definedCursorWindow; ++ X11Cursor *_definedCursor; ++ BOOL _cursorHidden; + Window _confinedWindow; + unsigned int _otherButtonsDown; KeySym _lastKeySym; @@ -2130,7 +2283,7 @@ index 18189404..dd6215c9 100644 int _rrEventBase; NSArray* _lastScreens; -@@ -45,7 +48,9 @@ +@@ -45,7 +58,9 @@ XIM _xim; } @@ -2141,10 +2294,10 @@ index 18189404..dd6215c9 100644 - (void) setWindow: (id) window forID: (XID) i; diff --git a/AppKit/X11.backend/X11Display.m b/AppKit/X11.backend/X11Display.m -index 5ae29bf4..358e606b 100644 +index 5ae29bf4..bc4ceaf3 100644 --- a/AppKit/X11.backend/X11Display.m +++ b/AppKit/X11.backend/X11Display.m -@@ -48,15 +48,17 @@ +@@ -48,12 +48,271 @@ #import "X11KeySymToUCS.h" #import #import @@ -2156,13 +2309,267 @@ index 5ae29bf4..358e606b 100644 #import #import +#include ++ ++/* darling-lol: passive, opt-in input trace for diagnosing relative mouse mode (e.g. League on Xwayland). ++ * Enabled only when the file /tmp/rclol-input-trace exists (checked once per process); lines are appended to ++ * /tmp/rclol-input-trace.log with the pid and a monotonic millisecond timestamp. It records pointer grabs, ++ * warps, cursor visibility, motion handling and pointer-position queries, and never changes behaviour. When ++ * disabled the cost is one load and branch; when enabled no X requests are added except the one-time startup ++ * extension queries. High-rate sources log a few lines verbatim and are then summarised once per second. ++ * Never logs key codes, text or window titles. Independent of the older RCLOL_INPUT_TRACE stderr logging. */ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++static int rclolTraceState = -1; ++static FILE *rclolTraceFile; ++static pthread_once_t rclolTraceOnce = PTHREAD_ONCE_INIT; ++static pthread_mutex_t rclolTraceLock = PTHREAD_MUTEX_INITIALIZER; ++static unsigned rclolGrabGeneration; ++ ++static void rclolTraceInit(void) { ++ int on = 0; ++ if (access("/tmp/rclol-input-trace", F_OK) == 0) { ++ rclolTraceFile = fopen("/tmp/rclol-input-trace.log", "a"); ++ if (rclolTraceFile) { ++ setvbuf(rclolTraceFile, NULL, _IOLBF, 0); ++ on = 1; ++ } ++ } ++ __atomic_store_n(&rclolTraceState, on, __ATOMIC_RELEASE); ++} ++ ++static inline BOOL rclolTraceOn(void) { ++ int state = __atomic_load_n(&rclolTraceState, __ATOMIC_ACQUIRE); ++ if (__builtin_expect(state >= 0, 1)) ++ return state; ++ pthread_once(&rclolTraceOnce, rclolTraceInit); ++ return __atomic_load_n(&rclolTraceState, __ATOMIC_ACQUIRE); ++} ++ ++static unsigned long long rclolTraceNowMs(void) { ++ static mach_timebase_info_data_t tb; ++ if (tb.denom == 0) mach_timebase_info(&tb); ++ return (unsigned long long) ((__uint128_t) mach_absolute_time() * tb.numer / tb.denom / 1000000u); ++} ++ ++static void rclolTraceLine(const char *format, ...) __attribute__((format(printf, 1, 2))); ++static void rclolTraceLine(const char *format, ...) { ++ char message[640]; ++ va_list args; ++ va_start(args, format); ++ vsnprintf(message, sizeof message, format, args); ++ va_end(args); ++ fprintf(rclolTraceFile, "pid=%d t=%llums %s\n", (int) getpid(), rclolTraceNowMs(), message); ++} ++ ++/* A rate-limited source: the first `limit` calls are logged verbatim, later ones are counted and summarised ++ * (count and last message) once at least a second has passed. */ ++typedef struct { ++ const char *name; ++ unsigned limit, verbatim, pending; ++ unsigned long long windowStart; ++ char last[320]; ++} RclolTraceChannel; ++#define RCLOL_CHANNEL(n, l) { n, l, 0, 0, 0, "" } ++static RclolTraceChannel rclolWarpAppChannel = RCLOL_CHANNEL("warp[app]", 20); ++static RclolTraceChannel rclolWarpRewarpChannel = RCLOL_CHANNEL("warp[motion-rewarp]", 20); ++static RclolTraceChannel rclolHideChannel = RCLOL_CHANNEL("hideCursor", 20); ++static RclolTraceChannel rclolUnhideChannel = RCLOL_CHANNEL("unhideCursor", 20); ++static RclolTraceChannel rclolSetCursorChannel = RCLOL_CHANNEL("setCursor", 20); ++static RclolTraceChannel rclolFocusChannel = RCLOL_CHANNEL("focus", 60); ++static RclolTraceChannel rclolConfineChannel = RCLOL_CHANNEL("confinement", 60); ++static RclolTraceChannel rclolErrorChannel = RCLOL_CHANNEL("xerror", 40); ++static RclolTraceChannel *rclolChannels[] = { ++ &rclolWarpAppChannel, &rclolWarpRewarpChannel, &rclolHideChannel, ++ &rclolUnhideChannel, &rclolSetCursorChannel, &rclolFocusChannel, &rclolConfineChannel, &rclolErrorChannel, ++}; ++ ++static struct { ++ unsigned verbatim, events, echoes, rewarps, relative, absolute; ++ unsigned long long windowStart; ++ unsigned long window; ++ int x, y, rootX, rootY, grabbed; ++ double dx, dy, lastX, lastY; ++} rclolMotion; ++ ++static struct { ++ unsigned verbatim, calls, hits, queries, failures; ++ unsigned long long windowStart; ++ double x, y; ++ int rootX, rootY; ++} rclolLocation; ++ ++static void rclolTraceFlushChannelLocked(RclolTraceChannel *channel, unsigned long long now) { ++ if (channel->pending) ++ rclolTraceLine("%s summary: %u calls in %llums, last: %s", channel->name, channel->pending, ++ now - channel->windowStart, channel->last); ++ channel->pending = 0; ++ channel->windowStart = now; ++} ++ ++static void rclolTraceTickLocked(unsigned long long now, BOOL force) { ++ for (size_t i = 0; i < sizeof rclolChannels / sizeof *rclolChannels; ++i) ++ if (rclolChannels[i]->pending && (force || now - rclolChannels[i]->windowStart >= 1000)) ++ rclolTraceFlushChannelLocked(rclolChannels[i], now); ++ if (rclolMotion.events && (force || now - rclolMotion.windowStart >= 1000)) { ++ rclolTraceLine("motion summary: %u events (%u warp-echo, %u relative+rewarp, %u relative, %u absolute) in " ++ "%llums window=%lu relative=%d last ev=%d,%d root=%d,%d delta=%.1f,%.1f last=%.1f,%.1f", ++ rclolMotion.events, rclolMotion.echoes, rclolMotion.rewarps, rclolMotion.relative, ++ rclolMotion.absolute, ++ now - rclolMotion.windowStart, rclolMotion.window, rclolMotion.grabbed, rclolMotion.x, ++ rclolMotion.y, rclolMotion.rootX, rclolMotion.rootY, rclolMotion.dx, rclolMotion.dy, ++ rclolMotion.lastX, rclolMotion.lastY); ++ rclolMotion.events = rclolMotion.echoes = rclolMotion.rewarps = rclolMotion.relative = 0; ++ rclolMotion.absolute = 0; ++ } ++ if (rclolLocation.calls && (force || now - rclolLocation.windowStart >= 1000)) { ++ rclolTraceLine("mouseLocation summary: %u calls (%u cache hits, %u queries, %u failed) in %llums " ++ "last=%.1f,%.1f root=%d,%d", ++ rclolLocation.calls, rclolLocation.hits, rclolLocation.queries, rclolLocation.failures, ++ now - rclolLocation.windowStart, rclolLocation.x, rclolLocation.y, rclolLocation.rootX, ++ rclolLocation.rootY); ++ rclolLocation.calls = rclolLocation.hits = rclolLocation.queries = rclolLocation.failures = 0; ++ } ++} ++ ++static void rclolTraceRated(RclolTraceChannel *channel, const char *format, ...) __attribute__((format(printf, 2, 3))); ++static void rclolTraceRated(RclolTraceChannel *channel, const char *format, ...) { ++ char message[320]; ++ va_list args; ++ va_start(args, format); ++ vsnprintf(message, sizeof message, format, args); ++ va_end(args); ++ pthread_mutex_lock(&rclolTraceLock); ++ unsigned long long now = rclolTraceNowMs(); ++ if (channel->verbatim < channel->limit) { ++ channel->verbatim++; ++ rclolTraceLine("%s #%u gen=%u: %s", channel->name, channel->verbatim, rclolGrabGeneration, message); ++ } else { ++ if (!channel->pending) channel->windowStart = now; ++ channel->pending++; ++ memcpy(channel->last, message, sizeof message); ++ } ++ rclolTraceTickLocked(now, NO); ++ pthread_mutex_unlock(&rclolTraceLock); ++} ++ ++/* Each grabMouse: call starts a new generation: flush pending summaries and re-arm the verbatim budgets for ++ * warps, motion and mouseLocation so the loop's first steps after every grab change are visible. */ ++static void rclolTraceGrabChanged(void) { ++ pthread_mutex_lock(&rclolTraceLock); ++ rclolTraceTickLocked(rclolTraceNowMs(), YES); ++ rclolGrabGeneration++; ++ rclolWarpAppChannel.verbatim = rclolWarpRewarpChannel.verbatim = 0; ++ rclolMotion.verbatim = rclolLocation.verbatim = 0; ++ pthread_mutex_unlock(&rclolTraceLock); ++} ++ ++/* pos/last are the window-local event position and previous position for absolute motion, and the virtual cursor ++ * and previous physical pointer (root coordinates) for relative motion. */ ++static void rclolTraceMotion(XEvent *ev, BOOL grabbed, CGPoint pos, CGPoint last, CGFloat dx, CGFloat dy, ++ const char *decision, int forwarded) ++{ ++ pthread_mutex_lock(&rclolTraceLock); ++ unsigned long long now = rclolTraceNowMs(); ++ if (rclolMotion.verbatim < 30) { ++ rclolMotion.verbatim++; ++ rclolTraceLine("motion #%u gen=%u: window=%lu ev=%d,%d root=%d,%d pos=%.1f,%.1f last=%.1f,%.1f " ++ "delta=%.1f,%.1f relative=%d decision=%s forwarded=%d send_event=%d time=%lu state=0x%x", ++ rclolMotion.verbatim, rclolGrabGeneration, (unsigned long) ev->xany.window, ev->xmotion.x, ++ ev->xmotion.y, ev->xmotion.x_root, ev->xmotion.y_root, pos.x, pos.y, last.x, last.y, dx, dy, ++ grabbed, decision, forwarded, ev->xany.send_event, (unsigned long) ev->xmotion.time, ++ ev->xmotion.state); ++ } ++ if (!rclolMotion.events) rclolMotion.windowStart = now; ++ rclolMotion.events++; ++ if (!strcmp(decision, "echo")) rclolMotion.echoes++; ++ else if (!strcmp(decision, "relative+rewarp")) rclolMotion.rewarps++; ++ else if (!strncmp(decision, "relative", 8)) rclolMotion.relative++; ++ else rclolMotion.absolute++; ++ rclolMotion.window = ev->xany.window; ++ rclolMotion.x = ev->xmotion.x; ++ rclolMotion.y = ev->xmotion.y; ++ rclolMotion.rootX = ev->xmotion.x_root; ++ rclolMotion.rootY = ev->xmotion.y_root; ++ rclolMotion.grabbed = grabbed; ++ rclolMotion.dx = dx; ++ rclolMotion.dy = dy; ++ rclolMotion.lastX = last.x; ++ rclolMotion.lastY = last.y; ++ rclolTraceTickLocked(now, NO); ++ pthread_mutex_unlock(&rclolTraceLock); ++} ++ ++static void rclolTraceMouseLocation(NSPoint result, BOOL ok, BOOL hit, int rootX, int rootY, BOOL grabbed) { ++ pthread_mutex_lock(&rclolTraceLock); ++ unsigned long long now = rclolTraceNowMs(); ++ const char *source = grabbed ? "virtual" : !ok ? "query-failed" : hit ? "cache-hit" : "XQueryPointer"; ++ if (rclolLocation.verbatim < 30) { ++ rclolLocation.verbatim++; ++ rclolTraceLine("mouseLocation #%u gen=%u: -> %.1f,%.1f root=%d,%d source=%s relative=%d", ++ rclolLocation.verbatim, rclolGrabGeneration, result.x, result.y, rootX, rootY, source, ++ grabbed); ++ } ++ if (!rclolLocation.calls) rclolLocation.windowStart = now; ++ rclolLocation.calls++; ++ if (!ok) rclolLocation.failures++; ++ else if (hit) rclolLocation.hits++; ++ else rclolLocation.queries++; ++ rclolLocation.x = result.x; ++ rclolLocation.y = result.y; ++ rclolLocation.rootX = rootX; ++ rclolLocation.rootY = rootY; ++ rclolTraceTickLocked(now, NO); ++ pthread_mutex_unlock(&rclolTraceLock); ++} ++ ++static void rclolTraceStartup(Display *display) { ++ if (!rclolTraceOn()) return; ++ int opcode, event, error; ++ Bool xwayland = XQueryExtension(display, "XWAYLAND", &opcode, &event, &error); ++ int xiOpcode = 0, xiEvent = 0, xiError = 0; ++ Bool xi = XQueryExtension(display, "XInputExtension", &xiOpcode, &xiEvent, &xiError); ++ int screen = DefaultScreen(display); ++ rclolTraceLine("startup: input trace enabled; vendor=\"%s\" release=%d protocol=%d.%d xwayland=%d " ++ "XInputExtension=%d opcode=%d (libXi not linked; version not queried) screens=%d default=%d " ++ "size=%dx%d (%dx%dmm) RCLOL_INPUT_TRACE=%d", ++ ServerVendor(display), VendorRelease(display), ProtocolVersion(display), ++ ProtocolRevision(display), xwayland, xi, xiOpcode, ScreenCount(display), screen, ++ DisplayWidth(display, screen), DisplayHeight(display, screen), ++ DisplayWidthMM(display, screen), DisplayHeightMM(display, screen), ++ getenv("RCLOL_INPUT_TRACE") != NULL); ++} ++ ++static const char *rclolGrabResultName(int result) { ++ switch (result) { ++ case GrabSuccess: return "GrabSuccess"; ++ case AlreadyGrabbed: return "AlreadyGrabbed"; ++ case GrabInvalidTime: return "GrabInvalidTime"; ++ case GrabNotViewable: return "GrabNotViewable"; ++ case GrabFrozen: return "GrabFrozen"; ++ default: return "not-called"; ++ } ++} ++ ++static const char *rclolFocusModeName(int mode) { ++ static const char *names[] = {"Normal", "Grab", "Ungrab", "WhileGrabbed"}; ++ return mode >= 0 && mode < 4 ? names[mode] : "?"; ++} ++ ++static const char *rclolFocusDetailName(int detail) { ++ static const char *names[] = {"Ancestor", "Virtual", "Inferior", "Nonlinear", "NonlinearVirtual", ++ "Pointer", "PointerRoot", "DetailNone"}; ++ return detail >= 0 && detail < 8 ? names[detail] : "?"; ++} void NSColorSetCatalogColor(NSColorListName catalogName, NSColorName colorName, NSColor *color); - @implementation X11Display - - static int errorHandler(Display *display, XErrorEvent *errorEvent) { -@@ -75,6 +81,15 @@ static void socketCallback(CFSocketRef s, CFSocketCallBackType type, +@@ -75,6 +334,15 @@ static void socketCallback(CFSocketRef s, CFSocketCallBackType type, - init { if (self = [super init]) { @@ -2178,9 +2585,22 @@ index 5ae29bf4..358e606b 100644 _display = XOpenDisplay(NULL); if (_display == NULL) { -@@ -148,14 +163,16 @@ static void socketCallback(CFSocketRef s, CFSocketCallBackType type, +@@ -136,6 +404,10 @@ static void socketCallback(CFSocketRef s, CFSocketCallBackType type, + if (XRRQueryExtension(_display, &_rrEventBase, &errorBase)) { + XRRSelectInput(_display, DefaultRootWindow(_display), RRScreenChangeNotifyMask | RRCrtcChangeNotifyMask | RROutputChangeNotifyMask | RROutputPropertyNotifyMask); + } ++ /* Xwayland forwards pointer warps to the compositor only while the X cursor is hidden. */ ++ int xwaylandOpcode, xwaylandEvent, xwaylandError; ++ _isXwayland = XQueryExtension(_display, "XWAYLAND", &xwaylandOpcode, &xwaylandEvent, &xwaylandError); ++ rclolTraceStartup(_display); + + lastFocusedWindow = nil; + lastClickTimeStamp = 0.0; +@@ -147,15 +419,18 @@ static void socketCallback(CFSocketRef s, CFSocketCallBackType type, + - (void) dealloc { [_blankCursor release]; [_defaultCursor release]; ++ [_definedCursor release]; - XCloseIM(_xim); + if (_xim) @@ -2198,7 +2618,7 @@ index 5ae29bf4..358e606b 100644 if (_cfSocket != NULL) CFRelease(_cfSocket); #endif -@@ -172,6 +189,56 @@ static void socketCallback(CFSocketRef s, CFSocketCallBackType type, +@@ -172,6 +447,56 @@ static void socketCallback(CFSocketRef s, CFSocketCallBackType type, return _display; } @@ -2255,7 +2675,7 @@ index 5ae29bf4..358e606b 100644 - (void) _enableDetectableAutoRepeat { int major = XkbMajorVersion, minor = XkbMinorVersion; XkbStateRec state; -@@ -536,12 +603,53 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -536,12 +861,53 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { return &layout->layout; } @@ -2311,7 +2731,7 @@ index 5ae29bf4..358e606b 100644 } - (NSPasteboard *) pasteboardWithName: (NSString *) name { -@@ -675,6 +782,15 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -675,6 +1041,15 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { blue: 0.60f alpha: 1.0f]; @@ -2327,16 +2747,63 @@ index 5ae29bf4..358e606b 100644 NSLog(@"missing color for %@", colorName); return [NSColor redColor]; } -@@ -754,4 +870,7 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -688,6 +1063,10 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { + } + + - (void) hideCursor { ++ if (rclolTraceOn()) ++ rclolTraceRated(&rclolHideChannel, "blankExisted=%d relative=%d/%d hidden=%d confined=%lu", ++ _blankCursor != nil, _relativeRequested, _relativeMouse.active, _cursorHidden, ++ (unsigned long) _confinedWindow); + if (!_blankCursor) + _blankCursor = [[X11Cursor alloc] initBlank]; + [self setCursor: _blankCursor]; +@@ -695,6 +1074,10 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { + + - (void) unhideCursor { + NSCursor *current = [NSCursor currentCursor]; ++ if (rclolTraceOn()) ++ rclolTraceRated(&rclolUnhideChannel, "path=%s current=%p relative=%d/%d hidden=%d", ++ current ? "push-pop" : "default", current, _relativeRequested, _relativeMouse.active, ++ _cursorHidden); + if (current != nil) { + [current push]; + [current pop]; +@@ -747,12 +1130,28 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { + + - (void) setCursor: (id) cursor { + CGWindow *window = [[NSApp keyWindow] platformWindow]; +- if (window != nil) { +- Window w = [(X11Window *) window windowHandle]; +- Cursor c = ((X11Cursor *) cursor).cursor; +- ++ Window w = window ? [(X11Window *) window windowHandle] : None; ++ Cursor c = ((X11Cursor *) cursor).cursor; ++ /* unhideCursor re-sets the current cursor and games call it every frame: only send actual changes. The ++ * defined cursor stays retained, so its X id cannot be freed and handed to a different cursor meanwhile. */ ++ BOOL unchanged = w != None && w == _definedCursorWindow && c == _definedCursor.cursor; ++ if (rclolTraceOn()) ++ rclolTraceRated(&rclolSetCursorChannel, "cursor=%p blank=%d default=%d xcursor=%lu keyWindow=%d xwin=%lu applied=%d relative=%d/%d", ++ cursor, cursor != nil && cursor == _blankCursor, cursor != nil && cursor == _defaultCursor, ++ (unsigned long) c, [NSApp keyWindow] != nil, (unsigned long) w, w != None && !unchanged, ++ _relativeRequested, _relativeMouse.active); ++ if (w != None && !unchanged) { XDefineCursor(_display, w, c); - XSync(_display, False); + /* Send cursor changes immediately without waiting for an X server + * reply on the input thread. X requests on this connection remain + * ordered; no server result is consumed here. */ + XFlush(_display); ++ [cursor retain]; ++ [_definedCursor release]; ++ _definedCursor = cursor; ++ _definedCursorWindow = w; ++ /* initBlank's empty-mask cursor counts as hidden for Xwayland's pointer warp emulation. */ ++ _cursorHidden = cursor != nil && cursor == _blankCursor && c != None; } } -@@ -921,15 +1040,9 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { + +@@ -921,15 +1320,22 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { } - (NSPoint) mouseLocation { @@ -2347,14 +2814,44 @@ index 5ae29bf4..358e606b 100644 - - XQueryPointer(_display, root, &root, &child, &root_x, &root_y, &win_x, - &win_y, &mask); -+ rclolQueryPointer(_display); ++ if (_relativeMouse.active) { ++ /* The cursor is disassociated: report the frozen (virtual) position, as macOS does. */ ++ NSPoint result = NSMakePoint(_relativeMouse.virtualX, ++ DisplayHeight(_display, DefaultScreen(_display)) - _relativeMouse.virtualY); ++ if (rclolTraceOn()) ++ rclolTraceMouseLocation(result, YES, YES, (int) _relativeMouse.virtualX, (int) _relativeMouse.virtualY, YES); ++ return result; ++ } ++ uint64_t rclolStampBefore = rclolPointerStamp; ++ BOOL rclolQueried = rclolQueryPointer(_display); int height = DisplayHeight(_display, DefaultScreen(_display)); - return NSMakePoint(root_x, height - root_y); -+ return NSMakePoint(rclolRootX, height - rclolRootY); ++ NSPoint result = NSMakePoint(rclolRootX, height - rclolRootY); ++ if (rclolTraceOn()) ++ rclolTraceMouseLocation(result, rclolQueried, rclolStampBefore != 0 && rclolPointerStamp == rclolStampBefore, ++ rclolRootX, rclolRootY, NO); ++ return result; } - (void) setWindow: (id) window forID: (XID) i { -@@ -962,20 +1075,85 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -948,6 +1354,16 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { + // retaining the window causes it to keep popping back up endlessly even after dismissing it + if (lastFocusedWindow && [[lastFocusedWindow platformWindow] windowHandle] == i) + lastFocusedWindow = nil; ++ if (_definedCursorWindow == i) { ++ _definedCursorWindow = None; ++ [_definedCursor release]; ++ _definedCursor = nil; ++ } ++ /* The server released any grab on a destroyed window. */ ++ if (_confinedWindow == i) ++ _confinedWindow = None; ++ if (_relativeWindow == i) ++ [self suspendRelativeMouse]; + } + } + +@@ -962,20 +1378,85 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { { #ifndef DARLING [[NSRunLoop currentRunLoop] addInputSource: _inputSource forMode: mode]; @@ -2445,7 +2942,7 @@ index 5ae29bf4..358e606b 100644 } - (NSEventModifierFlags) modifierFlagsForState: (unsigned int) state { -@@ -1010,17 +1185,93 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1010,17 +1491,96 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { return result; } @@ -2516,6 +3013,9 @@ index 5ae29bf4..358e606b 100644 + default: + break; + } ++ /* Expected warp echoes are among the events queued when the warp was synced; motion ages them itself. */ ++ if (_relativeMouse.active && ev->type != MotionNotify) ++ X11RelativeMouseTick(&_relativeMouse); switch (ev->type) { case KeyPress: @@ -2539,7 +3039,7 @@ index 5ae29bf4..358e606b 100644 NSEventModifierFlags modifierFlags = [self modifierFlagsForState: ev->xkey.state]; char buf[20] = {0}; KeySym keySym; -@@ -1038,6 +1289,18 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1038,6 +1598,18 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { buf[strLen] = 0; } @@ -2558,13 +3058,13 @@ index 5ae29bf4..358e606b 100644 id str = [[NSString alloc] initWithCString: buf encoding: NSUTF8StringEncoding]; NSPoint pos = -@@ -1077,6 +1340,29 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1077,6 +1649,30 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { break; case ButtonPress:; -+ [self updateMouseConfinementForWindow:window]; + if (ev->xbutton.button >= Button4 && ev->xbutton.button <= 7) { -+ pos = [window transformPoint:NSMakePoint(ev->xbutton.x, ev->xbutton.y)]; ++ pos = [self locationInWindow: window x: ev->xbutton.x y: ev->xbutton.y ++ rootX: ev->xbutton.x_root rootY: ev->xbutton.y_root]; + CGFloat wheelX = ev->xbutton.button == 6 ? 1.0 : + ev->xbutton.button == 7 ? -1.0 : 0.0; + CGFloat wheelY = ev->xbutton.button == Button4 ? 1.0 : @@ -2589,7 +3089,18 @@ index 5ae29bf4..358e606b 100644 NSTimeInterval now = [[NSDate date] timeIntervalSinceReferenceDate]; if (now - lastClickTimeStamp < [self doubleClickInterval]) { -@@ -1096,10 +1382,6 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1086,8 +1682,8 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { + } + lastClickTimeStamp = now; + +- pos = [window +- transformPoint: NSMakePoint(ev->xbutton.x, ev->xbutton.y)]; ++ pos = [self locationInWindow: window x: ev->xbutton.x y: ev->xbutton.y ++ rootX: ev->xbutton.x_root rootY: ev->xbutton.y_root]; + + switch (ev->xbutton.button) { + case Button1: +@@ -1096,10 +1692,6 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { case Button3: type = NSRightMouseDown; break; @@ -2600,7 +3111,7 @@ index 5ae29bf4..358e606b 100644 default: type = NSOtherMouseDown; } -@@ -1113,14 +1395,22 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1113,14 +1705,22 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { clickCount: clickCount deltaX: 0.0 deltaY: 0.0]; @@ -2612,11 +3123,14 @@ index 5ae29bf4..358e606b 100644 break; case ButtonRelease: +- pos = [window +- transformPoint: NSMakePoint(ev->xbutton.x, ev->xbutton.y)]; +- + // One scroll event per notch, sent on press; releases are not clicks. + if (ev->xbutton.button >= Button4 && ev->xbutton.button <= 7) break; - pos = [window - transformPoint: NSMakePoint(ev->xbutton.x, ev->xbutton.y)]; - ++ pos = [self locationInWindow: window x: ev->xbutton.x y: ev->xbutton.y ++ rootX: ev->xbutton.x_root rootY: ev->xbutton.y_root]; ++ + if (ev->xbutton.button == Button2 || ev->xbutton.button >= 8) { + unsigned b = ev->xbutton.button == Button2 ? 2 : ev->xbutton.button - 5; + if (b < 32) _otherButtonsDown &= ~(1u << b); @@ -2624,7 +3138,7 @@ index 5ae29bf4..358e606b 100644 CGFloat deltaY = 0.0; switch (ev->xbutton.button) { -@@ -1130,14 +1420,6 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1130,14 +1730,6 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { case Button3: type = NSRightMouseUp; break; @@ -2639,7 +3153,7 @@ index 5ae29bf4..358e606b 100644 default: type = NSOtherMouseUp; } -@@ -1151,14 +1433,24 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1151,7 +1743,9 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { clickCount: clickCount deltaX: 0.0 deltaY: deltaY]; @@ -2650,22 +3164,70 @@ index 5ae29bf4..358e606b 100644 [self postEvent: event atStart: NO]; break; - case MotionNotify:; { - // NSLog(@"MotionNotify, x=%d, y=%d, xroot=%d, yroot=%d\n", - // ev->xmotion.x, ev->xmotion.y, ev->xmotion.x_root, - // ev->xmotion.y_root); -+ /* darling-lol perf: drain pending MotionNotify events for this -+ * window from the X11 queue so we only process the latest position. */ -+ while (XCheckTypedWindowEvent(_display, ev->xmotion.window, MotionNotify, ev)) -+ ; -+ rclolRootX = ev->xmotion.x_root; -+ rclolRootY = ev->xmotion.y_root; -+ rclolPointerState = ev->xmotion.state; -+ rclolPointerStamp = mach_absolute_time(); - - CGPoint lastMotionPos = [window mouseLocationOutsideOfEventStream]; +@@ -1164,45 +1758,82 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { pos = [window -@@ -1195,14 +1479,29 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { + transformPoint: NSMakePoint(ev->xmotion.x, ev->xmotion.y)]; + +- CGFloat deltaX = pos.x - lastMotionPos.x; +- CGFloat deltaY = pos.y - lastMotionPos.y; +- +- // NSLog(@"cursorGrabbed=%d, deltaX=%f, deltaY=%f\n", +- // _cursorGrabbed, deltaX, deltaY); +- if (_cursorGrabbed) { +- if (pos.x != lastMotionPos.x || pos.y != lastMotionPos.y) { +- CGPoint globalPos = [window transformPoint: lastMotionPos]; +- // NSLog(@"last known pos in window: x=%f, y=%f", +- // globalPos.x, globalPos.y); +- CGRect frame = [window transformFrame: [window frame]]; +- +- globalPos.x += frame.origin.x; +- globalPos.y += frame.origin.y; +- +- [self warpMouse: globalPos]; +- +- pos.x = lastMotionPos.x; +- pos.y = lastMotionPos.y; +- } else { +- // This is an event generated by [self warpMouse:] above ++ double deltaX, deltaY; ++ CGPoint rclolEventPos = pos, rclolLastPos = lastMotionPos; ++ const char *rclolDecision = "absolute"; ++ ++ if (_relativeMouse.active) { ++ /* The cursor is disassociated (CGAssociateMouseAndMouseCursorPosition(false)): physical motion ++ * only produces deltas and the event reports the frozen virtual cursor. Where warps are honoured ++ * the pointer is put back under it, so the visible cursor stays frozen as on macOS. */ ++ BOOL warpsHonoured = !_isXwayland || _cursorHidden; ++ rclolLastPos = NSMakePoint(_relativeMouse.physicalX, _relativeMouse.physicalY); ++ X11RelativeMouseMotion relative = X11RelativeMouseHandleMotion( ++ &_relativeMouse, ev->xmotion.x_root, ev->xmotion.y_root, warpsHonoured); ++ rclolEventPos = NSMakePoint(_relativeMouse.virtualX, _relativeMouse.virtualY); ++ if (relative.rewarp) { ++ if (rclolTraceOn()) ++ rclolTraceRated(&rclolWarpRewarpChannel, "target=%d,%d from=%d,%d honoured=%d", ++ relative.warpX, relative.warpY, ev->xmotion.x_root, ev->xmotion.y_root, ++ warpsHonoured); ++ unsigned queued = [self warpPointerToRootX: relative.warpX y: relative.warpY]; ++ X11RelativeMouseExpectEcho(&_relativeMouse, relative.warpX, relative.warpY, queued); ++ } ++ if (!relative.post) { ++ if (rclolTraceOn()) ++ rclolTraceMotion(ev, YES, rclolEventPos, rclolLastPos, relative.deltaX, relative.deltaY, ++ relative.echo ? "echo" : "relative-still", -1); + break; + } ++ deltaX = relative.deltaX; ++ deltaY = relative.deltaY; ++ pos = [self locationInWindow: window x: ev->xmotion.x y: ev->xmotion.y ++ rootX: ev->xmotion.x_root rootY: ev->xmotion.y_root]; ++ rclolDecision = relative.rewarp ? "relative+rewarp" : "relative"; + } else { +- [window setLastKnownCursorPosition: pos]; ++ X11AbsoluteMouseDelta(lastMotionPos.x, lastMotionPos.y, pos.x, pos.y, &deltaX, &deltaY); + } ++ [window setLastKnownCursorPosition: pos]; + + type = NSMouseMoved; + [self pointerMoved]; if (ev->xmotion.state & Button1Mask) { @@ -2679,6 +3241,11 @@ index 5ae29bf4..358e606b 100644 - if (type == NSMouseMoved && ![delegate acceptsMouseMovedEvents]) - break; ++ if (rclolTraceOn()) ++ rclolTraceMotion(ev, _relativeMouse.active, rclolEventPos, rclolLastPos, deltaX, deltaY, rclolDecision, ++ type != NSMouseMoved || [delegate acceptsMouseMovedEvents] || ++ getenv("RCLOL_FORWARD_MOUSE_MOVED") != NULL); ++ + if (inputTrace) { + static NSTimeInterval lastMotionTrace; + NSTimeInterval now = [NSDate timeIntervalSinceReferenceDate]; @@ -2687,7 +3254,7 @@ index 5ae29bf4..358e606b 100644 + NSLog(@"LOLINPUT motion window=%lu mapped=%@ pos=%.0f,%.0f delta=%.0f,%.0f state=%u grabbed=%d forward=%d", + (unsigned long)ev->xany.window, [delegate class], + pos.x, pos.y, deltaX, deltaY, ev->xmotion.state, -+ _cursorGrabbed, type != NSMouseMoved || [delegate acceptsMouseMovedEvents] || ++ _relativeMouse.active, type != NSMouseMoved || [delegate acceptsMouseMovedEvents] || + getenv("RCLOL_FORWARD_MOUSE_MOVED") != NULL); + } + } @@ -2695,11 +3262,18 @@ index 5ae29bf4..358e606b 100644 - event = [NSEvent + if (type != NSMouseMoved || [delegate acceptsMouseMovedEvents] || + getenv("RCLOL_FORWARD_MOUSE_MOVED")) { ++ /* Relative motion is all in the deltas: keep those of the queued events coalesced below. */ ++ if (_relativeMouse.active) ++ for (NSEvent *queued in _eventQueue) ++ if ([queued type] == type) { ++ deltaX += [queued deltaX]; ++ deltaY += [queued deltaY]; ++ } + event = [NSEvent mouseEventWithType: type location: pos modifierFlags: -@@ -1211,25 +1510,40 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1211,28 +1842,49 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { clickCount: 1 deltaX: deltaX deltaY: deltaY]; @@ -2722,18 +3296,19 @@ index 5ae29bf4..358e606b 100644 case EnterNotify: + [self updateMouseConfinementForWindow:window]; NSLog(@"EnterNotify"); - if (!_cursorGrabbed) +- if (!_cursorGrabbed) ++ if (!_relativeMouse.active) [window setLastKnownCursorPosition: [window transformPoint: NSMakePoint( ev->xcrossing.x, ev->xcrossing.y)]]; -+ if (!_cursorGrabbed && ev->xcrossing.detail != NotifyInferior) ++ if (!_relativeMouse.active && ev->xcrossing.detail != NotifyInferior) + [delegate platformWindowSetCursorEvent: window]; break; case LeaveNotify: NSLog(@"LeaveNotify"); -+ if (!_cursorGrabbed && ev->xcrossing.detail != NotifyInferior) { ++ if (!_relativeMouse.active && ev->xcrossing.detail != NotifyInferior) { + [window setLastKnownCursorPosition: + [window transformPoint: NSMakePoint(ev->xcrossing.x, ev->xcrossing.y)]]; + [delegate platformWindowSetCursorEvent: window]; @@ -2741,7 +3316,16 @@ index 5ae29bf4..358e606b 100644 break; case FocusIn: -@@ -1243,28 +1557,42 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { ++ if (rclolTraceOn()) ++ rclolTraceRated(&rclolFocusChannel, "FocusIn window=%lu known=%d mode=%s detail=%s relative=%d/%d confined=%lu appActive=%d sheet=%d", ++ (unsigned long) ev->xany.window, window != nil, rclolFocusModeName(ev->xfocus.mode), ++ rclolFocusDetailName(ev->xfocus.detail), _relativeRequested, _relativeMouse.active, ++ (unsigned long) _confinedWindow, ++ [NSApp isActive], [delegate attachedSheet] != nil); + NSLog(@"FocusIn"); + if ([delegate attachedSheet]) { + [[delegate attachedSheet] makeKeyAndOrderFront: delegate]; +@@ -1243,28 +1895,55 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { checkForAppDeactivation: NO]; lastFocusedWindow = nil; } @@ -2752,18 +3336,33 @@ index 5ae29bf4..358e606b 100644 lastFocusedWindow = delegate; if (window != nil) XSetICFocus(window->_xic); ++ /* Relative mode released its grab on focus loss; take it again. */ ++ if (_relativeRequested && !_relativeMouse.active) ++ [self beginRelativeMouseWithWindow: NULL pick: NULL]; + [self updateMouseConfinementForWindow:window]; ++ if (rclolTraceOn()) ++ rclolTraceRated(&rclolFocusChannel, "FocusIn done window=%lu relative=%d/%d confined=%lu appActive=%d", ++ (unsigned long) ev->xany.window, _relativeRequested, _relativeMouse.active, ++ (unsigned long) _confinedWindow, [NSApp isActive]); break; case FocusOut: ++ if (rclolTraceOn()) ++ rclolTraceRated(&rclolFocusChannel, "FocusOut window=%lu known=%d mode=%s detail=%s relative=%d/%d confined=%lu appActive=%d", ++ (unsigned long) ev->xany.window, window != nil, rclolFocusModeName(ev->xfocus.mode), ++ rclolFocusDetailName(ev->xfocus.detail), _relativeRequested, _relativeMouse.active, ++ (unsigned long) _confinedWindow, ++ [NSApp isActive]); NSLog(@"FocusOut"); + [self reconcileModifierKeys:NULL window:delegate]; + _lastKeySym = 0; [delegate platformWindowDeactivated: window checkForAppDeactivation: NO]; lastFocusedWindow = nil; - if (_cursorGrabbed) - [self grabMouse: NO]; +- if (_cursorGrabbed) +- [self grabMouse: NO]; ++ /* Give the pointer back to other clients; relative mode stays requested and resumes on FocusIn. */ ++ [self suspendRelativeMouse]; + if (_confinedWindow) { + XUngrabPointer(_display, CurrentTime); + _confinedWindow = None; @@ -2784,7 +3383,7 @@ index 5ae29bf4..358e606b 100644 O2Rect rect = NSMakeRect(ev->xexpose.x, ev->xexpose.y, ev->xexpose.width, ev->xexpose.height); -@@ -1301,11 +1629,20 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1301,11 +1980,23 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { break; case UnmapNotify: @@ -2798,6 +3397,9 @@ index 5ae29bf4..358e606b 100644 + if(window && ev->xany.window == [window windowHandle] && + [delegate respondsToSelector: @selector(_setVisible:)]) { + BOOL visible = ev->type == MapNotify; ++ /* The server released a grab whose window became unviewable. */ ++ if (!visible && ev->xany.window == _relativeWindow) ++ [self suspendRelativeMouse]; + [self updateMouseConfinementForWindow:window]; + if([delegate isVisible] != visible) { + [delegate _setVisible: visible]; @@ -2809,7 +3411,7 @@ index 5ae29bf4..358e606b 100644 break; case MapRequest: -@@ -1321,6 +1658,7 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1321,6 +2012,7 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { [delegate platformWindow: window frameChanged: [window frame] didSize: YES]; @@ -2817,7 +3419,7 @@ index 5ae29bf4..358e606b 100644 break; case ConfigureRequest: -@@ -1344,6 +1682,8 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { +@@ -1344,6 +2036,8 @@ static NSDictionary *modeInfoToDictionary(const XRRModeInfo *mi, int depth) { break; case PropertyNotify: @@ -2826,19 +3428,78 @@ index 5ae29bf4..358e606b 100644 if ([window respondsToSelector: @selector(propertyNotify:)]) { [window propertyNotify: &ev->xproperty]; } -@@ -1483,6 +1823,113 @@ void CGNativeBorderFrameWidthsForStyle(NSUInteger styleMask, CGFloat *top, - XSync(_display, False); +@@ -1433,6 +2127,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); ++ if (rclolTraceOn()) ++ rclolTraceRated(&rclolErrorChannel, "request=%d:%d error=%d resource=%lu serial=%lu", ++ errorEvent->request_code, errorEvent->minor_code, errorEvent->error_code, ++ (unsigned long) errorEvent->resourceid, (unsigned long) errorEvent->serial); + return 0; } - -+/* Confinement preserves absolute motion and edge panning. Relative-mode -+ * capture remains separate: it deliberately warps to the center. */ + +@@ -1476,54 +2174,234 @@ void CGNativeBorderFrameWidthsForStyle(NSUInteger styleMask, CGFloat *top, + return frame; + } + ++/* Moves the X pointer and waits for the server, so the warp's MotionNotify (if any) is already queued on return. ++ * Returns the number of queued events, the window in which that echo must arrive. */ ++- (unsigned) warpPointerToRootX: (int) x y: (int) y { ++ XWarpPointer(_display, None, DefaultRootWindow(_display), 0, 0, 0, 0, x, y); ++ XSync(_display, False); ++ rclolPointerStamp = 0; ++ return (unsigned) XEventsQueued(_display, QueuedAlready); ++} ++ ++/* Window-local Cocoa location of a pointer event at (x, y) in window, root (rootX, rootY). While relative this is ++ * the virtual cursor: macOS reports the frozen cursor position, not where the physical pointer went. */ ++- (NSPoint) locationInWindow: (X11Window *) window x: (int) x y: (int) y rootX: (int) rootX rootY: (int) rootY { ++ if (_relativeMouse.active) ++ return [window transformPoint: NSMakePoint(_relativeMouse.virtualX - (rootX - x), ++ _relativeMouse.virtualY - (rootY - y))]; ++ return [window transformPoint: NSMakePoint(x, y)]; ++} ++ + - (void) warpMouse: (NSPoint) position { ++ if (rclolTraceOn()) ++ rclolTraceRated(&rclolWarpAppChannel, "target=%.1f,%.1f relative=%d/%d virtual=%.1f,%.1f physical=%d,%d " ++ "honoured=%d confined=%lu cachedRoot=%d,%d appActive=%d", ++ position.x, position.y, _relativeRequested, _relativeMouse.active, _relativeMouse.virtualX, ++ _relativeMouse.virtualY, _relativeMouse.physicalX, _relativeMouse.physicalY, ++ !_isXwayland || _cursorHidden, (unsigned long) _confinedWindow, rclolRootX, rclolRootY, ++ [NSApp isActive]); ++ int x = X11RelativeMouseWarpCoordinate(position.x), y = X11RelativeMouseWarpCoordinate(position.y); ++ if (_relativeMouse.active) { ++ /* CGWarpMouseCursorPosition moves the disassociated cursor without producing a delta: the virtual ++ * cursor follows and the warp's echo is dropped. Not honoured on Xwayland with a visible cursor. */ ++ X11RelativeMouseWarp(&_relativeMouse, position.x, position.y, [self warpPointerToRootX: x y: y]); ++ return; ++ } + NSLog(@"Warp to: x=%f, y=%f\n", position.x, position.y); +- XWarpPointer(_display, None, DefaultRootWindow(_display), 0, 0, 0, 0, +- 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 { -+ if (getenv("RCLOL_INPUT_TRACE")) fprintf(stderr,"CONFINEMENT check window=%p delegate=%p focused=%p relative=%d\n",window,[window delegate],lastFocusedWindow,_cursorGrabbed); -+ if (!window || _cursorGrabbed) return; -+ if (getenv("RCLOL_MOUSE_CONFINEMENT") && strcmp(getenv("RCLOL_MOUSE_CONFINEMENT"), "0") == 0) return; ++ if (getenv("RCLOL_INPUT_TRACE")) fprintf(stderr,"CONFINEMENT check window=%p delegate=%p focused=%p relative=%d\n",window,[window delegate],lastFocusedWindow,_relativeRequested); ++ if (!window || _relativeRequested) { ++ if (rclolTraceOn()) ++ rclolTraceRated(&rclolConfineChannel, "skip: %s confined=%lu", !window ? "window=nil" : "relative mode requested", ++ (unsigned long) _confinedWindow); ++ return; ++ } + Window win = [window windowHandle]; + XWindowAttributes attrs; -+ if (!XGetWindowAttributes(_display, win, &attrs)) return; ++ if (!XGetWindowAttributes(_display, win, &attrs)) { ++ if (rclolTraceOn()) ++ rclolTraceRated(&rclolConfineChannel, "skip: XGetWindowAttributes failed window=%lu", (unsigned long) win); ++ return; ++ } + + BOOL fullscreen = NO; + Atom actual; int format; unsigned long count, remaining; unsigned char *data = NULL; @@ -2849,6 +3510,7 @@ index 5ae29bf4..358e606b 100644 + for (unsigned long i=0; i%d requested=%d confined %lu->%lu appActive=%d virtual=%.1f,%.1f xwayland=%d " ++ "hidden=%d cachedRoot=%d,%d", ++ rclolGrabGeneration, doGrab, rclolPick, rclolWindow, ++ rclolWindow ? object_getClassName(rclolWindow) : "-", ++ (unsigned long) (rclolWindow ? [(X11Window *) [rclolWindow platformWindow] windowHandle] : None), ++ rclolResult, doGrab ? rclolGrabResultName(rclolResult) : "ungrab", rclolRelativeBefore, ++ _relativeMouse.active, _relativeRequested, (unsigned long) rclolConfinedBefore, ++ (unsigned long) _confinedWindow, [NSApp isActive], _relativeMouse.virtualX, ++ _relativeMouse.virtualY, _isXwayland, _cursorHidden, rclolRootX, rclolRootY); } + + @end diff --git a/AppKit/X11.backend/X11Pasteboard.h b/AppKit/X11.backend/X11Pasteboard.h index 411903cc..4b196afd 100644 --- a/AppKit/X11.backend/X11Pasteboard.h @@ -3137,6 +3905,159 @@ index 0e91a5d1..1e4cec1b 100644 } - (void) selectionNotify: (XSelectionEvent *) event { +diff --git a/AppKit/X11.backend/X11RelativeMouse.h b/AppKit/X11.backend/X11RelativeMouse.h +new file mode 100644 +index 00000000..7fc39cc4 +--- /dev/null ++++ b/AppKit/X11.backend/X11RelativeMouse.h +@@ -0,0 +1,147 @@ ++/* darling-lol: decision logic for macOS-style relative mouse mode in the X11 backend. ++ * ++ * While CGAssociateMouseAndMouseCursorPosition(false) is in effect, macOS freezes the cursor: physical motion ++ * only produces deltas, events report the frozen cursor position, and CGWarpMouseCursorPosition moves the cursor ++ * without producing a delta. Here the frozen cursor is the "virtual" position (X root coordinates). Core X motion ++ * still moves the real pointer, so deltas are measured against the last physical position, and where the server ++ * honours warps the pointer is warped back to the virtual position after each motion. Every warp (ours or the ++ * application's) is followed by a MotionNotify at its target; those echoes are recognised by position and ++ * dropped. Warps are synchronous (XSync), so an echo can only be among the events already queued at that moment; ++ * an expected echo expires once that many events have been handled. ++ * ++ * On Xwayland a warp only reaches the compositor while the X cursor is hidden. With a visible cursor the echo ++ * still arrives, but the next real motion reports the unmoved physical pointer, so honoured=false keeps the last ++ * physical position across echoes and never warps back. ++ * ++ * Plain C without X11 or Objective-C dependencies, so tools/test-x11-relative-mouse.c can run it on the host. */ ++#ifndef X11_RELATIVE_MOUSE_H ++#define X11_RELATIVE_MOUSE_H ++ ++#include ++ ++#define X11_RELATIVE_MOUSE_MAX_ECHOES 8 ++ ++typedef struct { ++ int x, y; ++ unsigned remaining; /* X events (of any type) still to be handled before this echo is given up */ ++} X11RelativeMouseEcho; ++ ++typedef struct { ++ bool active; /* relative motion handling is on (the pointer grab is held) */ ++ double virtualX, virtualY; /* the frozen macOS cursor, X root coordinates */ ++ int physicalX, physicalY; /* last physical pointer position, X root coordinates */ ++ unsigned echoCount; ++ X11RelativeMouseEcho echoes[X11_RELATIVE_MOUSE_MAX_ECHOES]; /* oldest first */ ++} X11RelativeMouse; ++ ++typedef struct { ++ bool post; /* deliver a mouse moved/dragged event */ ++ bool echo; /* the event was the echo of a warp */ ++ double deltaX, deltaY; /* NSEvent convention: positive deltaY is downwards */ ++ bool rewarp; /* warp the pointer back to (warpX, warpY) */ ++ int warpX, warpY; ++} X11RelativeMouseMotion; ++ ++/* X11 warps take integer coordinates; conversions truncate like the original implicit double-to-int argument. */ ++static inline int X11RelativeMouseWarpCoordinate(double value) { ++ return (int) value; ++} ++ ++static inline void X11RelativeMouseBegin(X11RelativeMouse *mouse, int rootX, int rootY) { ++ mouse->active = true; ++ mouse->virtualX = rootX; ++ mouse->virtualY = rootY; ++ mouse->physicalX = rootX; ++ mouse->physicalY = rootY; ++ mouse->echoCount = 0; ++} ++ ++static inline void X11RelativeMouseEnd(X11RelativeMouse *mouse) { ++ mouse->active = false; ++ mouse->echoCount = 0; ++} ++ ++/* Record a warp to (x, y) that was just sent and synced. queuedEvents is the number of X events queued after the ++ * sync; zero means the warp produced no event (the pointer was already there). */ ++static inline void X11RelativeMouseExpectEcho(X11RelativeMouse *mouse, int x, int y, unsigned queuedEvents) { ++ if (!queuedEvents) return; ++ if (mouse->echoCount) { ++ X11RelativeMouseEcho *last = &mouse->echoes[mouse->echoCount - 1]; ++ if (last->x == x && last->y == y) { ++ if (last->remaining < queuedEvents) last->remaining = queuedEvents; ++ return; ++ } ++ } ++ if (mouse->echoCount == X11_RELATIVE_MOUSE_MAX_ECHOES) { ++ for (unsigned i = 1; i < mouse->echoCount; ++i) ++ mouse->echoes[i - 1] = mouse->echoes[i]; ++ mouse->echoCount--; ++ } ++ mouse->echoes[mouse->echoCount++] = (X11RelativeMouseEcho) {x, y, queuedEvents}; ++} ++ ++/* CGWarpMouseCursorPosition while relative: the virtual cursor moves to the target; the physical position is ++ * only updated when the echo arrives (and only if the warp was honoured). */ ++static inline void X11RelativeMouseWarp(X11RelativeMouse *mouse, double x, double y, unsigned queuedEvents) { ++ mouse->virtualX = x; ++ mouse->virtualY = y; ++ X11RelativeMouseExpectEcho(mouse, X11RelativeMouseWarpCoordinate(x), X11RelativeMouseWarpCoordinate(y), ++ queuedEvents); ++} ++ ++/* One X event has been handled: age the expected echoes. */ ++static inline void X11RelativeMouseTick(X11RelativeMouse *mouse) { ++ unsigned kept = 0; ++ for (unsigned i = 0; i < mouse->echoCount; ++i) ++ if (mouse->echoes[i].remaining > 1) { ++ mouse->echoes[kept] = mouse->echoes[i]; ++ mouse->echoes[kept++].remaining--; ++ } ++ mouse->echoCount = kept; ++} ++ ++/* A MotionNotify at root position (rootX, rootY) while relative. Ages the expected echoes. */ ++static inline X11RelativeMouseMotion X11RelativeMouseHandleMotion(X11RelativeMouse *mouse, int rootX, int rootY, ++ bool warpsHonoured) ++{ ++ X11RelativeMouseMotion result = {false, false, 0.0, 0.0, false, 0, 0}; ++ for (unsigned i = 0; i < mouse->echoCount; ++i) ++ if (mouse->echoes[i].x == rootX && mouse->echoes[i].y == rootY) { ++ /* Warps are echoed in order; earlier expected echoes that did not arrive never will. */ ++ for (unsigned j = i + 1; j < mouse->echoCount; ++j) ++ mouse->echoes[j - i - 1] = mouse->echoes[j]; ++ mouse->echoCount -= i + 1; ++ if (warpsHonoured) { ++ mouse->physicalX = rootX; ++ mouse->physicalY = rootY; ++ } ++ result.echo = true; ++ X11RelativeMouseTick(mouse); ++ return result; ++ } ++ result.deltaX = rootX - mouse->physicalX; ++ result.deltaY = rootY - mouse->physicalY; ++ mouse->physicalX = rootX; ++ mouse->physicalY = rootY; ++ result.post = result.deltaX != 0.0 || result.deltaY != 0.0; ++ int warpX = X11RelativeMouseWarpCoordinate(mouse->virtualX); ++ int warpY = X11RelativeMouseWarpCoordinate(mouse->virtualY); ++ if (warpsHonoured && (rootX != warpX || rootY != warpY)) { ++ result.rewarp = true; ++ result.warpX = warpX; ++ result.warpY = warpY; ++ } ++ X11RelativeMouseTick(mouse); ++ return result; ++} ++ ++/* NSEvent deltas for absolute (non-relative) motion between two positions in Cocoa window coordinates, which ++ * are bottom-up while NSEvent's deltaY is positive downwards. */ ++static inline void X11AbsoluteMouseDelta(double lastX, double lastY, double x, double y, double *deltaX, ++ double *deltaY) ++{ ++ *deltaX = x - lastX; ++ *deltaY = lastY - y; ++} ++ ++#endif diff --git a/AppKit/X11.backend/X11Window.h b/AppKit/X11.backend/X11Window.h index bfe3839a..2521ffd5 100644 --- a/AppKit/X11.backend/X11Window.h @@ -3186,7 +4107,7 @@ index bfe3839a..2521ffd5 100644 - (void) frameChanged; - (void) setLastKnownCursorPosition: (CGPoint) point; diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m -index f4925b66..5ad2d1f7 100644 +index f4925b66..d33c389b 100644 --- a/AppKit/X11.backend/X11Window.m +++ b/AppKit/X11.backend/X11Window.m @@ -38,6 +38,34 @@ @@ -3224,10 +4145,12 @@ index f4925b66..5ad2d1f7 100644 @implementation X11Window -@@ -191,10 +219,30 @@ static NSData *makeWindowIcon() { - return nil; - } +@@ -193,8 +221,28 @@ static NSData *makeWindowIcon() { + XSetWindowAttributes xattr; + unsigned long xattr_mask; +- xattr.override_redirect = +- _styleMask == NSBorderlessWindowMask ? True : False; + NSString *procName = [[NSProcessInfo processInfo] processName]; + BOOL isGame = (procName && [procName rangeOfString:@"League" options:NSCaseInsensitiveSearch].location != NSNotFound && + [procName rangeOfString:@"Ux" options:NSCaseInsensitiveSearch].location == NSNotFound); @@ -3246,10 +4169,6 @@ index f4925b66..5ad2d1f7 100644 + } + } + - XSetWindowAttributes xattr; - unsigned long xattr_mask; -- xattr.override_redirect = -- _styleMask == NSBorderlessWindowMask ? True : False; + // A borderless game is still a focusable application window. Leaving it + // override-redirect makes XWayland bypass compositor keyboard activation. + // Menus and nonactivating popups keep their unmanaged behavior. @@ -3284,7 +4203,10 @@ index f4925b66..5ad2d1f7 100644 + XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&bypass, 1); + } + -@@ -266,6 +305,11 @@ static NSData *makeWindowIcon() { + XSetWindowBackgroundPixmap(_display, _window, None); + + _isModal = [delegate isSheet] || [NSApp modalWindow] == delegate; +@@ -266,6 +330,11 @@ static NSData *makeWindowIcon() { [self setWindowIcon]; @@ -3296,7 +4218,31 @@ index f4925b66..5ad2d1f7 100644 return self; } -@@ -338,7 +382,6 @@ static NSData *makeWindowIcon() { +@@ -287,10 +356,19 @@ static NSData *makeWindowIcon() { + // Make resizable + sh->flags = 0; + } else { +- // Make non-resizable +- sh->flags = PMinSize | PMaxSize; +- sh->min_width = sh->max_width = _frame.size.width; +- sh->min_height = sh->max_height = _frame.size.height; ++ NSString *procName = [[NSProcessInfo processInfo] processName]; ++ BOOL isGame = (procName && [procName rangeOfString:@"League" options:NSCaseInsensitiveSearch].location != NSNotFound && ++ [procName rangeOfString:@"Ux" options:NSCaseInsensitiveSearch].location == NSNotFound); ++ if (isGame || mask == NSBorderlessWindowMask) { ++ sh->flags = PMinSize; ++ sh->min_width = _frame.size.width; ++ sh->min_height = _frame.size.height; ++ } else { ++ // Make non-resizable ++ sh->flags = PMinSize | PMaxSize; ++ sh->min_width = sh->max_width = _frame.size.width; ++ sh->min_height = sh->max_height = _frame.size.height; ++ } + } + + XSetWMSizeHints(_display, _window, sh, XA_WM_NORMAL_HINTS); +@@ -338,7 +416,6 @@ static NSData *makeWindowIcon() { } + (void) removeDecorationForWindow: (Window) w onDisplay: (Display *) dpy { @@ -3304,7 +4250,7 @@ index f4925b66..5ad2d1f7 100644 struct { unsigned long flags; unsigned long functions; -@@ -358,6 +401,7 @@ static NSData *makeWindowIcon() { +@@ -358,6 +435,7 @@ static NSData *makeWindowIcon() { if (!_mapped) { XMapWindow(_display, _window); _mapped = YES; @@ -3312,7 +4258,7 @@ index f4925b66..5ad2d1f7 100644 } } -@@ -377,15 +421,27 @@ static NSData *makeWindowIcon() { +@@ -377,15 +455,27 @@ static NSData *makeWindowIcon() { - (void) syncDelegateProperties { long mask = KeyPressMask | KeyReleaseMask | ExposureMask | StructureNotifyMask | EnterWindowMask | LeaveWindowMask | @@ -3345,7 +4291,7 @@ index f4925b66..5ad2d1f7 100644 // TODO: background color } -@@ -393,6 +449,28 @@ static NSData *makeWindowIcon() { +@@ -393,6 +483,28 @@ static NSData *makeWindowIcon() { // This is essentially dealloc; we release our contexts // and windows, but unlike dealloc, this method can be called // several times, so set everything to nil/NULL/0. @@ -3374,7 +4320,7 @@ index f4925b66..5ad2d1f7 100644 [_context release]; _context = nil; -@@ -421,6 +499,388 @@ static NSData *makeWindowIcon() { +@@ -421,6 +533,388 @@ static NSData *makeWindowIcon() { } } @@ -3763,23 +4709,28 @@ index f4925b66..5ad2d1f7 100644 - (Window) windowHandle { return _window; } -@@ -478,6 +938,15 @@ static NSData *makeWindowIcon() { +@@ -478,6 +972,20 @@ static NSData *makeWindowIcon() { frame.size.width, frame.size.height); [self invalidateContextWithNewSize: frame.size]; _frame = frame; + // Nonresizable windows can be created at 1x1 and enlarged before display. + // Refresh the constraints so a managing compositor sees the current size. + if (!(_styleMask & NSWindowStyleMaskResizable)) { -+ XSizeHints hints = {0}; -+ hints.flags = PMinSize | PMaxSize; -+ hints.min_width = hints.max_width = frame.size.width; -+ hints.min_height = hints.max_height = frame.size.height; -+ XSetWMSizeHints(_display, _window, &hints, XA_WM_NORMAL_HINTS); ++ NSString *procName = [[NSProcessInfo processInfo] processName]; ++ BOOL isGame = (procName && [procName rangeOfString:@"League" options:NSCaseInsensitiveSearch].location != NSNotFound && ++ [procName rangeOfString:@"Ux" options:NSCaseInsensitiveSearch].location == NSNotFound); ++ if (!isGame && _styleMask != NSBorderlessWindowMask) { ++ XSizeHints hints = {0}; ++ hints.flags = PMinSize | PMaxSize; ++ hints.min_width = hints.max_width = frame.size.width; ++ hints.min_height = hints.max_height = frame.size.height; ++ XSetWMSizeHints(_display, _window, &hints, XA_WM_NORMAL_HINTS); ++ } + } } - (void) setHasShadow: (BOOL) value { -@@ -488,6 +957,25 @@ static NSData *makeWindowIcon() { +@@ -488,6 +996,25 @@ static NSData *makeWindowIcon() { _level = value; } @@ -3805,7 +4756,7 @@ index f4925b66..5ad2d1f7 100644 - (void) setOpaque: (BOOL) value { _isOpaque = value; } -@@ -559,7 +1047,29 @@ static NSData *makeWindowIcon() { +@@ -559,7 +1086,29 @@ static NSData *makeWindowIcon() { XRaiseWindow(_display, _window); } @@ -3835,7 +4786,7 @@ index f4925b66..5ad2d1f7 100644 Atom wmState = XInternAtom(_display, "_NET_WM_STATE", FALSE); Atom hiddenState = XInternAtom(_display, "_NET_WM_STATE_HIDDEN", FALSE); -@@ -615,6 +1125,12 @@ static NSData *makeWindowIcon() { +@@ -615,6 +1164,12 @@ static NSData *makeWindowIcon() { } - (void) openGLFlushBuffer { @@ -3848,7 +4799,7 @@ index f4925b66..5ad2d1f7 100644 CGLError error; CGLContextObj prevContext = CGLGetCurrentContext(); -@@ -666,6 +1182,7 @@ static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) { +@@ -666,6 +1221,7 @@ static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) { } - (void) frameChanged { @@ -3924,10 +4875,10 @@ index b5ae3a65..6d555f60 100644 - (NSColor *) blendedColorWithFraction: (CGFloat) fraction ofColor: (NSColor *) color; diff --git a/AppKit/include/AppKit/NSDisplay.h b/AppKit/include/AppKit/NSDisplay.h -index 8860a087..aec7116d 100644 +index 8860a087..a897e2ce 100644 --- a/AppKit/include/AppKit/NSDisplay.h +++ b/AppKit/include/AppKit/NSDisplay.h -@@ -22,6 +22,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +@@ -22,15 +22,18 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #import #import #import @@ -3935,7 +4886,7 @@ index 8860a087..aec7116d 100644 @class NSColor, NSDraggingManager, NSPrintInfo, NSView, NSSavePanel, NSOpenPanel, NSFontTypeface, CGWindow; -@@ -29,8 +30,10 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + @interface NSDisplay : NSObject { NSMutableArray *_eventQueue; + BOOL _cursorHiddenUntilMouseMoves; @@ -3946,7 +4897,8 @@ index 8860a087..aec7116d 100644 - (NSArray *) screens; -@@ -50,7 +53,9 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ +@@ -47,6 +50,8 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ + - (void) hideCursor; - (void) unhideCursor; +- (void) setCursorHiddenUntilMouseMoves: (BOOL) flag; @@ -3954,8 +4906,6 @@ index 8860a087..aec7116d 100644 - (id) cursorWithImage: (NSImage *) image hotSpot: (NSPoint) hotSpot; - (id) cursorWithName: (NSString *) name; - - (void) setCursor: (id) cursor; - @@ -95,6 +100,7 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ - (NSPoint) mouseLocation; @@ -8581,6 +9531,7 @@ index 985f77f3..db64f55d 100644 + (CAMediaTimingFunction *) animationTimingFunction; diff --git a/QuartzCore/include/QuartzCore/CATransformLayer.h b/QuartzCore/include/QuartzCore/CATransformLayer.h new file mode 100644 +index 00000000..19cd9ea9 --- /dev/null +++ b/QuartzCore/include/QuartzCore/CATransformLayer.h @@ -0,0 +1,28 @@ @@ -8612,140 +9563,3 @@ new file mode 100644 +- (instancetype) init; + +@end -diff --git a/AppKit/NSWorkspace.m b/AppKit/NSWorkspace.m -index ad5e4f9d..dbd997aa 100644 ---- a/AppKit/NSWorkspace.m -+++ b/AppKit/NSWorkspace.m -@@ -20,9 +20,58 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ - #import - #import - #import -+#include - - NSString *const NSWorkspaceApplicationKey = @"NSWorkspaceApplicationKey"; - -+/* Electron's shell.openExternal() uses -openURL:configuration:completionHandler: (macOS 10.15) -+ * with a configured NSWorkspaceOpenConfiguration, so the configuration needs real properties. -+ * The URL itself is opened by -openURL: (LaunchServices in MacWorkspace). */ -+@interface NSWorkspaceOpenConfiguration () { -+ BOOL _activates, _hides, _hidesOthers, _addsToRecentItems, _createsNewApplicationInstance; -+ BOOL _allowsRunningApplicationSubstitution, _forPrinting, _promptsUserIfNeeded, _requiresUniversalLinks; -+ NSAppleEventDescriptor *_appleEvent; -+ NSArray *_arguments; -+ NSDictionary *_environment; -+ cpu_type_t _architecture; -+} -+@end -+ -+@implementation NSWorkspaceOpenConfiguration -+@synthesize activates = _activates, hides = _hides, hidesOthers = _hidesOthers; -+@synthesize addsToRecentItems = _addsToRecentItems, createsNewApplicationInstance = _createsNewApplicationInstance; -+@synthesize allowsRunningApplicationSubstitution = _allowsRunningApplicationSubstitution; -+@synthesize forPrinting = _forPrinting, promptsUserIfNeeded = _promptsUserIfNeeded; -+@synthesize requiresUniversalLinks = _requiresUniversalLinks; -+@synthesize appleEvent = _appleEvent, arguments = _arguments, environment = _environment; -+@synthesize architecture = _architecture; -++ (instancetype) configuration { -+ NSWorkspaceOpenConfiguration *c = [[[self alloc] init] autorelease]; -+ c->_activates = YES; -+ c->_addsToRecentItems = YES; -+ c->_allowsRunningApplicationSubstitution = YES; -+ return c; -+} -+- (id) copyWithZone: (NSZone *) zone { -+ NSWorkspaceOpenConfiguration *c = [[[self class] allocWithZone: zone] init]; -+ c->_activates = _activates; c->_hides = _hides; c->_hidesOthers = _hidesOthers; -+ c->_addsToRecentItems = _addsToRecentItems; -+ c->_createsNewApplicationInstance = _createsNewApplicationInstance; -+ c->_allowsRunningApplicationSubstitution = _allowsRunningApplicationSubstitution; -+ c->_forPrinting = _forPrinting; c->_promptsUserIfNeeded = _promptsUserIfNeeded; -+ c->_requiresUniversalLinks = _requiresUniversalLinks; c->_architecture = _architecture; -+ c->_appleEvent = [_appleEvent retain]; -+ c->_arguments = [_arguments copy]; c->_environment = [_environment copy]; -+ return c; -+} -+- (void) dealloc { -+ [_appleEvent release]; -+ [_arguments release]; -+ [_environment release]; -+ [super dealloc]; -+} -+@end -+ - NSString *const NSWorkspaceWillPowerOffNotification = - @"NSWorkspaceWillPowerOffNotification"; - -@@ -161,6 +210,24 @@ NSNotificationName NSWorkspaceWillUnmountNotification = @"NSWorkspaceWillUnmount - return 0; - } - -+- (void) openURL: (NSURL *) url -+ configuration: (NSWorkspaceOpenConfiguration *) configuration -+ completionHandler: (void (^)(id application, NSError *error)) completionHandler -+{ -+ // Opening may wait on another process; keep it off the caller's thread. -+ dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ -+ BOOL opened = [self openURL: url]; -+ if (completionHandler != NULL) { -+ NSError *error = nil; -+ if (!opened) -+ error = [NSError errorWithDomain: NSCocoaErrorDomain code: 256 userInfo: nil]; -+ dispatch_async(dispatch_get_main_queue(), ^{ -+ completionHandler(nil, error); -+ }); -+ } -+ }); -+} -+ - - (BOOL) selectFile: (NSString *) path - inFileViewerRootedAtPath: (NSString *) rootedAtPath - { -@@ -317,16 +384,3 @@ NSNotificationName NSWorkspaceWillUnmountNotification = @"NSWorkspaceWillUnmount - } - - @end -- --static dispatch_once_t _initOnceNsWorkspaceOpenConfig; --static NSWorkspaceOpenConfiguration* _singletonNsWorkspaceOpenConfig; -- --@implementation NSWorkspaceOpenConfiguration --+ (instancetype)configuration { -- dispatch_once(&_initOnceNsWorkspaceOpenConfig, ^{ -- _singletonNsWorkspaceOpenConfig = [[NSWorkspaceOpenConfiguration alloc] init]; -- }); -- return _singletonNsWorkspaceOpenConfig; --} -- --@end -\ No newline at end of file -diff --git a/AppKit/NSGeometryTrace.h b/AppKit/NSGeometryTrace.h -new file mode 100644 -index 00000000..a51a9241 ---- /dev/null -+++ b/AppKit/NSGeometryTrace.h -@@ -0,0 +1,25 @@ -+/* Opt-in runtime geometry diagnostics; never log view text or client data. */ -+#ifndef DARLING_NS_GEOMETRY_TRACE_H -+#define DARLING_NS_GEOMETRY_TRACE_H -+#include -+#include -+#include -+ -+static inline void DarlingGeometryTrace(const char *format, ...) { -+ if (!getenv("DARLING_UI_GEOMETRY_TRACE")) -+ return; -+ va_list args, copy; -+ va_start(args, format); -+ va_copy(copy, args); -+ vfprintf(stderr, format, args); -+ va_end(args); -+ /* Clients may redirect stderr to a non-log descriptor. Keep a copy in -+ * the guest's temporary directory so their resize events remain visible. */ -+ FILE *file = fopen("/tmp/darling-ui-geometry.log", "a"); -+ if (file) { -+ vfprintf(file, format, copy); -+ fclose(file); -+ } -+ va_end(copy); -+} -+#endif diff --git a/tools/test-x11-relative-mouse.c b/tools/test-x11-relative-mouse.c new file mode 100644 index 0000000..489701c --- /dev/null +++ b/tools/test-x11-relative-mouse.c @@ -0,0 +1,139 @@ +#include "../build/darling-src/src/external/cocotron/AppKit/X11.backend/X11RelativeMouse.h" +#include +#include + +/* Host model of the X11 backend's relative mouse mode (CGAssociateMouseAndMouseCursorPosition(false)). + * A warp's echo is simulated as the MotionNotify the server queues at the target; `queued` is the number of + * X events queued after the backend's XSync. */ +static X11RelativeMouseMotion motion(X11RelativeMouse *m, int x, int y, bool honoured) { + return X11RelativeMouseHandleMotion(m, x, y, honoured); +} + +static void xorgHonouredWarps(void) { + X11RelativeMouse m = {0}; + X11RelativeMouseBegin(&m, 500, 400); + /* Physical motion: delta reported, the cursor stays frozen, the pointer is warped back. */ + X11RelativeMouseMotion r = motion(&m, 510, 395, true); + assert(r.post && !r.echo && r.deltaX == 10 && r.deltaY == -5); + assert(r.rewarp && r.warpX == 500 && r.warpY == 400); + X11RelativeMouseExpectEcho(&m, r.warpX, r.warpY, 1); + /* The rewarp's echo is consumed and is not a delta back to the centre. */ + r = motion(&m, 500, 400, true); + assert(!r.post && r.echo && !r.rewarp && m.echoCount == 0); + assert(m.physicalX == 500 && m.physicalY == 400); + r = motion(&m, 503, 404, true); + assert(r.post && r.deltaX == 3 && r.deltaY == 4 && r.rewarp); + X11RelativeMouseExpectEcho(&m, r.warpX, r.warpY, 1); + + /* A real motion queued before the echo still measures against the last physical position. */ + X11RelativeMouseBegin(&m, 500, 400); + r = motion(&m, 520, 400, true); + X11RelativeMouseExpectEcho(&m, r.warpX, r.warpY, 2); /* the next motion was already queued */ + r = motion(&m, 530, 400, true); + assert(r.post && r.deltaX == 10 && r.deltaY == 0 && r.rewarp); + X11RelativeMouseExpectEcho(&m, r.warpX, r.warpY, 1); /* same target: merged, not a second echo */ + assert(m.echoCount == 1); + r = motion(&m, 500, 400, true); + assert(r.echo && m.echoCount == 0); + r = motion(&m, 499, 400, true); + assert(r.post && r.deltaX == -1); + + /* The application warps from the event it was given while our rewarp's echo is still queued: + * both echoes are dropped in order and neither cancels the motion. */ + X11RelativeMouseBegin(&m, 500, 400); + r = motion(&m, 512, 400, true); + assert(r.deltaX == 12 && r.rewarp); + X11RelativeMouseExpectEcho(&m, r.warpX, r.warpY, 1); + X11RelativeMouseWarp(&m, 524.0, 400.0, 2); /* game: mouseLocation + 2 * delta */ + assert(m.virtualX == 524.0 && m.physicalX == 512); + r = motion(&m, 500, 400, true); + assert(r.echo && m.physicalX == 500); + r = motion(&m, 524, 400, true); + assert(r.echo && m.physicalX == 524 && m.echoCount == 0); + r = motion(&m, 530, 401, true); + assert(r.post && r.deltaX == 6 && r.deltaY == 1 && r.rewarp && r.warpX == 524 && r.warpY == 400); + puts("PASS: Xorg honoured warps rewarp to the frozen cursor and drop their echoes"); +} + +static void xwaylandVisibleCursor(void) { + X11RelativeMouse m = {0}; + X11RelativeMouseBegin(&m, 800, 450); + /* Warps are not honoured: no rewarp, the pointer moves freely, deltas follow it. */ + X11RelativeMouseMotion r = motion(&m, 810, 450, false); + assert(r.post && r.deltaX == 10 && !r.rewarp); + X11RelativeMouseWarp(&m, 820.0, 450.0, 1); + /* The echo at the target is dropped without moving the physical position... */ + r = motion(&m, 820, 450, false); + assert(r.echo && !r.post && m.physicalX == 810 && m.physicalY == 450); + /* ...so the next real motion, at the real absolute position, gives the true delta. */ + r = motion(&m, 812, 453, false); + assert(r.post && r.deltaX == 2 && r.deltaY == 3 && !r.rewarp); + assert(m.virtualX == 820.0 && m.virtualY == 450.0); + /* An expected echo that never arrives expires after the queued events and cannot eat a later motion. */ + X11RelativeMouseWarp(&m, 700.0, 400.0, 1); + X11RelativeMouseTick(&m); /* some other X event was the one queued */ + assert(m.echoCount == 0); + r = motion(&m, 700, 400, false); + assert(r.post && !r.echo && r.deltaX == -112 && r.deltaY == -53); + puts("PASS: Xwayland visible cursor keeps the physical position across unhonoured warp echoes"); +} + +static void gameWarpUpdatesVirtual(void) { + X11RelativeMouse m = {0}; + X11RelativeMouseBegin(&m, 100, 100); + X11RelativeMouseWarp(&m, 150.75, 120.5, 0); /* no event: the pointer was already there */ + assert(m.virtualX == 150.75 && m.virtualY == 120.5 && m.echoCount == 0); + assert(m.physicalX == 100 && m.physicalY == 100); + /* mouseLocation reports the virtual cursor, Cocoa bottom-up global, for a 1080-pixel screen. */ + double cocoaX = m.virtualX, cocoaY = 1080 - m.virtualY; + assert(cocoaX == 150.75 && cocoaY == 959.5); + X11RelativeMouseMotion r = motion(&m, 101, 100, true); + assert(r.post && r.deltaX == 1 && r.rewarp && r.warpX == 150 && r.warpY == 120); + puts("PASS: application warps while relative move the virtual cursor that mouseLocation returns"); +} + +static void deltaSign(void) { + X11RelativeMouse m = {0}; + X11RelativeMouseBegin(&m, 0, 0); + X11RelativeMouseMotion r = motion(&m, 0, 7, false); + assert(r.deltaY == 7); /* X root y grows downwards: positive deltaY means down, as on macOS */ + double dx, dy; + /* Absolute motion in Cocoa window coordinates (bottom-up): moving down lowers y. */ + X11AbsoluteMouseDelta(10, 300, 14, 290, &dx, &dy); + assert(dx == 4 && dy == 10); + puts("PASS: deltaY is positive downwards for relative and absolute motion"); +} + +static void ungrabRestores(void) { + X11RelativeMouse m = {0}; + X11RelativeMouseBegin(&m, 10, 10); + X11RelativeMouseWarp(&m, 40, 40, 3); + X11RelativeMouseEnd(&m); + assert(!m.active && m.echoCount == 0); + X11RelativeMouseBegin(&m, 60, 70); + assert(m.active && m.virtualX == 60 && m.virtualY == 70 && m.physicalX == 60 && m.physicalY == 70); + X11RelativeMouseMotion r = motion(&m, 40, 40, true); + assert(r.post && !r.echo && r.deltaX == -20 && r.deltaY == -30); + puts("PASS: ungrab clears expected echoes and a new grab starts from the current pointer"); +} + +static void echoQueueBound(void) { + X11RelativeMouse m = {0}; + X11RelativeMouseBegin(&m, 0, 0); + for (int i = 1; i <= X11_RELATIVE_MOUSE_MAX_ECHOES + 3; ++i) + X11RelativeMouseWarp(&m, i, 0, 100); + assert(m.echoCount == X11_RELATIVE_MOUSE_MAX_ECHOES && m.echoes[0].x == 4); + X11RelativeMouseMotion r = motion(&m, 6, 0, true); + assert(r.echo && m.echoCount == X11_RELATIVE_MOUSE_MAX_ECHOES - 3 && m.echoes[0].x == 7); + puts("PASS: expected echoes are bounded and consumed in order"); +} + +int main(void) { + xorgHonouredWarps(); + xwaylandVisibleCursor(); + gameWarpUpdatesVirtual(); + deltaSign(); + ungrabRestores(); + echoQueueBound(); + return 0; +}