From ed9cc8310aeeb7e91cb50c0929c03fded97d50b5 Mon Sep 17 00:00:00 2001 From: lucannez64 <58551020+lucannez64@users.noreply.github.com> Date: Fri, 16 Oct 2026 17:13:11 +0200 Subject: [PATCH] Restore X11 fullscreen requests for monitor-sized borderless windows Port the fullscreen policy omitted when adapting PR #12. Track requested fullscreen state across frame and style changes, exclude unmanaged windows, and use EWMH REMOVE when leaving fullscreen. Extend the native geometry probe and document passive tracing for the reported vertical click offset, whose cause remains unconfirmed. --- docs/TESTING.md | 10 +- docs/TROUBLESHOOTING.md | 30 ++++++ patches/cocotron.patch | 143 ++++++++++++++++++++++++---- tools/check-x11-display-geometry.py | 5 +- tools/riot-display-geometry-check.m | 45 +++++++++ 5 files changed, 209 insertions(+), 24 deletions(-) diff --git a/docs/TESTING.md b/docs/TESTING.md index 6fff517..fe6ed65 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -434,7 +434,7 @@ build/test-x11-relative-mouse The native probe requires the existing per-user AppKit and CoreGraphics overlays and a running X11/Xwayland display. It rebuilds the changed X11 and CoreGraphics objects and tests them in a temporary framework overlay, without installing them. -Six `DISPLAY_GEOMETRY ...=pass` lines are required; full output is saved in +Seven `DISPLAY_GEOMETRY ...=pass` lines are required; full output is saved in `build/pr12-display-check.log`. It checks the live primary screen's origin, synthetic single-monitor and offset @@ -445,8 +445,12 @@ relative deltas, both RandR cache-invalidation events, and the root-screen fallb Warps are intercepted: the probe does not move the desktop pointer or send game input. Synthetic layouts exercise coordinate conversion and display API behavior; they do not simulate physical monitor hotplug or validate League rendering. -Fullscreen policy is unchanged. Manually verify game launch, window placement, -click alignment and edge panning on the affected monitor layout before rollout. +The probe also checks that managed borderless windows covering a whole monitor +request EWMH fullscreen, including on secondary monitors, and clear the property +when resized before mapping. Titled windows, unmanaged popups and windows sized +to the panel-reduced work area are excluded. Mapped-window transitions and panel +stacking require manual verification under KDE Plasma/Xwayland. Also verify game +launch, window placement, click alignment and edge panning before rollout. After installing the tested X11 and CoreGraphics binaries locally, the user reported that everything was working (2026-10-16). Fullscreen policy was unchanged. diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 75b1c4d..4b54432 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -236,6 +236,36 @@ The backend's relative mouse mode (`CGAssociateMouseAndMouseCursorPosition(false 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. +## Click offset grows towards the bottom of the screen + +A growing vertical offset has been reported on KDE Plasma/Wayland with Xwayland. +The cause is unconfirmed, including whether it is related to commit `426fb75`. +The borderless fullscreen request has been restored separately; retest alignment +once the game covers the panel. + +If it persists, enable the passive input trace before the next manual game launch: + +```sh +darling shell touch /tmp/rclol-input-trace +``` + +The flag is checked once per process, so enable it before launching the game. +During a short manual test, compare clicks near the top, middle and bottom, and +note the direction and approximate size of the error. Include the in-game +resolution and window mode, Plasma's scale setting for each monitor, and the +host output of `xrandr --listmonitors`. Collect the guest log with: + +```sh +darling shell cat /tmp/rclol-input-trace.log > rclol-input-trace.log +darling shell rm -f /tmp/rclol-input-trace +``` + +Removing the flag disables tracing for subsequently started processes. The log +records motion, warps, pointer-position queries and relative-mode transitions; +it can establish whether the relative path is active, but cannot by itself +establish where the game rendered a clicked target. If practical, compare the +same resolution in a smaller window and in borderless fullscreen. + ## Cursor stops changing shape in fullscreen On Hyprland the game's cursor can keep its first shape (for example, it never switches to the attack cursor) until you leave and re-enter fullscreen. Darling sends every change to the X server; what freezes the shape on screen is Hyprland's direct scanout of the fullscreen Xwayland window. Turn it off in `hyprland.conf`: diff --git a/patches/cocotron.patch b/patches/cocotron.patch index 65733bd..376a448 100644 --- a/patches/cocotron.patch +++ b/patches/cocotron.patch @@ -3950,7 +3950,7 @@ index 0e91a5d1..1e4cec1b 100644 - (void) selectionNotify: (XSelectionEvent *) event { diff --git a/AppKit/X11.backend/X11Window.h b/AppKit/X11.backend/X11Window.h -index bfe3839a..dbc974a0 100644 +index bfe3839a..e8c8828f 100644 --- a/AppKit/X11.backend/X11Window.h +++ b/AppKit/X11.backend/X11Window.h @@ -26,7 +26,7 @@ @@ -3962,8 +3962,12 @@ index bfe3839a..dbc974a0 100644 @interface X11Window : CGWindow { int _level; // TODO: care about this value -@@ -49,6 +49,26 @@ +@@ -47,8 +47,30 @@ + BOOL _mapped; + CGPoint _lastMotionPos; BOOL _isModal; ++ BOOL _overrideRedirect; ++ BOOL _fullscreenRequested; BOOL _isOpaque; BOOL _hasShadow; + NSTimer *_remoteFrameTimer; @@ -3989,7 +3993,7 @@ index bfe3839a..dbc974a0 100644 @public XIC _xic; -@@ -60,9 +80,9 @@ +@@ -60,9 +82,9 @@ - (Visual *) visual; - (Drawable) drawable; - (NSPoint) transformPoint: (NSPoint) pos; @@ -4001,7 +4005,7 @@ index bfe3839a..dbc974a0 100644 - (void) frameChanged; - (void) setLastKnownCursorPosition: (CGPoint) point; diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m -index f4925b66..6f653f6d 100644 +index f4925b66..959047d7 100644 --- a/AppKit/X11.backend/X11Window.m +++ b/AppKit/X11.backend/X11Window.m @@ -38,6 +38,34 @@ @@ -4048,7 +4052,7 @@ index f4925b66..6f653f6d 100644 BOOL isPanel = [delegate isKindOfClass: [NSPanel class]]; if (isPanel && _styleMask & NSDocModalWindowMask) _styleMask = NSBorderlessWindowMask; -@@ -193,8 +221,13 @@ static NSData *makeWindowIcon() { +@@ -193,8 +221,14 @@ static NSData *makeWindowIcon() { XSetWindowAttributes xattr; unsigned long xattr_mask; @@ -4061,10 +4065,11 @@ index f4925b66..6f653f6d 100644 + ([delegate isKindOfClass:[NSMenuWindow class]] || + [delegate isKindOfClass:[NSPopUpWindow class]] || + ![delegate canBecomeKeyWindow]) ? True : False; ++ _overrideRedirect = xattr.override_redirect; xattr_mask = CWOverrideRedirect | CWColormap; xattr.colormap = cmap; -@@ -206,6 +239,12 @@ static NSData *makeWindowIcon() { +@@ -206,6 +240,12 @@ static NSData *makeWindowIcon() { (_visualInfo == NULL) ? CopyFromParent : _visualInfo->visual, xattr_mask, &xattr); @@ -4077,7 +4082,7 @@ index f4925b66..6f653f6d 100644 [self syncDelegateProperties]; Atom atm = XInternAtom(_display, "WM_DELETE_WINDOW", False); -@@ -266,6 +305,11 @@ static NSData *makeWindowIcon() { +@@ -266,6 +306,11 @@ static NSData *makeWindowIcon() { [self setWindowIcon]; @@ -4089,7 +4094,103 @@ index f4925b66..6f653f6d 100644 return self; } -@@ -338,7 +382,6 @@ static NSData *makeWindowIcon() { +@@ -279,24 +324,23 @@ static NSData *makeWindowIcon() { + return _styleMask; + } + +-- (void) setStyleMaskInternal: (NSUInteger) mask force: (BOOL) force { +- if (force || (mask & NSWindowStyleMaskResizable) != +- (_styleMask & NSWindowStyleMaskResizable)) { +- XSizeHints *sh = XAllocSizeHints(); +- if (mask & NSWindowStyleMaskResizable) { +- // 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; +- } +- +- XSetWMSizeHints(_display, _window, sh, XA_WM_NORMAL_HINTS); +- XFree(sh); ++/* A borderless application covering a monitor needs EWMH fullscreen so the ++ * compositor places it above desktop panels. Compare full monitor bounds, ++ * never the work area, and leave unmanaged menus/popups alone. */ ++- (BOOL) wantsFullscreenWithStyleMask: (NSUInteger) mask rootFrame: (O2Rect) frame { ++ if (_overrideRedirect) return NO; ++ if (mask & NSWindowStyleMaskFullScreen) return YES; ++ if (mask & NSTitledWindowMask) return NO; ++ X11Display *display = (X11Display *) [NSDisplay currentDisplay]; ++ for (NSScreen *screen in [display screens]) { ++ if (!NSIsEmptyRect([screen frame]) && ++ NSEqualRects([display rootRectForFrame: [screen frame]], frame)) ++ return YES; + } ++ return NO; ++} + ++- (void) requestFullscreen: (BOOL) fullscreen force: (BOOL) force { + if (!_mapped) { + long states[2]; + int states_cnt = 0; +@@ -304,7 +348,7 @@ static NSData *makeWindowIcon() { + states[states_cnt++] = + (long) XInternAtom(_display, "_NET_WM_STATE_MODAL", False); + } +- if (mask & NSWindowStyleMaskFullScreen) { ++ if (fullscreen) { + states[states_cnt++] = (long) XInternAtom( + _display, "_NET_WM_STATE_FULLSCREEN", False); + } +@@ -312,14 +356,13 @@ static NSData *makeWindowIcon() { + XInternAtom(_display, "_NET_WM_STATE", False), XA_ATOM, + 32, PropModeReplace, (const unsigned char *) states, + states_cnt); +- } else if (force || ((mask & NSWindowStyleMaskFullScreen) != +- (_styleMask & NSWindowStyleMaskFullScreen))) { ++ } else if (force || fullscreen != _fullscreenRequested) { + XClientMessageEvent event = {0}; + event.type = ClientMessage; + event.window = _window; + event.message_type = XInternAtom(_display, "_NET_WM_STATE", False); + event.format = 32; +- event.data.l[0] = (mask & NSWindowStyleMaskFullScreen) ? 1 : 2; ++ event.data.l[0] = fullscreen ? 1 : 0; /* EWMH ADD / REMOVE, not TOGGLE */ + event.data.l[1] = + (long) XInternAtom(_display, "_NET_WM_STATE_FULLSCREEN", False); + event.data.l[3] = 1; +@@ -327,6 +370,29 @@ static NSData *makeWindowIcon() { + SubstructureNotifyMask | SubstructureRedirectMask, + (XEvent *) &event); + } ++ _fullscreenRequested = fullscreen; ++} ++ ++- (void) setStyleMaskInternal: (NSUInteger) mask force: (BOOL) force { ++ if (force || (mask & NSWindowStyleMaskResizable) != ++ (_styleMask & NSWindowStyleMaskResizable)) { ++ XSizeHints *sh = XAllocSizeHints(); ++ if (mask & NSWindowStyleMaskResizable) { ++ // 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; ++ } ++ ++ XSetWMSizeHints(_display, _window, sh, XA_WM_NORMAL_HINTS); ++ XFree(sh); ++ } ++ ++ [self requestFullscreen: [self wantsFullscreenWithStyleMask: mask rootFrame: _frame] ++ force: force]; + } + + - (void) setStyleMask: (NSUInteger) mask { +@@ -338,7 +404,6 @@ static NSData *makeWindowIcon() { } + (void) removeDecorationForWindow: (Window) w onDisplay: (Display *) dpy { @@ -4097,7 +4198,7 @@ index f4925b66..6f653f6d 100644 struct { unsigned long flags; unsigned long functions; -@@ -358,6 +401,7 @@ static NSData *makeWindowIcon() { +@@ -358,6 +423,7 @@ static NSData *makeWindowIcon() { if (!_mapped) { XMapWindow(_display, _window); _mapped = YES; @@ -4105,7 +4206,7 @@ index f4925b66..6f653f6d 100644 } } -@@ -377,15 +421,27 @@ static NSData *makeWindowIcon() { +@@ -377,15 +443,27 @@ static NSData *makeWindowIcon() { - (void) syncDelegateProperties { long mask = KeyPressMask | KeyReleaseMask | ExposureMask | StructureNotifyMask | EnterWindowMask | LeaveWindowMask | @@ -4138,7 +4239,7 @@ index f4925b66..6f653f6d 100644 // TODO: background color } -@@ -393,6 +449,28 @@ static NSData *makeWindowIcon() { +@@ -393,6 +471,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. @@ -4167,7 +4268,7 @@ index f4925b66..6f653f6d 100644 [_context release]; _context = nil; -@@ -421,6 +499,388 @@ static NSData *makeWindowIcon() { +@@ -421,6 +521,388 @@ static NSData *makeWindowIcon() { } } @@ -4556,12 +4657,15 @@ index f4925b66..6f653f6d 100644 - (Window) windowHandle { return _window; } -@@ -473,11 +933,20 @@ static NSData *makeWindowIcon() { +@@ -473,11 +955,24 @@ static NSData *makeWindowIcon() { } - (void) setFrame: (O2Rect) frame { - frame = [self transformFrame: frame]; + frame = [(X11Display *) [NSDisplay currentDisplay] rootRectForFrame: frame]; ++ BOOL fullscreen = [self wantsFullscreenWithStyleMask: _styleMask rootFrame: frame]; ++ // Release the WM's fullscreen size before requesting a smaller frame. ++ if (!fullscreen) [self requestFullscreen: NO force: NO]; XMoveResizeWindow(_display, _window, frame.origin.x, frame.origin.y, frame.size.width, frame.size.height); [self invalidateContextWithNewSize: frame.size]; @@ -4575,10 +4679,11 @@ index f4925b66..6f653f6d 100644 + hints.min_height = hints.max_height = frame.size.height; + XSetWMSizeHints(_display, _window, &hints, XA_WM_NORMAL_HINTS); + } ++ if (fullscreen) [self requestFullscreen: YES force: NO]; } - (void) setHasShadow: (BOOL) value { -@@ -488,6 +957,25 @@ static NSData *makeWindowIcon() { +@@ -488,6 +983,25 @@ static NSData *makeWindowIcon() { _level = value; } @@ -4604,7 +4709,7 @@ index f4925b66..6f653f6d 100644 - (void) setOpaque: (BOOL) value { _isOpaque = value; } -@@ -559,7 +1047,29 @@ static NSData *makeWindowIcon() { +@@ -559,7 +1073,29 @@ static NSData *makeWindowIcon() { XRaiseWindow(_display, _window); } @@ -4634,7 +4739,7 @@ index f4925b66..6f653f6d 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 +1151,12 @@ static NSData *makeWindowIcon() { } - (void) openGLFlushBuffer { @@ -4647,7 +4752,7 @@ index f4925b66..6f653f6d 100644 CGLError error; CGLContextObj prevContext = CGLGetCurrentContext(); -@@ -652,7 +1168,7 @@ static NSData *makeWindowIcon() { +@@ -652,7 +1194,7 @@ static NSData *makeWindowIcon() { } - (O2Rect) frame { @@ -4656,7 +4761,7 @@ index f4925b66..6f653f6d 100644 } static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) { -@@ -666,6 +1182,7 @@ static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) { +@@ -666,6 +1208,7 @@ static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) { } - (void) frameChanged { @@ -4664,7 +4769,7 @@ index f4925b66..6f653f6d 100644 XErrorHandler previousHandler = XSetErrorHandler(ignoreBadWindow); @try { Window root, parent; -@@ -710,14 +1227,6 @@ static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) { +@@ -710,14 +1253,6 @@ static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) { [_deviceDictionary addEntriesFromDictionary: entries]; } diff --git a/tools/check-x11-display-geometry.py b/tools/check-x11-display-geometry.py index d84fa5d..bde1a1c 100644 --- a/tools/check-x11-display-geometry.py +++ b/tools/check-x11-display-geometry.py @@ -41,7 +41,8 @@ subprocess.run(['clang', *flags_for(frameworks / 'AppKit', 'X11_backend'), subprocess.run(['clang', '-target', 'x86_64-apple-macosx11.0', '-fuse-ld=lld', '-nostdlib', '-isysroot', config['sdk'], str(probe) + '.o', '-lSystem', '-framework', 'AppKit', '-framework', 'Foundation', - '-framework', 'CoreGraphics', '-lobjc', '-o', str(probe)], check=True) + '-framework', 'CoreGraphics', '-lobjc', + '-L' + str(Path(config['sdk']) / 'usr/lib/native'), '-lX11', '-o', str(probe)], check=True) original = Path(config['prefix']) / config['guest_home'].lstrip('/') / 'RuntimeFrameworks' with tempfile.TemporaryDirectory(prefix='pr12-display-frameworks-', dir=root / 'build') as directory: @@ -66,5 +67,5 @@ with tempfile.TemporaryDirectory(prefix='pr12-display-frameworks-', dir=root / ' output.seek(0) lines = [line.rstrip() for line in output if line.startswith('DISPLAY_GEOMETRY')] print('\n'.join(lines)) - if result.returncode or len(lines) != 6: + if result.returncode or len(lines) != 7: raise SystemExit(f'Display probe failed ({result.returncode}); inspect {log}') diff --git a/tools/riot-display-geometry-check.m b/tools/riot-display-geometry-check.m index a457890..c4445ca 100644 --- a/tools/riot-display-geometry-check.m +++ b/tools/riot-display-geometry-check.m @@ -8,6 +8,7 @@ #import "../build/darling-src/src/external/cocotron/AppKit/X11.backend/X11Display.h" #import "../build/darling-src/src/external/cocotron/AppKit/X11.backend/X11Window.h" #include +#include #include #include @@ -20,6 +21,27 @@ extern CGError CGWarpMouseCursorPosition(CGPoint position); - (void)postXEvent:(XEvent *)event; @end +@interface X11Window (FullscreenProbe) +- (BOOL)wantsFullscreenWithStyleMask:(NSUInteger)mask rootFrame:(O2Rect)frame; +@end +@interface FullscreenProbeWindow : NSWindow +@end +@implementation FullscreenProbeWindow +- (BOOL)canBecomeKeyWindow { return YES; } +@end +static BOOL fullscreenProperty(X11Display *display, X11Window *window) { + Display *d = [display display]; + Atom actual, fs = XInternAtom(d,"_NET_WM_STATE_FULLSCREEN",False); + int format; unsigned long count, remaining; unsigned char *data = NULL; + assert(XGetWindowProperty(d,[window windowHandle],XInternAtom(d,"_NET_WM_STATE",False), + 0,32,False,XA_ATOM,&actual,&format,&count,&remaining,&data) == Success); + BOOL found = NO; + assert(actual == XA_ATOM && format == 32 && remaining == 0); + for (unsigned long i=0; i