From 3142128575bd1c6824578f588f882cd35bb2dcbe Mon Sep 17 00:00:00 2001 From: lucannez64 <58551020+lucannez64@users.noreply.github.com> Date: Mon, 19 Oct 2026 01:10:03 +0200 Subject: [PATCH 1/3] Fix Metal VSync propagation to window swap intervals --- docs/TESTING.md | 20 +++- docs/TROUBLESHOOTING.md | 15 +++ patches/cocotron.patch | 44 ++++++-- patches/darling.patch | 58 +++++------ tests/test_metal_vsync.py | 139 +++++++++++++++++++++++++ tools/riot-metal-presentation-check.mm | 6 ++ 6 files changed, 243 insertions(+), 39 deletions(-) create mode 100644 tests/test_metal_vsync.py diff --git a/docs/TESTING.md b/docs/TESTING.md index b1d7df4..7ab764c 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -167,7 +167,7 @@ Useful probes (build tokenized Python helpers from `build/runtime/tools/`): - `riot-event-coalescing-check`: preserve key events and latest mouse motion. - `m327-cgl-resolver-check`: first calls through dynamically looked-up GL functions retain floating-point arguments; expected `first_sampler_linear=1 first_color_arguments=1 errors=0,0`. Build with `build-m58f327-opengl.py` and run with `run-riot-native-check.py m327-cgl-resolver-check`. - `m327-cgl-direct-check`: with `RCLOL_GPU_DIRECT_OTHER_WINDOW_TEST=1` and an owned 337x339 native target, interleave 338x339 fallback surfaces; expected `active_presenter_preserved=1 fallback_frames=357`. This needs the native presentation fixture; it is not a `run-riot-native-check.py` choice. -- `riot-metal-presentation-check`: Vulkan clear reaches the GL texture, including late window/context attachment. Set `RIOT_PRESENTATION_NESTED=1` to place the Metal layer beneath two ordinary layers and verify that its drawables are still presented and recycled past the three-drawable pool limit. +- `riot-metal-presentation-check`: Vulkan clear reaches the GL texture, including late window/context attachment. Each frame toggles `displaySyncEnabled` and checks the CGL window swap interval. Set `RIOT_PRESENTATION_NESTED=1` to place the Metal layer beneath two ordinary layers and verify that its drawables are still presented and recycled past the three-drawable pool limit. - `riot-ca-render-policy-check`: headless check of the actual compositor render policy. A nested Metal layer must still be prepared when a full-window IOSurface is present; surface-only trees should retain the local-render shortcut. Built by `build-riot-metal-presentation.py`; expected `PASS: mixed-tree Metal preparation survives IOSurface shortcut`. Creates no window or GPU work and does not publish its test surface. - `riot-metal-concurrency-check`: submission ordering, resource lifetime and concurrent recording. @@ -533,3 +533,21 @@ 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. + +## Metal VSync regression (#46) + +```sh +python3 -m unittest discover -s tests -p 'test_metal_vsync.py' +``` + +These host checks compile the patched layer-tree policy, flush body and CGL +swap methods against inert fixtures. They cover nested layers, VSync toggles, +shared-window requests, independent contexts, late attachment and failed +context/interval changes. They do not measure physical display refresh. + +For manual validation after updating with the clients closed, compare the +same scene with in-game VSync on and off. With VSync on, presentation should +follow the display/compositor cadence; with it off, the game's frame cap +should continue to work. Record monitor refresh, compositor, X11/Xwayland, +GPU/driver and window mode. The window presentation probe above checks the +API wiring; an in-game/compositor timing comparison remains necessary. diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 3574291..b7d57fd 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -541,3 +541,18 @@ Recovery requires every matching client to be at least 60 seconds old and a complete Hyprland window record (`class`, `title`, `mapped`, `hidden`). A second process scan after window inspection must find the same client identities and no game. Missing information or shutdown failure leaves normal launch in charge. + +## VSync and frame caps (#46) + +The Metal layer's `displaySyncEnabled` property previously had no effect on +the window swap interval. A game that delegates pacing to VSync could run +uncapped when VSync was enabled. The compositor now applies that property to +the window before swapping, including nested Metal layers and setting changes. +If several Metal layers share a window, any layer requesting VSync enables it +for that window. Other windows retain independent swap settings. + +Exit the game and both clients, then run `./scripts/update.sh` to rebuild and +install the QuartzCore and OpenGL changes. Until updated, disable in-game +VSync to use the game's frame cap, as the issue reporter confirmed. Physical +scanout timing still depends on the driver and desktop compositor; the fix +has standalone regression coverage but needs an in-game timing retest. diff --git a/patches/cocotron.patch b/patches/cocotron.patch index 72d30f8..828ecc3 100644 --- a/patches/cocotron.patch +++ b/patches/cocotron.patch @@ -9418,13 +9418,14 @@ index 5dcac115..5de4f03f 100644 + @end diff --git a/QuartzCore/CALayerContext.m b/QuartzCore/CALayerContext.m -index 5dce8c5..3a7f937 100644 +index 5dce8c56..8e636724 100644 --- a/QuartzCore/CALayerContext.m +++ b/QuartzCore/CALayerContext.m -@@ -1,47 +1,77 @@ +@@ -1,47 +1,92 @@ #import +#import #import ++#import #import #import #import "CALayerInternal.h" @@ -9446,6 +9447,20 @@ index 5dce8c5..3a7f937 100644 + return NO; +} + ++/* One window has one swap interval. Any synchronized Metal layer requests ++ * VSync for that window; -1 leaves non-Metal windows on their existing policy. ++ * Re-evaluate at flush so toggles and nested/replaced layers take effect. */ ++static GLint metalSwapInterval(CALayer *layer) { ++ GLint interval = -1; ++ if ([[layer class] isSubclassOfClass: [CAMetalLayerInternal class]]) ++ interval = [(CAMetalLayer *)layer displaySyncEnabled] ? 1 : 0; ++ for (CALayer *child in layer.sublayers) { ++ GLint childInterval = metalSwapInterval(child); ++ if (childInterval > interval) interval = childInterval; ++ } ++ return interval; ++} ++ +/* The X11 transport presents Chromium's flattened full-window surface. + * Avoid uploading and compositing that same window again into a covered GL + * child. Restrict this to the explicitly configured native presentation path. */ @@ -9518,7 +9533,7 @@ index 5dce8c5..3a7f937 100644 [super dealloc]; } -@@ -51,12 +81,22 @@ +@@ -51,12 +96,22 @@ } - (void) setLayer: (CALayer *) layer { @@ -9541,7 +9556,7 @@ index 5dce8c5..3a7f937 100644 } - (void) setSubwindow: (CGSubWindow*) subwindow -@@ -68,16 +108,50 @@ +@@ -68,16 +123,50 @@ } _subwindow = [subwindow retain]; @@ -9593,7 +9608,7 @@ index 5dce8c5..3a7f937 100644 } - (void) assignTextureIdsToLayerTree: (CALayer *) layer { -@@ -93,8 +167,27 @@ +@@ -93,8 +182,27 @@ [self assignTextureIdsToLayerTree: child]; } @@ -9621,7 +9636,7 @@ index 5dce8c5..3a7f937 100644 glEnable(GL_DEPTH_TEST); glShadeModel(GL_SMOOTH); -@@ -118,27 +211,62 @@ +@@ -118,27 +226,62 @@ [_deleteTextureIds removeAllObjects]; @@ -9689,7 +9704,7 @@ index 5dce8c5..3a7f937 100644 } - (void) startTimerIfNeeded { -@@ -147,7 +275,7 @@ +@@ -147,7 +290,7 @@ target: self selector: @selector(timer:) userInfo: nil @@ -9698,14 +9713,25 @@ index 5dce8c5..3a7f937 100644 } - (void) deleteTextureId: (NSNumber *) textureId { -@@ -155,7 +283,9 @@ +@@ -155,7 +298,20 @@ } - (void) flush { - CGLFlushDrawable(_glContext); + if(skipLocalRender(_layer, _frame.size))return; -+ if (_glContext != NULL && _cglWindow != NULL) ++ if (_glContext != NULL && _cglWindow != NULL) { ++ CGLContextObj previous = CGLGetCurrentContext(); ++ // EGL applies the interval to the current draw surface, so attach this ++ // window before setting it (including after late window attachment). ++ if (CGLContextMakeCurrentAndAttachToWindow(_glContext, _cglWindow) != kCGLNoError) ++ return; ++ GLint interval = metalSwapInterval(_layer); ++ if (interval >= 0) ++ CGLSetParameter(_glContext, kCGLCPSwapInterval, &interval); + CGLFlushDrawable(_glContext); ++ if (previous != _glContext) ++ CGLSetCurrentContext(previous); ++ } } @end diff --git a/patches/darling.patch b/patches/darling.patch index 5194fda..5508e49 100644 --- a/patches/darling.patch +++ b/patches/darling.patch @@ -6257,7 +6257,7 @@ index deca16b87..d75d5e22d 100644 set_property(TARGET OpenGL APPEND_STRING PROPERTY LINK_FLAGS diff --git a/src/frameworks/OpenGL/OpenGL.c b/src/frameworks/OpenGL/OpenGL.c -index ffd7fe054..f25fd9aa5 100644 +index ffd7fe054..a40a634d3 100644 --- a/src/frameworks/OpenGL/OpenGL.c +++ b/src/frameworks/OpenGL/OpenGL.c @@ -2,11 +2,22 @@ @@ -6358,7 +6358,7 @@ index ffd7fe054..f25fd9aa5 100644 static EGLConfig config; static int num_config; -@@ -48,10 +124,18 @@ struct _CGLContextObj { +@@ -48,10 +124,19 @@ struct _CGLContextObj { GLuint retain_count; pthread_mutex_t lock; EGLContext egl_context; @@ -6368,6 +6368,7 @@ index ffd7fe054..f25fd9aa5 100644 // EGL has no function for getting the current swap interval, // so we need to save the last set value. The default is 1. int swap_interval; ++ int swap_interval_requested; + EGLSurface native_present_surface; + EGLContext native_present_context; + DarlingNativePresentTarget *native_present_target; @@ -6377,7 +6378,7 @@ index ffd7fe054..f25fd9aa5 100644 }; struct _CGLPixelFormatObj { -@@ -59,6 +143,24 @@ struct _CGLPixelFormatObj { +@@ -59,6 +144,24 @@ struct _CGLPixelFormatObj { CGLPixelFormatAttribute *attributes; }; @@ -6402,7 +6403,7 @@ index ffd7fe054..f25fd9aa5 100644 static inline int attribute_has_argument(CGLPixelFormatAttribute attr) { switch (attr) { case kCGLPFAAuxBuffers: -@@ -69,6 +171,9 @@ static inline int attribute_has_argument(CGLPixelFormatAttribute attr) { +@@ -69,6 +172,9 @@ static inline int attribute_has_argument(CGLPixelFormatAttribute attr) { case kCGLPFAAccumSize: case kCGLPFARendererID: case kCGLPFADisplayMask: @@ -6412,7 +6413,7 @@ index ffd7fe054..f25fd9aa5 100644 return 1; default: return 0; -@@ -78,6 +183,7 @@ static inline int attribute_has_argument(CGLPixelFormatAttribute attr) { +@@ -78,6 +184,7 @@ static inline int attribute_has_argument(CGLPixelFormatAttribute attr) { __attribute__((constructor)) static void _CGLInitialize(void) { @@ -6420,7 +6421,7 @@ index ffd7fe054..f25fd9aa5 100644 g_displays = CFDictionaryCreateMutable(NULL, 0, NULL, NULL); } -@@ -93,41 +199,71 @@ static int attributes_count(const CGLPixelFormatAttribute *attrs) { +@@ -93,41 +200,71 @@ static int attributes_count(const CGLPixelFormatAttribute *attrs) { CGLError CGLRegisterNativeDisplay(void *native_display) { @@ -6500,7 +6501,7 @@ index ffd7fe054..f25fd9aa5 100644 pthread_mutex_lock(&g_displaysMutex); CFDictionaryAddValue(g_displays, (const void*)(unsigned long) cid, rv); -@@ -139,6 +275,8 @@ static struct _CGLDisplay* getCGLDisplay(CGSConnectionID cid) +@@ -139,6 +276,8 @@ static struct _CGLDisplay* getCGLDisplay(CGSConnectionID cid) CGLError CGLSetSurface(CGLContextObj gl, CGSConnectionID cid, CGSWindowID wid, CGSSurfaceID sid) { @@ -6509,7 +6510,7 @@ index ffd7fe054..f25fd9aa5 100644 struct _CGLDisplay* disp = getCGLDisplay(cid); if (!disp) return kCGLBadConnection; -@@ -152,7 +290,7 @@ CGLError CGLSetSurface(CGLContextObj gl, CGSConnectionID cid, CGSWindowID wid, C +@@ -152,7 +291,7 @@ CGLError CGLSetSurface(CGLContextObj gl, CGSConnectionID cid, CGSWindowID wid, C if (!window) return kCGLBadWindow; @@ -6518,7 +6519,7 @@ index ffd7fe054..f25fd9aa5 100644 if (gl->egl_surface == EGL_NO_SURFACE) return kCGLBadState; return kCGLNoError; -@@ -161,7 +299,7 @@ CGLError CGLSetSurface(CGLContextObj gl, CGSConnectionID cid, CGSWindowID wid, C +@@ -161,7 +300,7 @@ CGLError CGLSetSurface(CGLContextObj gl, CGSConnectionID cid, CGSWindowID wid, C CGLWindowRef CGLGetWindow(void *native_window) { EGLNativeWindowType window = (EGLNativeWindowType) native_window; @@ -6527,7 +6528,7 @@ index ffd7fe054..f25fd9aa5 100644 if (surface == EGL_NO_SURFACE) { return NULL; -@@ -170,15 +308,53 @@ CGLWindowRef CGLGetWindow(void *native_window) { +@@ -170,15 +309,53 @@ CGLWindowRef CGLGetWindow(void *native_window) { return (CGLWindowRef) surface; } @@ -6583,7 +6584,7 @@ index ffd7fe054..f25fd9aa5 100644 } static pthread_key_t current_context_key; -@@ -198,18 +374,42 @@ CGLContextObj CGLGetCurrentContext(void) { +@@ -198,18 +375,42 @@ CGLContextObj CGLGetCurrentContext(void) { return pthread_getspecific(key); } @@ -6631,7 +6632,7 @@ index ffd7fe054..f25fd9aa5 100644 } CGLError CGLSetFullScreen(CGLContextObj ctx) { -@@ -223,11 +423,13 @@ CGLError CGLChoosePixelFormat( +@@ -223,11 +424,13 @@ CGLError CGLChoosePixelFormat( CGLPixelFormatObj *result, GLint *number_of_screens ) { @@ -6646,7 +6647,7 @@ index ffd7fe054..f25fd9aa5 100644 for (int i = 0; i < count; i++) { format->attributes[i] = attrs[i]; } -@@ -251,6 +453,10 @@ CGLError CGLDescribePixelFormat( +@@ -251,6 +454,10 @@ CGLError CGLDescribePixelFormat( CGLPixelFormatAttribute attr, GLint *value ) { @@ -6657,7 +6658,7 @@ index ffd7fe054..f25fd9aa5 100644 for (int i = 0; format->attributes[i] != 0; i++) { int has_arg = attribute_has_argument(format->attributes[i]); -@@ -303,37 +509,98 @@ GLuint CGLGetPixelFormatRetainCount(CGLPixelFormatObj pixelFormat) { +@@ -303,37 +510,99 @@ GLuint CGLGetPixelFormatRetainCount(CGLPixelFormatObj pixelFormat) { return pixelFormat->retain_count; } @@ -6719,6 +6720,7 @@ index ffd7fe054..f25fd9aa5 100644 + context->egl_config = contextConfig; context->egl_surface = NULL; context->swap_interval = 1; ++ context->swap_interval_requested = 0; + context->native_present_surface=EGL_NO_SURFACE; + context->native_present_context=EGL_NO_CONTEXT; + context->native_present_target=NULL; @@ -6759,7 +6761,7 @@ index ffd7fe054..f25fd9aa5 100644 return context; } -@@ -342,11 +609,17 @@ void CGLReleaseContext(CGLContextObj context) { +@@ -342,11 +611,17 @@ void CGLReleaseContext(CGLContextObj context) { return; } @@ -6780,7 +6782,7 @@ index ffd7fe054..f25fd9aa5 100644 if (CGLGetCurrentContext() == context) { CGLSetCurrentContext(NULL); -@@ -354,7 +627,11 @@ void CGLReleaseContext(CGLContextObj context) { +@@ -354,7 +629,11 @@ void CGLReleaseContext(CGLContextObj context) { pthread_mutex_destroy(&(context->lock)); @@ -6793,12 +6795,9 @@ index ffd7fe054..f25fd9aa5 100644 free(context); } -@@ -383,9 +660,330 @@ CGLError CGLUnlockContext(CGLContextObj context) { - return kCGLNoError; +@@ -384,8 +663,325 @@ CGLError CGLUnlockContext(CGLContextObj context) { } -+static int swapIntervalRequested; -+ CGLError CGLFlushDrawable(CGLContextObj context) { - eglSwapBuffers(display,context->egl_surface); - return kCGLNoError; @@ -6807,11 +6806,9 @@ index ffd7fe054..f25fd9aa5 100644 + /* darling-lol: EGL defaults to a swap interval of 1. When the window is covered or the compositor throttles it + * (Xwayland answers about once a second), every swap blocks that long and the game's frame rate collapses to ~1 fps. + * macOS apps get an unthrottled flush unless they ask for vsync through kCGLCPSwapInterval, which is handled in -+ * CGLSetParameter; apply "no vsync" once per context here, while it is current. */ -+ static CGLContextObj swapIntervalContext; -+ if (!swapIntervalRequested && context != swapIntervalContext && p_eglSwapInterval && CGLGetCurrentContext() == context) { ++ * CGLSetParameter; apply "no vsync" to unconfigured contexts while current. */ ++ if (!context->swap_interval_requested && p_eglSwapInterval && CGLGetCurrentContext() == context) { + p_eglSwapInterval(context->egl_display, 0); -+ swapIntervalContext = context; + } + return p_eglSwapBuffers(context->egl_display, context->egl_surface) ? kCGLNoError : kCGLBadState; +} @@ -7126,18 +7123,21 @@ index ffd7fe054..f25fd9aa5 100644 } CGLError CGLSetParameter(CGLContextObj context, CGLContextParameter parameter, const GLint *value) { -@@ -394,8 +992,9 @@ CGLError CGLSetParameter(CGLContextObj context, CGLContextParameter parameter, c - +@@ -395,9 +991,11 @@ CGLError CGLSetParameter(CGLContextObj context, CGLContextParameter parameter, c if (parameter == kCGLCPSwapInterval) { -+ swapIntervalRequested = 1; GLint v = *value; - EGLBoolean success = eglSwapInterval(display, v); +- if (success) + EGLBoolean success = p_eglSwapInterval(display, v); - if (success) ++ if (success) { context->swap_interval = v; ++ context->swap_interval_requested = 1; ++ } return success ? kCGLNoError : kCGLBadValue; -@@ -417,14 +1016,19 @@ CGLError CGLGetParameter(CGLContextObj context, CGLContextParameter parameter, G + } + fprintf(stderr, "CGLSetParameter unimplemented for parameter %d\n", parameter); +@@ -417,14 +1015,19 @@ CGLError CGLGetParameter(CGLContextObj context, CGLContextParameter parameter, G return kCGLNoError; } diff --git a/tests/test_metal_vsync.py b/tests/test_metal_vsync.py new file mode 100644 index 0000000..49bba1d --- /dev/null +++ b/tests/test_metal_vsync.py @@ -0,0 +1,139 @@ +"""Exercise the patched presentation policy and CGL call order without a GPU.""" +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest +import test_runtime_compatibility as sources + + +@unittest.skipUnless(shutil.which('c++'), 'C++ compiler required') +class MetalVSyncChecks(unittest.TestCase): + patched = sources.CompatibilityChecks.patched + + def test_explicit_vsync_is_scoped_to_each_cgl_context(self): + source = self.patched('darling', 'src/frameworks/OpenGL/OpenGL.c') + methods = '\n'.join(sources.function(source, name) for name in ('CGLFlushDrawable', 'CGLSetParameter')) + fixture = r''' +#include +#include +using GLint=int; +using CGLError=int; +using CGLContextParameter=int; +using EGLBoolean=int; +constexpr int kCGLNoError=0, kCGLBadContext=1, kCGLBadState=2, kCGLBadAddress=3, kCGLBadValue=4; +constexpr int kCGLCPSwapInterval=1; +struct Context { int egl_display=1, egl_surface=1, swap_interval=1, swap_interval_requested=0; }; +using CGLContextObj=Context*; +static Context* current; +static int display=1, applied=-1; +static bool fail=false; +static int swapInterval(int, int value) { if(fail) return 0; applied=value; return 1; } +static auto p_eglSwapInterval=swapInterval; +static int p_eglSwapBuffers(int,int) { return 1; } +static Context* CGLGetCurrentContext() { return current; } +''' + methods + r''' +int main() { + Context a, b; + current=&a; + int interval=1; + assert(CGLSetParameter(&a,kCGLCPSwapInterval,&interval)==0 && applied==1); + CGLFlushDrawable(&a); assert(applied==1); + current=&b; + CGLFlushDrawable(&b); assert(applied==0); // Another window retains its default. + current=&a; + interval=0; + assert(CGLSetParameter(&a,kCGLCPSwapInterval,&interval)==0 && applied==0); + interval=1; + assert(CGLSetParameter(&a,kCGLCPSwapInterval,&interval)==0 && applied==1); + current=&b; fail=true; + assert(CGLSetParameter(&b,kCGLCPSwapInterval,&interval)==kCGLBadValue); + assert(!b.swap_interval_requested); // Failed requests must not change policy. + fail=false; CGLFlushDrawable(&b); assert(applied==0); + assert(CGLFlushDrawable(nullptr)==kCGLBadContext); +} +''' + self.run_cpp(fixture) + + def test_nested_layers_toggles_windows_and_context_failures(self): + source = self.patched('cocotron', 'QuartzCore/CALayerContext.m') + policy = sources.function(source, 'metalSwapInterval') + # Translate only Objective-C message/iteration syntax; compile the actual + # policy and flush body against inert layer and CGL fixtures. + policy = policy.replace('[[layer class] isSubclassOfClass: [CAMetalLayerInternal class]]', 'layer->metal') + policy = policy.replace('[(CAMetalLayer *)layer displaySyncEnabled]', 'layer->sync') + policy = policy.replace('for (CALayer *child in layer.sublayers)', 'for (CALayer *child : layer->children)') + flush = source.split('- (void) flush {', 1)[1].split('\n}\n', 1)[0] + fixture = r''' +#include +#include +using GLint = int; +using CGLContextObj = int; +constexpr int kCGLNoError = 0, kCGLCPSwapInterval = 1; +constexpr int NULL_CONTEXT = 0; +struct CALayer { bool metal=false, sync=true; std::vector children; }; +struct Context { int interval=-2, swaps=0; } contexts[3]; +static int current=2, attached=0; +static bool fail=false, skip=false; +static int CGLGetCurrentContext() { return current; } +static int CGLSetCurrentContext(int context) { current=context; return 0; } +static int CGLContextMakeCurrentAndAttachToWindow(int context, int window) { + if(fail) return 1; + current=context; attached=window; return 0; +} +static int CGLSetParameter(int context, int parameter, const int* interval) { + assert(current==context && attached==context && parameter==kCGLCPSwapInterval); + contexts[context].interval=*interval; return 0; +} +static int CGLFlushDrawable(int context) { + assert(current==context && attached==context); + ++contexts[context].swaps; return 0; +} +static bool skipLocalRender(CALayer*, int) { return skip; } +''' + fixture += policy + fixture += '\nstruct Window { int _glContext=1, _cglWindow=1, _frame=0; CALayer* _layer; void flush() {' + fixture += flush.replace('NULL', 'NULL_CONTEXT').replace('_frame.size', '_frame') + '\n} };\n' + fixture += r''' +int main() { + CALayer root, container, metal, other; + metal.metal=other.metal=true; + root.children={&container}; container.children={&metal}; + assert(metalSwapInterval(&root)==1); + Window a{1,1,0,&root}; + a.flush(); assert(contexts[1].interval==1 && current==2); + metal.sync=false; + a.flush(); assert(contexts[1].interval==0 && current==2); + container.children.push_back(&other); + a.flush(); assert(contexts[1].interval==1); + other.sync=false; + a.flush(); assert(contexts[1].interval==0); + CALayer plain; + assert(metalSwapInterval(&plain)==-1); + Window b{2,2,0,&plain}; + b.flush(); assert(contexts[2].interval==-2); + other.sync=true; + a.flush(); assert(contexts[1].interval==1 && contexts[2].interval==-2); + a._cglWindow=0; + int swaps=contexts[1].swaps; + a.flush(); assert(contexts[1].swaps==swaps); + a._cglWindow=1; fail=true; + a.flush(); assert(contexts[1].swaps==swaps && current==2); + fail=false; skip=true; + a.flush(); assert(contexts[1].swaps==swaps); + skip=false; current=0; + a.flush(); assert(current==0); + current=1; a.flush(); assert(current==1); + root.children.clear(); + assert(metalSwapInterval(&root)==-1); +} +''' + self.run_cpp(fixture) + + def run_cpp(self, fixture): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) + (path/'check.cpp').write_text(fixture) + result = subprocess.run(['c++', '-std=c++17', str(path/'check.cpp'), '-o', str(path/'check')], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + subprocess.run([str(path/'check')], check=True, timeout=10) diff --git a/tools/riot-metal-presentation-check.mm b/tools/riot-metal-presentation-check.mm index 5266d8d..cf1891b 100644 --- a/tools/riot-metal-presentation-check.mm +++ b/tools/riot-metal-presentation-check.mm @@ -35,6 +35,7 @@ int main() { id device = MTLCreateSystemDefaultDevice(); verifyPresentation(device != nil, "device"); CAMetalLayer* layer = [CAMetalLayer layer]; + verifyPresentation(layer.displaySyncEnabled == YES, "VSync enabled by default"); layer.device = device; layer.drawableSize = CGSizeMake(160,96); layer.frame = CGRectMake(0,0,160,96); @@ -66,6 +67,8 @@ int main() { verifyPresentation(flag != nullptr, "request second bounded capture"); fclose(flag); } + // Toggle the public property while the same layer/window stays live. + layer.displaySyncEnabled = (frame % 3) != 1; @autoreleasepool { id drawable = [layer nextDrawable]; verifyPresentation(drawable != nil, "drawable after late window/context attachment"); @@ -82,6 +85,9 @@ int main() { [commands commit]; [commands waitUntilCompleted]; pump(app); + GLint interval = -1; + CGLGetParameter([(CALayerContext*)[layer _context] glContext], kCGLCPSwapInterval, &interval); + verifyPresentation(interval == (layer.displaySyncEnabled ? 1 : 0), "Metal VSync reaches window swap interval"); } } CGLContextObj previous = CGLGetCurrentContext(); From 99cd0f038dfe228916ced1375f7efc2229303d46 Mon Sep 17 00:00:00 2001 From: lucannez64 <58551020+lucannez64@users.noreply.github.com> Date: Mon, 19 Oct 2026 07:35:16 +0200 Subject: [PATCH 2/3] Present full-window Metal layers on the managed X11 surface for VRR --- docs/TESTING.md | 34 ++++ docs/TROUBLESHOOTING.md | 22 ++ patches/cocotron.patch | 249 ++++++++++++++++++++--- tests/test_x11_vrr.py | 164 +++++++++++++++ tools/build-riot-coregraphics-runtime.py | 13 +- tools/build-riot-x11-runtime.py | 2 +- tools/riot-metal-presentation-check.mm | 40 +++- upstream/manifest.json | 2 +- 8 files changed, 482 insertions(+), 44 deletions(-) create mode 100644 tests/test_x11_vrr.py diff --git a/docs/TESTING.md b/docs/TESTING.md index b1d7df4..86be75d 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -533,3 +533,37 @@ 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. + +## VRR presentation regression (#46) + +```sh +python3 -m unittest discover -s tests -p 'test_x11_vrr.py' +``` + +The host fixtures exercise the patched eligibility, X11 property updates and +EGL surface ownership transitions. They cover hidden/titled/partial views, +multiple views, parent recreation, child fallback and current-context handling. + +Build `build-riot-coregraphics-runtime.py`, `build-riot-x11-runtime.py` and +`build-riot-metal-presentation.py` together. Stage all three updated frameworks +(CoreGraphics, AppKit's X11 backend, QuartzCore) when testing isolated binaries. +Run `riot-metal-presentation-check` with `RIOT_PRESENTATION_PARENT=1` to create +an owned borderless game-like window. It verifies the parent drawable is +borrowed. Add `RIOT_PRESENTATION_RESIZE=1` to verify fallback to a child at +half width and borrowing the parent again when restored. Add +`RIOT_PRESENTATION_NESTED=1` for nested layers. +The default probe still exercises the independent child-window path. + +For physical VRR validation, enable adaptive sync in the monitor and desktop, +then manually test fullscreen and borderless League with a frame cap inside +the monitor's VRR range (for example 157 FPS on a 165 Hz display), with VSync +off. Check the monitor's live refresh display as well as the game FPS counter. +Also test leaving fullscreen, restoring it, resizing, and alt-tab. Report GPU, +driver, compositor and session type. A stable FPS counter or the X11 property +alone does not prove that the monitor is using variable refresh. + +Local native validation on 2026-10-19 passed all three modes (child, parent +with resize, nested parent with resize), including Vulkan-to-GL pixel readback +and ten presentations per mode, using staged frameworks in a private prefix. +Physical VRR and in-game performance on the reporter's Mutter setup remain +unverified. diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 3574291..033ca2d 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -541,3 +541,25 @@ Recovery requires every matching client to be at least 60 seconds old and a complete Hyprland window record (`class`, `title`, `mapped`, `hidden`). A second process scan after window inspection must find the same client identities and no game. Missing information or shutdown failure leaves normal launch in charge. + +## Variable refresh rate (VRR) + +Full-window borderless game layers now use the opaque managed X11 parent +surface for presentation. Previously they swapped an independent child while +the managed parent remained static. Using one surface lets the compositor +track the game's updates for VRR and avoids scanning out an empty parent. +Partial views, titled windows and other applications retain child drawables. +The child fallback retains the compositor-bypass prohibition that prevents +black screens. The fullscreen parent path advertises `_VARIABLE_REFRESH=1`; +leaving fullscreen clears that request. Desktop/driver policy still decides +whether to enable adaptive sync. + +With the game and both clients closed, rebuild with `./scripts/update.sh`; +CoreGraphics, QuartzCore and the X11 backend must be updated together. Enable +VRR in the desktop and monitor, and compare the monitor's live refresh reading +to a frame cap below its maximum refresh, initially with VSync off. VRR and +VSync are separate: this change does not implement an in-game frame limiter. +See [the VRR probe and manual checks](TESTING.md#vrr-presentation-regression-46). + +Standalone child, parent and nested-parent presentation probes passed locally; +monitor-level VRR and the reporter's in-game behavior still need confirmation. diff --git a/patches/cocotron.patch b/patches/cocotron.patch index 22a1fa3..167ddb9 100644 --- a/patches/cocotron.patch +++ b/patches/cocotron.patch @@ -4941,7 +4941,7 @@ index 0000000..31fa646 + +#endif diff --git a/AppKit/X11.backend/X11Window.h b/AppKit/X11.backend/X11Window.h -index bfe3839a..48cfac4c 100644 +index bfe3839a..bc592711 100644 --- a/AppKit/X11.backend/X11Window.h +++ b/AppKit/X11.backend/X11Window.h @@ -26,7 +26,7 @@ @@ -4953,7 +4953,7 @@ index bfe3839a..48cfac4c 100644 @interface X11Window : CGWindow { int _level; // TODO: care about this value -@@ -47,8 +47,32 @@ +@@ -47,8 +47,33 @@ BOOL _mapped; CGPoint _lastMotionPos; BOOL _isModal; @@ -4963,6 +4963,7 @@ index bfe3839a..48cfac4c 100644 + BOOL _hasAlpha; // created with the 32-bit ARGB visual + BOOL _wasMapped; // the visual is settled at the first map BOOL _hasShadow; ++ id _layerDrawableOwner; + NSTimer *_remoteFrameTimer; + Window _remoteFrameWindow; + GC _remoteFrameGC; @@ -4986,19 +4987,22 @@ index bfe3839a..48cfac4c 100644 @public XIC _xic; -@@ -60,9 +84,9 @@ +@@ -60,9 +85,12 @@ - (Visual *) visual; - (Drawable) drawable; - (NSPoint) transformPoint: (NSPoint) pos; -- (O2Rect) transformFrame: (O2Rect) frame; - (Window) windowHandle; ++- (void *) layerDrawableFor: (id) owner; ++- (void) acquireLayerDrawable: (id) owner; ++- (void) releaseLayerDrawable: (id) owner; +- (BOOL) handleRemoteFrameExpose: (Window) window; - (void) frameChanged; - (void) setLastKnownCursorPosition: (CGPoint) point; diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m -index f4925b66..730d4692 100644 +index f4925b66..55be7854 100644 --- a/AppKit/X11.backend/X11Window.m +++ b/AppKit/X11.backend/X11Window.m @@ -18,11 +18,14 @@ @@ -5362,7 +5366,7 @@ index f4925b66..730d4692 100644 } - (void) dealloc { -@@ -279,24 +502,30 @@ static NSData *makeWindowIcon() { +@@ -279,24 +502,59 @@ static NSData *makeWindowIcon() { return _styleMask; } @@ -5399,7 +5403,36 @@ index f4925b66..730d4692 100644 } + return NO; +} ++ ++/* Keep the game on one opaque managed surface. Mutter's VRR frame clock ++ * follows that surface's repaint events, not an independently swapped child. ++ * _VARIABLE_REFRESH also advertises eligibility to X11 drivers that use it. */ ++- (void) updateLayerPresentationHints: (BOOL) fullscreen { ++ if (!X11IsLeagueGameWindow(_display, _window, _delegate)) return; ++ unsigned long adaptive = fullscreen && _layerDrawableOwner != nil; ++ unsigned long bypass = fullscreen && !_layerDrawableOwner ? 2 : 0; ++ XChangeProperty(_display, _window, XInternAtom(_display, "_VARIABLE_REFRESH", False), ++ XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&adaptive, 1); ++ XChangeProperty(_display, _window, XInternAtom(_display, "_NET_WM_BYPASS_COMPOSITOR", False), ++ XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&bypass, 1); ++} ++ ++- (void *) layerDrawableFor: (id) owner { ++ return (!_layerDrawableOwner || _layerDrawableOwner == owner) && !_hasAlpha && X11IsLeagueGameWindow(_display, _window, _delegate) ? _cglWindow : NULL; ++} ++- (void) acquireLayerDrawable: (id) owner { ++ if (_layerDrawableOwner && _layerDrawableOwner != owner) return; ++ _layerDrawableOwner = owner; ++ [self updateLayerPresentationHints: _fullscreenRequested]; ++} ++ ++- (void) releaseLayerDrawable: (id) owner { ++ if (_layerDrawableOwner != owner) return; ++ _layerDrawableOwner = nil; ++ [self updateLayerPresentationHints: _fullscreenRequested]; ++} ++ +- (void) requestFullscreen: (BOOL) fullscreen force: (BOOL) force { + if (fullscreen) { + unsigned long bypass = 2; @@ -5409,7 +5442,7 @@ index f4925b66..730d4692 100644 if (!_mapped) { long states[2]; int states_cnt = 0; -@@ -304,7 +533,7 @@ static NSData *makeWindowIcon() { +@@ -304,7 +562,7 @@ static NSData *makeWindowIcon() { states[states_cnt++] = (long) XInternAtom(_display, "_NET_WM_STATE_MODAL", False); } @@ -5418,7 +5451,7 @@ index f4925b66..730d4692 100644 states[states_cnt++] = (long) XInternAtom( _display, "_NET_WM_STATE_FULLSCREEN", False); } -@@ -312,14 +541,13 @@ static NSData *makeWindowIcon() { +@@ -312,14 +570,13 @@ static NSData *makeWindowIcon() { XInternAtom(_display, "_NET_WM_STATE", False), XA_ATOM, 32, PropModeReplace, (const unsigned char *) states, states_cnt); @@ -5435,11 +5468,12 @@ index f4925b66..730d4692 100644 event.data.l[1] = (long) XInternAtom(_display, "_NET_WM_STATE_FULLSCREEN", False); event.data.l[3] = 1; -@@ -327,6 +555,35 @@ static NSData *makeWindowIcon() { +@@ -327,6 +584,36 @@ static NSData *makeWindowIcon() { SubstructureNotifyMask | SubstructureRedirectMask, (XEvent *) &event); } + _fullscreenRequested = fullscreen; ++ [self updateLayerPresentationHints: fullscreen]; + XFlush(_display); +} + @@ -5471,7 +5505,7 @@ index f4925b66..730d4692 100644 } - (void) setStyleMask: (NSUInteger) mask { -@@ -338,7 +595,6 @@ static NSData *makeWindowIcon() { +@@ -338,7 +625,6 @@ static NSData *makeWindowIcon() { } + (void) removeDecorationForWindow: (Window) w onDisplay: (Display *) dpy { @@ -5479,7 +5513,7 @@ index f4925b66..730d4692 100644 struct { unsigned long flags; unsigned long functions; -@@ -356,8 +612,14 @@ static NSData *makeWindowIcon() { +@@ -356,8 +642,14 @@ static NSData *makeWindowIcon() { - (void) ensureMapped { if (!_mapped) { @@ -5494,7 +5528,7 @@ index f4925b66..730d4692 100644 } } -@@ -377,15 +639,27 @@ static NSData *makeWindowIcon() { +@@ -377,15 +669,27 @@ static NSData *makeWindowIcon() { - (void) syncDelegateProperties { long mask = KeyPressMask | KeyReleaseMask | ExposureMask | StructureNotifyMask | EnterWindowMask | LeaveWindowMask | @@ -5527,7 +5561,7 @@ index f4925b66..730d4692 100644 // TODO: background color } -@@ -393,6 +667,28 @@ static NSData *makeWindowIcon() { +@@ -393,6 +697,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. @@ -5556,7 +5590,7 @@ index f4925b66..730d4692 100644 [_context release]; _context = nil; -@@ -421,6 +717,392 @@ static NSData *makeWindowIcon() { +@@ -421,6 +747,392 @@ static NSData *makeWindowIcon() { } } @@ -5949,7 +5983,7 @@ index f4925b66..730d4692 100644 - (Window) windowHandle { return _window; } -@@ -470,14 +1152,32 @@ static NSData *makeWindowIcon() { +@@ -470,14 +1182,32 @@ static NSData *makeWindowIcon() { const char *text = [title cString]; XStringListToTextProperty((char **) &text, 1, &prop); XSetWMName(_display, _window, &prop); @@ -5983,7 +6017,7 @@ index f4925b66..730d4692 100644 } - (void) setHasShadow: (BOOL) value { -@@ -488,6 +1188,25 @@ static NSData *makeWindowIcon() { +@@ -488,6 +1218,25 @@ static NSData *makeWindowIcon() { _level = value; } @@ -6009,7 +6043,7 @@ index f4925b66..730d4692 100644 - (void) setOpaque: (BOOL) value { _isOpaque = value; } -@@ -543,6 +1262,21 @@ static NSData *makeWindowIcon() { +@@ -543,6 +1292,21 @@ static NSData *makeWindowIcon() { - (void) makeKey { [self ensureMapped]; XRaiseWindow(_display, _window); @@ -6031,7 +6065,7 @@ index f4925b66..730d4692 100644 } - (void) makeMain { -@@ -559,7 +1293,29 @@ static NSData *makeWindowIcon() { +@@ -559,7 +1323,29 @@ static NSData *makeWindowIcon() { XRaiseWindow(_display, _window); } @@ -6061,7 +6095,7 @@ index f4925b66..730d4692 100644 Atom wmState = XInternAtom(_display, "_NET_WM_STATE", FALSE); Atom hiddenState = XInternAtom(_display, "_NET_WM_STATE_HIDDEN", FALSE); -@@ -593,8 +1349,10 @@ static NSData *makeWindowIcon() { +@@ -593,8 +1379,10 @@ static NSData *makeWindowIcon() { if (_cglContext == NULL) { CGLError error; @@ -6074,10 +6108,12 @@ index f4925b66..730d4692 100644 NSLog(@"CGLCreateContext failed at %s %d with error %d", __FILE__, __LINE__, error); -@@ -615,6 +1373,12 @@ static NSData *makeWindowIcon() { +@@ -615,6 +1403,14 @@ static NSData *makeWindowIcon() { } - (void) openGLFlushBuffer { ++ // QuartzCore owns presentation while a full-window game layer borrows it. ++ if (_layerDrawableOwner) return; + /* A remote compositor surface owns this drawable while it is present. + * Swapping the local Onyx surface would erase it until the next timer + * tick. Keep local presentation for windows without a usable remote frame. */ @@ -6087,7 +6123,7 @@ index f4925b66..730d4692 100644 CGLError error; CGLContextObj prevContext = CGLGetCurrentContext(); -@@ -652,7 +1416,7 @@ static NSData *makeWindowIcon() { +@@ -652,7 +1448,7 @@ static NSData *makeWindowIcon() { } - (O2Rect) frame { @@ -6096,7 +6132,7 @@ index f4925b66..730d4692 100644 } static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) { -@@ -666,6 +1430,7 @@ static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) { +@@ -666,6 +1462,7 @@ static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) { } - (void) frameChanged { @@ -6104,7 +6140,7 @@ index f4925b66..730d4692 100644 XErrorHandler previousHandler = XSetErrorHandler(ignoreBadWindow); @try { Window root, parent; -@@ -710,19 +1475,13 @@ static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) { +@@ -710,19 +1507,13 @@ static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) { [_deviceDictionary addEntriesFromDictionary: entries]; } @@ -9489,7 +9525,7 @@ index 5dcac115..5de4f03f 100644 + @end diff --git a/QuartzCore/CALayerContext.m b/QuartzCore/CALayerContext.m -index 5dce8c5..3a7f937 100644 +index 5dce8c56..77b729c7 100644 --- a/QuartzCore/CALayerContext.m +++ b/QuartzCore/CALayerContext.m @@ -1,47 +1,77 @@ @@ -9582,8 +9618,9 @@ index 5dce8c5..3a7f937 100644 + [_deleteTextureIds release]; [_renderer release]; CGLReleaseContext(_glContext); +- CGLDestroyWindow(_cglWindow); + CGLReleasePixelFormat(_pixelFormat); - CGLDestroyWindow(_cglWindow); ++ if (_ownsCglWindow) CGLDestroyWindow(_cglWindow); [_subwindow release]; - [_layer release]; [super dealloc]; @@ -9612,12 +9649,18 @@ index 5dce8c5..3a7f937 100644 } - (void) setSubwindow: (CGSubWindow*) subwindow -@@ -68,16 +108,50 @@ +@@ -64,20 +104,55 @@ + CGSubWindow* oldSubwindow = _subwindow; + + if (_cglWindow) { +- CGLDestroyWindow(_cglWindow); ++ if (_ownsCglWindow) CGLDestroyWindow(_cglWindow); } _subwindow = [subwindow retain]; - _cglWindow = CGLGetWindow([_subwindow nativeWindow]); + _cglWindow = NULL; ++ _ownsCglWindow = NO; + if (_subwindow != nil) { + if (_glContext == NULL) { + CGLPixelFormatAttribute attributes[] = { 0 }; @@ -9636,7 +9679,7 @@ index 5dce8c5..3a7f937 100644 + } + } + if (_glContext != NULL) -+ _cglWindow = CGLGetWindow([_subwindow nativeWindow]); ++ [self updateDrawable]; + } [_subwindow show]; @@ -9664,7 +9707,7 @@ index 5dce8c5..3a7f937 100644 } - (void) assignTextureIdsToLayerTree: (CALayer *) layer { -@@ -93,8 +167,27 @@ +@@ -93,8 +168,43 @@ [self assignTextureIdsToLayerTree: child]; } @@ -9681,6 +9724,22 @@ index 5dce8c5..3a7f937 100644 + for (CALayer *child in layer.sublayers) + [self prepareMetalLayers: child]; +} ++ ++- (void) updateDrawable { ++ if (!_subwindow || !_glContext) return; ++ [_subwindow setAllowsParentDrawable: hasMetalLayer(_layer)]; ++ void *shared = [_subwindow sharedCGLWindow]; ++ if (shared && shared == _cglWindow && !_ownsCglWindow) return; ++ if (!shared && _cglWindow && _ownsCglWindow) return; ++ CGLContextObj previous = CGLGetCurrentContext(); ++ if (previous == _glContext) CGLSetCurrentContext(NULL); ++ if (_ownsCglWindow) CGLDestroyWindow(_cglWindow); ++ _cglWindow = shared ? shared : CGLGetWindow([_subwindow nativeWindow]); ++ _ownsCglWindow = !shared && _cglWindow != NULL; ++ // Update CGL's surface binding even when another context was current. ++ CGLContextMakeCurrentAndAttachToWindow(_glContext, _cglWindow); ++ if (previous != _glContext) CGLSetCurrentContext(previous); ++} + - (void) renderLayer: (CALayer *) layer { + CFTimeInterval renderLayerStart = CACurrentMediaTime(); @@ -9692,7 +9751,7 @@ index 5dce8c5..3a7f937 100644 glEnable(GL_DEPTH_TEST); glShadeModel(GL_SMOOTH); -@@ -118,27 +211,62 @@ +@@ -118,27 +228,63 @@ [_deleteTextureIds removeAllObjects]; @@ -9714,6 +9773,7 @@ index 5dce8c5..3a7f937 100644 } - (void) render { ++ [self updateDrawable]; + if (_cglWindow == NULL || _layer == nil) + return; + if(skipLocalRender(_layer, _frame.size))return; @@ -9760,7 +9820,7 @@ index 5dce8c5..3a7f937 100644 } - (void) startTimerIfNeeded { -@@ -147,7 +275,7 @@ +@@ -147,7 +293,7 @@ target: self selector: @selector(timer:) userInfo: nil @@ -9769,7 +9829,7 @@ index 5dce8c5..3a7f937 100644 } - (void) deleteTextureId: (NSNumber *) textureId { -@@ -155,7 +283,9 @@ +@@ -155,7 +301,9 @@ } - (void) flush { @@ -11102,10 +11162,14 @@ index 49b03483..5ebbf535 100644 - (void) display; - (void) displayIfNeeded; diff --git a/QuartzCore/include/QuartzCore/CALayerContext.h b/QuartzCore/include/QuartzCore/CALayerContext.h -index e4b0c342..d61c37ba 100644 +index e4b0c342..69b39250 100644 --- a/QuartzCore/include/QuartzCore/CALayerContext.h +++ b/QuartzCore/include/QuartzCore/CALayerContext.h -@@ -20,6 +20,7 @@ +@@ -17,9 +17,11 @@ + NSTimer *_timer; + CGSubWindow* _subwindow; + void* _cglWindow; ++ BOOL _ownsCglWindow; } @property(readonly) CGLContextObj glContext; @@ -11252,3 +11316,124 @@ index 53bacc46..3c6adfb7 100644 - (void) beginSheetForDirectory: (NSString *) path file: (NSString *) name modalForWindow: (NSWindow *) docWindow +diff --git a/CoreGraphics/CGSubWindow.m b/CoreGraphics/CGSubWindow.m +index 008ccd74..6692deb5 100644 +--- a/CoreGraphics/CGSubWindow.m ++++ b/CoreGraphics/CGSubWindow.m +@@ -8,6 +8,10 @@ + return NULL; + } + ++// Backends may lend an existing EGL surface for full-window layer rendering. ++- (void *) sharedCGLWindow { return NULL; } ++- (void) setAllowsParentDrawable: (BOOL) allowed {} ++ + - (void) show { + O2InvalidAbstractInvocation(); + } +diff --git a/CoreGraphics/include/CoreGraphics/CGSubWindow.h b/CoreGraphics/include/CoreGraphics/CGSubWindow.h +index de7a3496..79ee0d31 100644 +--- a/CoreGraphics/include/CoreGraphics/CGSubWindow.h ++++ b/CoreGraphics/include/CoreGraphics/CGSubWindow.h +@@ -6,6 +6,9 @@ + @interface CGSubWindow : NSObject + + - (void *) nativeWindow; ++// Borrowed surface; the subwindow/backend retains ownership. ++- (void *) sharedCGLWindow; ++- (void) setAllowsParentDrawable: (BOOL) allowed; + + - (void) show; + - (void) hide; +diff --git a/AppKit/X11.backend/X11SubWindow.m b/AppKit/X11.backend/X11SubWindow.m +index 738ab457..6596dab5 100644 +--- a/AppKit/X11.backend/X11SubWindow.m ++++ b/AppKit/X11.backend/X11SubWindow.m +@@ -1,6 +1,7 @@ + #import "X11SubWindow.h" + #import "X11Display.h" + #import "X11Window.h" ++#import + + @implementation X11SubWindow + +@@ -15,8 +16,9 @@ + } + + - (id) initWithParentWindow: (X11Window *) parent frame: (CGRect) frame { +- _parent = parent; ++ _parent = [parent retain]; + frame = [self convertFrame: frame]; ++ _drawableFrame = frame; + + _display = [(X11Display *) [NSDisplay currentDisplay] display]; + +@@ -31,7 +33,9 @@ + } + + - (void) dealloc { ++ if (_usingParentDrawable) [_parent releaseLayerDrawable: self]; + XDestroyWindow(_display, _window); ++ [_parent release]; + [super dealloc]; + } + +@@ -39,16 +43,41 @@ + return (void *) _window; + } + ++- (void) setAllowsParentDrawable: (BOOL) allowed { ++ _allowsParentDrawable = allowed; ++} ++ ++- (void *) sharedCGLWindow { ++ // Only a full-window borderless game view may draw into the managed parent. ++ // Other views keep independent child drawables and their own coordinates. ++ CGRect parentFrame = [_parent frame]; ++ BOOL coversParent = _allowsParentDrawable && _visible && !([_parent styleMask] & NSTitledWindowMask) && ++ CGRectEqualToRect(_drawableFrame, CGRectMake(0, 0, parentFrame.size.width, parentFrame.size.height)); ++ void *surface = coversParent ? [_parent layerDrawableFor: self] : NULL; ++ BOOL usingParent = surface != NULL; ++ if (usingParent != _usingParentDrawable) { ++ if (usingParent) [_parent acquireLayerDrawable: self]; ++ else [_parent releaseLayerDrawable: self]; ++ _usingParentDrawable = usingParent; ++ } ++ if (_visible && !usingParent) XMapWindow(_display, _window); ++ else XUnmapWindow(_display, _window); ++ return surface; ++} ++ + - (void) show { +- XMapWindow(_display, _window); ++ _visible = YES; ++ [self sharedCGLWindow]; + } + + - (void) hide { +- XUnmapWindow(_display, _window); ++ _visible = NO; ++ [self sharedCGLWindow]; + } + + - (void) setFrame: (CGRect) frame { + frame = [self convertFrame: frame]; ++ _drawableFrame = frame; + + XMoveResizeWindow(_display, _window, frame.origin.x, frame.origin.y, + frame.size.width, frame.size.height); +diff --git a/AppKit/X11.backend/X11SubWindow.h b/AppKit/X11.backend/X11SubWindow.h +index 65b63a3e..e5b913b1 100644 +--- a/AppKit/X11.backend/X11SubWindow.h ++++ b/AppKit/X11.backend/X11SubWindow.h +@@ -7,6 +7,10 @@ + X11Window *_parent; + Display *_display; + Window _window; ++ CGRect _drawableFrame; ++ BOOL _visible; ++ BOOL _allowsParentDrawable; ++ BOOL _usingParentDrawable; + } + + - initWithParentWindow: (X11Window *) parent frame: (CGRect) frame; diff --git a/tests/test_x11_vrr.py b/tests/test_x11_vrr.py new file mode 100644 index 0000000..267a995 --- /dev/null +++ b/tests/test_x11_vrr.py @@ -0,0 +1,164 @@ +"""Compile the patched VRR eligibility and drawable ownership methods with inert fixtures.""" +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest +import test_runtime_compatibility as sources + + +def body(source, signature): + return source.split(signature + ' {', 1)[1].split('\n}', 1)[0] + + +@unittest.skipUnless(shutil.which('c++'), 'C++ compiler required') +class VrrChecks(unittest.TestCase): + patched = sources.CompatibilityChecks.patched + + def run_cpp(self, source): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) + (path/'check.cpp').write_text('#include \n' + source) + result = subprocess.run(['c++', '-std=c++17', str(path/'check.cpp'), '-o', str(path/'check')], capture_output=True, text=True) + self.assertEqual(result.returncode, 0, result.stderr) + subprocess.run([str(path/'check')], check=True, timeout=10) + + def test_hints_only_allow_vrr_for_fullscreen_parent_presentation(self): + source = self.patched('cocotron', 'AppKit/X11.backend/X11Window.m') + method = body(source, '- (void) updateLayerPresentationHints: (BOOL) fullscreen') + getter = body(source, '- (void *) layerDrawableFor: (id) owner') + acquire = body(source, '- (void) acquireLayerDrawable: (id) owner').replace('[self updateLayerPresentationHints: _fullscreenRequested]', 'hints(_fullscreenRequested)') + release = body(source, '- (void) releaseLayerDrawable: (id) owner').replace('[self updateLayerPresentationHints: _fullscreenRequested]', 'hints(_fullscreenRequested)') + self.run_cpp(r''' +#include +#include +using BOOL=bool; +constexpr auto nil=nullptr; +constexpr int False=0, XA_CARDINAL=6, PropModeReplace=0; +static bool game=true; +static int adaptive=-1,bypass=-1,calls=0; +static bool X11IsLeagueGameWindow(int,int,int) { return game; } +static const char* XInternAtom(int,const char* name,int) { return name; } +static int XChangeProperty(int,int,const char* name,int type,int bits,int mode,unsigned char* data,int count) { + assert(type==XA_CARDINAL && bits==32 && mode==PropModeReplace && count==1); + int value=*(unsigned long*)data; ++calls; + if(!strcmp(name,"_VARIABLE_REFRESH")) adaptive=value; + else { assert(!strcmp(name,"_NET_WM_BYPASS_COMPOSITOR")); bypass=value; } + return 0; +} +struct Window { + int _display=1,_window=1,_delegate=1; + void* _layerDrawableOwner=nullptr; + void* _cglWindow=(void*)3; bool _hasAlpha=false,_fullscreenRequested=true; + void hints(BOOL fullscreen) { +''' + method + '\n }\n void* drawable(void* owner) {' + getter + '\n}\n void acquire(void* owner) {' + acquire + '\n}\n void release(void* owner) {' + release + r''' + } +}; +int main() { + Window w; + w.hints(true); assert(adaptive==0 && bypass==2); + w._layerDrawableOwner=&w; + w.hints(true); assert(adaptive==1 && bypass==0); + w.hints(false); assert(adaptive==0 && bypass==0); + w._layerDrawableOwner=nullptr; + w.hints(true); assert(adaptive==0 && bypass==2); + int before=calls; game=false; w.hints(true); assert(calls==before); + assert(!w.drawable(&w)); game=true; + int a,b; + assert(w.drawable(&a)==(void*)3); + w.acquire(&a); assert(w.drawable(&a)==(void*)3 && !w.drawable(&b)); + w.acquire(&b); assert(w._layerDrawableOwner==&a); + w.release(&b); assert(w._layerDrawableOwner==&a); + w.release(&a); assert(!w._layerDrawableOwner && w.drawable(&b)==(void*)3); + w._hasAlpha=true; assert(!w.drawable(&a)); +} +''') + + def test_subwindows_borrow_only_when_visible_borderless_and_covering_parent(self): + source = self.patched('cocotron', 'AppKit/X11.backend/X11SubWindow.m') + method = body(source, '- (void *) sharedCGLWindow') + replacements = {'[_parent frame]':'_parent->frame', '[_parent styleMask]':'_parent->style', + '[_parent layerDrawableFor: self]':'_parent->drawable(this)', + '[_parent acquireLayerDrawable: self]':'_parent->acquire(this)', + '[_parent releaseLayerDrawable: self]':'_parent->release(this)'} + for old,new in replacements.items(): + method=method.replace(old,new) + self.run_cpp(r''' +#include +using BOOL=bool; +struct CGRect { struct {double x,y;} origin; struct {double width,height;} size; }; +static CGRect CGRectMake(double x,double y,double w,double h) {return {{x,y},{w,h}};} +static bool CGRectEqualToRect(CGRect a,CGRect b) {return a.origin.x==b.origin.x && a.origin.y==b.origin.y && a.size.width==b.size.width && a.size.height==b.size.height;} +constexpr int NSTitledWindowMask=1; +static bool mapped; +static void XMapWindow(int,int) {mapped=true;} +static void XUnmapWindow(int,int) {mapped=false;} +struct Parent { + CGRect frame=CGRectMake(0,0,800,600); int style=0; bool eligible=true; + void* owner=nullptr; void* surface=this; + void* drawable(void* requester) {return eligible && (!owner || owner==requester) ? surface : nullptr;} + void acquire(void* requester) {assert(!owner); owner=requester;} + void release(void* requester) {assert(owner==requester); owner=nullptr;} +}; +struct Child { + Parent* _parent; CGRect _drawableFrame=CGRectMake(0,0,800,600); + BOOL _visible=true,_usingParentDrawable=false,_allowsParentDrawable=true; int _display=1,_window=2; + void* shared() { +''' + method + r''' + } +}; +int main() { + Parent p; Child a{&p}; Child b{&p}; + assert(a.shared()==p.surface && !mapped && p.owner==&a); + assert(a.shared()==p.surface); // Repeated lookup does not reacquire. + assert(b.shared()==nullptr && mapped); // A second view cannot share ownership. + a._visible=false; assert(!a.shared() && !mapped && !p.owner); + a._visible=true; assert(a.shared()==p.surface); + a._drawableFrame.size.width=400; + assert(!a.shared() && mapped && !p.owner); + a._drawableFrame=CGRectMake(0,0,800,600); p.style=NSTitledWindowMask; + assert(!a.shared() && mapped); + p.style=0; p.eligible=false; assert(!a.shared() && mapped); + p.eligible=true; a._allowsParentDrawable=false; assert(!a.shared() && mapped); + a._allowsParentDrawable=true; a._drawableFrame.origin.x=1; assert(!a.shared() && mapped); + a._drawableFrame.origin.x=0; assert(a.shared()==p.surface && !mapped); +} +''') + + def test_switching_surfaces_destroys_only_owned_child_drawables(self): + source = self.patched('cocotron', 'QuartzCore/CALayerContext.m') + method = body(source, '- (void) updateDrawable') + method=method.replace('[_subwindow setAllowsParentDrawable: hasMetalLayer(_layer)]', '_subwindow->allow(hasMetalLayer(_layer))') + method=method.replace('[_subwindow sharedCGLWindow]', '_subwindow->shared').replace('[_subwindow nativeWindow]', '_subwindow->native') + self.run_cpp(r''' +#include +using BOOL=bool; +using CGLContextObj=int; +static int current=0,destroyed=0,created=0; +static void* destroyedSurface; +static int CGLGetCurrentContext() {return current;} +static void CGLSetCurrentContext(int c) {current=c;} +static void CGLDestroyWindow(void* surface) {assert(surface==(void*)10); destroyedSurface=surface; ++destroyed;} +static void* CGLGetWindow(void*) {++created; return (void*)10;} +static void CGLContextMakeCurrentAndAttachToWindow(int c,void*) {current=c;} +static bool hasMetalLayer(int layer) {return layer!=0;} +struct Subwindow {void* shared=nullptr; void* native=(void*)2; void allow(bool enabled) {assert(enabled);} }; +struct Context { + Subwindow* _subwindow; int _layer=1; int _glContext=1; void* _cglWindow=nullptr; bool _ownsCglWindow=false; + void update() { +''' + method.replace('CGLSetCurrentContext(NULL)', 'CGLSetCurrentContext(0)') + r''' + } +}; +int main() { + Subwindow sub; Context c{&sub}; + c.update(); assert(created==1 && c._ownsCglWindow); // Ordinary clients still get a child. + c.update(); assert(created==1 && destroyed==0); + current=1; sub.shared=(void*)20; + c.update(); assert(destroyed==1 && destroyedSurface==(void*)10 && !c._ownsCglWindow && current==1); + c.update(); assert(destroyed==1); + sub.shared=(void*)30; c.update(); assert(destroyed==1 && c._cglWindow==(void*)30); + sub.shared=nullptr; c.update(); assert(created==2 && destroyed==1 && c._ownsCglWindow); + current=2; sub.shared=(void*)20; c.update(); assert(current==2 && destroyed==2); + c._subwindow=nullptr; c.update(); assert(destroyed==2); +} +''') diff --git a/tools/build-riot-coregraphics-runtime.py b/tools/build-riot-coregraphics-runtime.py index 8c5e72d..e00ff20 100644 --- a/tools/build-riot-coregraphics-runtime.py +++ b/tools/build-riot-coregraphics-runtime.py @@ -10,12 +10,13 @@ for line in (folder / 'CMakeFiles/CoreGraphics.dir/flags.make').read_text().spli if line.startswith('C_') and ' = ' in line: key, value = line.split(' = ', 1) flags[key] = shlex.split(value) -obj = root / 'build/riot-CGDirectDisplay.o' -subprocess.run(['clang', *flags['C_DEFINES'], *flags['C_INCLUDES'], *flags['C_FLAGS'], - '-O2', '-Wno-macro-redefined', '-c', - str(root / 'build/darling-src/src/external/cocotron/CoreGraphics/CGDirectDisplay.m'), - '-o', str(obj)], check=True) link = shlex.split((folder / 'CMakeFiles/CoreGraphics.dir/link.txt').read_text()) -link[link.index('CMakeFiles/CoreGraphics.dir/CGDirectDisplay.m.o')] = str(obj) +for name in ('CGDirectDisplay', 'CGSubWindow'): + obj = root / f'build/riot-{name}.o' + subprocess.run(['clang', *flags['C_DEFINES'], *flags['C_INCLUDES'], *flags['C_FLAGS'], + '-O2', '-Wno-macro-redefined', '-c', + str(root / f'build/darling-src/src/external/cocotron/CoreGraphics/{name}.m'), + '-o', str(obj)], check=True) + link[link.index(f'CMakeFiles/CoreGraphics.dir/{name}.m.o')] = str(obj) link[link.index('-o') + 1] = str(root / 'build/riot-CoreGraphics') subprocess.run(link, cwd=folder, check=True) diff --git a/tools/build-riot-x11-runtime.py b/tools/build-riot-x11-runtime.py index 3068865..eb8ef6a 100644 --- a/tools/build-riot-x11-runtime.py +++ b/tools/build-riot-x11-runtime.py @@ -14,7 +14,7 @@ for line in (framework / 'CMakeFiles/X11_backend.dir/flags.make').read_text().sp key, value = line.split(' = ', 1) flags[key] = shlex.split(value) link = shlex.split((framework / 'CMakeFiles/X11_backend.dir/link.txt').read_text()) -for name in ('X11Display', 'X11Window'): +for name in ('X11Display', 'X11Window', 'X11SubWindow'): obj = project / f'build/riot-{name}.o' subprocess.run(['clang', *flags['C_DEFINES'], *flags['C_INCLUDES'], *flags['C_FLAGS'], '-O3', '-march=native', '-mtune=generic', '-Wno-macro-redefined', '-c', str(project / f'build/darling-src/src/external/cocotron/AppKit/X11.backend/{name}.m'), '-o', str(obj)], check=True) link[link.index(f'CMakeFiles/X11_backend.dir/X11.backend/{name}.m.o')] = str(obj) diff --git a/tools/riot-metal-presentation-check.mm b/tools/riot-metal-presentation-check.mm index 5266d8d..0bae3b1 100644 --- a/tools/riot-metal-presentation-check.mm +++ b/tools/riot-metal-presentation-check.mm @@ -8,6 +8,14 @@ #import #include #include +#include +extern "C" void _dispatch_main_queue_callback_4CF(void *); + +@interface PresentationCheckWindow : NSWindow +@end +@implementation PresentationCheckWindow +- (BOOL)canBecomeKeyWindow { return YES; } +@end static void verifyPresentation(bool value, const char* message) { if (!value) { fprintf(stderr, "FAIL: %s\n", message); exit(1); } @@ -18,7 +26,12 @@ static void pump(NSApplication* app) { NSEvent* event = [app nextEventMatchingMask:NSAnyEventMask untilDate:[NSDate distantPast] inMode:NSDefaultRunLoopMode dequeue:YES]; if (event) [app sendEvent:event]; - [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.005]]; + // A timed NSRunLoop wait can miss its deadline on Darling when the + // X connection is quiet. Parent presentation no longer supplies child + // Expose events to wake it; service sources without a blocking wait. + CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.0, true); + _dispatch_main_queue_callback_4CF(nullptr); + usleep(1000); } } int main() { @@ -52,12 +65,17 @@ int main() { } else { view.layer = layer; } - NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(50,50,160,96) - styleMask:NSTitledWindowMask backing:NSBackingStoreBuffered defer:NO]; - window.title = @"Metal presentation check"; + const bool parentDrawable = getenv("RIOT_PRESENTATION_PARENT") != nullptr; + NSWindow* window = [[PresentationCheckWindow alloc] initWithContentRect:NSMakeRect(50,50,160,96) + styleMask:(parentDrawable ? NSBorderlessWindowMask : NSTitledWindowMask) backing:NSBackingStoreBuffered defer:parentDrawable]; + window.title = parentDrawable ? @"Presentation (TM) Client" : @"Metal presentation check"; window.contentView = view; [window makeKeyAndOrderFront:nil]; pump(app); + if (parentDrawable) { + void *borrowed = [[(CALayerContext*)[layer _context] subwindow] sharedCGLWindow]; + verifyPresentation(borrowed != nullptr, "full-window game layer borrows managed parent drawable"); + } id queue = [device newCommandQueue]; const unsigned frameCount = captureTest ? 130u : getenv("RIOT_PRESENTATION_STRESS") ? 120u : 10u; for (unsigned frame = 0; frame < frameCount; ++frame) { @@ -82,6 +100,20 @@ int main() { [commands commit]; [commands waitUntilCompleted]; pump(app); + if (parentDrawable) { + void *borrowed = [[(CALayerContext*)[layer _context] subwindow] sharedCGLWindow]; + verifyPresentation(borrowed != nullptr, "parent drawable remains borrowed"); + if (frame == 3 && getenv("RIOT_PRESENTATION_RESIZE")) { + CALayerContext* context = (CALayerContext*)[layer _context]; + CGRect original = [view frame]; + [context setFrame:CGRectMake(0,0,original.size.width / 2,original.size.height)]; + [context render]; [context flush]; + verifyPresentation([[context subwindow] sharedCGLWindow] == nullptr, "partial view falls back to child drawable"); + [context setFrame:original]; + [context render]; [context flush]; + verifyPresentation([[context subwindow] sharedCGLWindow] != nullptr, "restored view borrows parent drawable again"); + } + } } } CGLContextObj previous = CGLGetCurrentContext(); diff --git a/upstream/manifest.json b/upstream/manifest.json index 1dd1cac..9bb8346 100644 --- a/upstream/manifest.json +++ b/upstream/manifest.json @@ -57,7 +57,7 @@ "revision": "c8d38d16a9f613d300157bebbab2b9501bc0c274", "repository": "https://github.com/darlinghq/darling-cocotron.git", "patch": "patches/cocotron.patch", - "modified_files": 104, + "modified_files": 114, "new_files": [ "QuartzCore/include/QuartzCore/CATransformLayer.h", "AppKit/NSGeometryTrace.h" From cf86cff688f9142306a126940b53c2f5e793b87c Mon Sep 17 00:00:00 2001 From: lucannez64 <58551020+lucannez64@users.noreply.github.com> Date: Mon, 19 Oct 2026 13:00:24 +0200 Subject: [PATCH 3/3] Keep the merged X11 patch in canonical diff format --- patches/cocotron.patch | 552 ++++++++++++----------------------------- 1 file changed, 161 insertions(+), 391 deletions(-) diff --git a/patches/cocotron.patch b/patches/cocotron.patch index 231899a..cc6452b 100644 --- a/patches/cocotron.patch +++ b/patches/cocotron.patch @@ -5092,6 +5092,7 @@ index bfe3839a..bc592711 100644 - (void) frameChanged; - (void) setLastKnownCursorPosition: (CGPoint) point; diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m +index f4925b6..b565b91 100644 --- a/AppKit/X11.backend/X11Window.m +++ b/AppKit/X11.backend/X11Window.m @@ -18,11 +18,14 @@ @@ -5109,134 +5110,10 @@ diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m #import #import #import -@@ -38,130 +41,40 @@ +@@ -38,6 +41,110 @@ #import "X11Window.h" #import #import -- --@implementation X11Window -- --+ (Visual *) visual { -- static Visual *ret = NULL; -- -- if (!ret) { -- int visuals_matched, i; -- XVisualInfo match = {0}; -- Display *dpy = [(X11Display *) [NSDisplay currentDisplay] display]; -- -- XVisualInfo *info = XGetVisualInfo(dpy, 0, &match, &visuals_matched); -- -- for (i = 0; i < visuals_matched; i++) { -- if (info[i].depth == 32 && (info[i].red_mask == 0xff0000 && -- info[i].green_mask == 0x00ff00 && -- info[i].blue_mask == 0x0000ff)) { -- ret = info[i].visual; -- } -- } -- XFree(info); -- if (!ret) -- ret = DefaultVisual(dpy, DefaultScreen(dpy)); -- } -- -- return ret; --} -- --static NSData *makeWindowIcon() { -- static NSMutableData *res; -- -- if (res != nil) -- return res; -- -- // Figure out the path. -- NSBundle *bundle = [NSBundle mainBundle]; -- NSString *name = [bundle objectForInfoDictionaryKey: @"CFBundleIconFile"]; -- if (name == nil || [name length] == 0) -- return nil; -- NSString *path = [bundle pathForImageResource: name]; -- if (path == nil) { -- NSLog(@"Cannot find an icon file named %@", name); -- return nil; -- } -- NSURL *url = [NSURL fileURLWithPath: path]; -- -- O2ImageSource *imageSource = [O2ImageSource newImageSourceWithURL: url -- options: nil]; -- if (imageSource == nil) { -- NSLog(@"Cannot parse the icon"); -- return nil; -- } -- -- res = [[NSMutableData alloc] initWithCapacity: 32 * 32 * 4 + 2]; -- O2ColorSpace *colorSpace = O2ColorSpaceCreateDeviceRGB(); -- -- // Go over the images, turning them into the required format -- // and appending them to res. -- for (NSUInteger i = 0; i < [imageSource count]; i++) { -- -- O2Image *image = [imageSource createImageAtIndex: i options: nil]; -- NSUInteger width = O2ImageGetWidth(image); -- NSUInteger height = O2ImageGetHeight(image); -- -- // Render the image in ARGB. -- O2BitmapContext *context = (O2BitmapContext *) O2BitmapContextCreate( -- NULL, width, height, 8, width * 4, colorSpace, -- kO2ImageAlphaPremultipliedFirst | kO2BitmapByteOrder32Host); -- -- [context drawImage: image inRect: O2RectMake(0, 0, width, height)]; -- int *imageData = O2BitmapContextGetData(context); -- -- // Convert to the format accepted by Xlib. -- NSMutableData *data = -- [NSMutableData dataWithLength: width * height * sizeof(long)]; -- long *dataPtr = (long *) [data mutableBytes]; -- int count = width * height; -- for (int i = 0; i < count; i++) { -- dataPtr[i] = imageData[i]; -- } -- -- struct { -- long w, h; -- } size = {width, height}; -- [res appendBytes: &size length: sizeof(size)]; -- [res appendData: data]; -- -- [context release]; -- [image release]; -- } -- -- [colorSpace release]; -- [imageSource release]; -- -- return res; --} -- --- (void) setWindowIcon { -- -- NSData *data = makeWindowIcon(); -- -- if (data == nil) -- return; -- -- XChangeProperty( -- _display, _window, XInternAtom(_display, "_NET_WM_ICON", False), -- XInternAtom(_display, "CARDINAL", False), 32, PropModeReplace, -- [data bytes], [data length] / sizeof(long)); --} -- --- (instancetype) initWithDelegate: (NSWindow *) delegate { -- _delegate = delegate; -- _level = [delegate level]; -- _styleMask = [delegate styleMask]; -- _backingType = (CGSBackingStoreType)[delegate backingType]; -- _deviceDictionary = [NSMutableDictionary new]; -- -- X11Display* x11disp = (X11Display *) [NSDisplay currentDisplay]; -- _display = [x11disp display]; -- -- _frame = [self transformFrame: [delegate frame]]; -- BOOL isPanel = [delegate isKindOfClass: [NSPanel class]]; -- if (isPanel && _styleMask & NSDocModalWindowMask) -- _styleMask = NSBorderlessWindowMask; +#import +#include +#include @@ -5271,16 +5148,19 @@ diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m + if (visualInfo != NULL) + return visualInfo; + - // TODO: get rid of these glX calls - GLint att[] = {GLX_RGBA, - GLX_DOUBLEBUFFER, -@@ -175,36 +88,252 @@ - 4, - None}; - -- int screen = DefaultScreen(_display); -- -- if ((_visualInfo = glXChooseVisual(_display, screen, att)) == NULL) { ++ // TODO: get rid of these glX calls ++ GLint att[] = {GLX_RGBA, ++ GLX_DOUBLEBUFFER, ++ GLX_RED_SIZE, ++ 4, ++ GLX_GREEN_SIZE, ++ 4, ++ GLX_BLUE_SIZE, ++ 4, ++ GLX_DEPTH_SIZE, ++ 4, ++ None}; ++ + int screen = DefaultScreen(display); + + // Rendering goes through EGL. NVIDIA's glXChooseVisual can return a GLX-only visual that every @@ -5295,16 +5175,8 @@ diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m + visualInfo = NULL; + } + if (visualInfo == NULL && (visualInfo = glXChooseVisual(display, screen, att)) == NULL) { - NSLog(@"glXChooseVisual failed at %s %d", __FILE__, __LINE__); - } -- -- Colormap cmap = -- XCreateColormap(_display, RootWindow(_display, _visualInfo->screen), -- _visualInfo->visual, AllocNone); -- -- if (cmap < 0) { -- NSLog(@"XCreateColormap failed"); -- [self release]; ++ NSLog(@"glXChooseVisual failed at %s %d", __FILE__, __LINE__); ++ } + return visualInfo; +} + @@ -5346,145 +5218,56 @@ diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m + } + return found; +} -+ -+@implementation X11Window -+ -++ (Visual *) visual { -+ static Visual *ret = NULL; -+ -+ if (!ret) { -+ int visuals_matched, i; -+ XVisualInfo match = {0}; -+ Display *dpy = [(X11Display *) [NSDisplay currentDisplay] display]; -+ -+ XVisualInfo *info = XGetVisualInfo(dpy, 0, &match, &visuals_matched); -+ -+ for (i = 0; i < visuals_matched; i++) { -+ if (info[i].depth == 32 && (info[i].red_mask == 0xff0000 && -+ info[i].green_mask == 0x00ff00 && -+ info[i].blue_mask == 0x0000ff)) { -+ ret = info[i].visual; -+ } -+ } -+ XFree(info); -+ if (!ret) -+ ret = DefaultVisual(dpy, DefaultScreen(dpy)); -+ } -+ -+ return ret; -+} -+ -+static NSData *makeWindowIcon() { -+ static NSMutableData *res; -+ -+ if (res != nil) -+ return res; -+ -+ // Figure out the path. -+ NSBundle *bundle = [NSBundle mainBundle]; -+ NSString *name = [bundle objectForInfoDictionaryKey: @"CFBundleIconFile"]; -+ if (name == nil || [name length] == 0) - return nil; -- } -+ NSString *path = [bundle pathForImageResource: name]; -+ if (path == nil) { -+ NSLog(@"Cannot find an icon file named %@", name); -+ return nil; -+ } -+ NSURL *url = [NSURL fileURLWithPath: path]; -+ -+ O2ImageSource *imageSource = [O2ImageSource newImageSourceWithURL: url -+ options: nil]; -+ if (imageSource == nil) { -+ NSLog(@"Cannot parse the icon"); -+ return nil; -+ } -+ -+ res = [[NSMutableData alloc] initWithCapacity: 32 * 32 * 4 + 2]; -+ O2ColorSpace *colorSpace = O2ColorSpaceCreateDeviceRGB(); -+ -+ // Go over the images, turning them into the required format -+ // and appending them to res. -+ for (NSUInteger i = 0; i < [imageSource count]; i++) { -+ -+ O2Image *image = [imageSource createImageAtIndex: i options: nil]; -+ NSUInteger width = O2ImageGetWidth(image); -+ NSUInteger height = O2ImageGetHeight(image); -+ -+ // Render the image in ARGB. -+ O2BitmapContext *context = (O2BitmapContext *) O2BitmapContextCreate( -+ NULL, width, height, 8, width * 4, colorSpace, -+ kO2ImageAlphaPremultipliedFirst | kO2BitmapByteOrder32Host); -+ -+ [context drawImage: image inRect: O2RectMake(0, 0, width, height)]; -+ int *imageData = O2BitmapContextGetData(context); -+ -+ // Convert to the format accepted by Xlib. -+ NSMutableData *data = -+ [NSMutableData dataWithLength: width * height * sizeof(long)]; -+ long *dataPtr = (long *) [data mutableBytes]; -+ int count = width * height; -+ for (int i = 0; i < count; i++) { -+ dataPtr[i] = imageData[i]; -+ } -+ -+ struct { -+ long w, h; -+ } size = {width, height}; -+ [res appendBytes: &size length: sizeof(size)]; -+ [res appendData: data]; -+ -+ [context release]; -+ [image release]; -+ } -+ -+ [colorSpace release]; -+ [imageSource release]; -+ -+ return res; -+} -+ -+- (void) setWindowIcon { -+ -+ NSData *data = makeWindowIcon(); -+ -+ if (data == nil) -+ return; -+ -+ XChangeProperty( -+ _display, _window, XInternAtom(_display, "_NET_WM_ICON", False), -+ XInternAtom(_display, "CARDINAL", False), 32, PropModeReplace, -+ [data bytes], [data length] / sizeof(long)); -+} -+ -+- (instancetype) initWithDelegate: (NSWindow *) delegate { -+ _delegate = delegate; -+ _level = [delegate level]; -+ _styleMask = [delegate styleMask]; -+ _backingType = (CGSBackingStoreType)[delegate backingType]; -+ _deviceDictionary = [NSMutableDictionary new]; -+ -+ X11Display* x11disp = (X11Display *) [NSDisplay currentDisplay]; -+ _display = [x11disp display]; -+ + + @implementation X11Window + +@@ -158,53 +265,75 @@ static NSData *makeWindowIcon() { + X11Display* x11disp = (X11Display *) [NSDisplay currentDisplay]; + _display = [x11disp display]; + +- _frame = [self transformFrame: [delegate frame]]; + _frame = [x11disp rootRectForFrame: [delegate frame]]; -+ BOOL isPanel = [delegate isKindOfClass: [NSPanel class]]; -+ if (isPanel && _styleMask & NSDocModalWindowMask) -+ _styleMask = NSBorderlessWindowMask; -+ + BOOL isPanel = [delegate isKindOfClass: [NSPanel class]]; + if (isPanel && _styleMask & NSDocModalWindowMask) + _styleMask = NSBorderlessWindowMask; +- // TODO: get rid of these glX calls +- GLint att[] = {GLX_RGBA, +- GLX_DOUBLEBUFFER, +- GLX_RED_SIZE, +- 4, +- GLX_GREEN_SIZE, +- 4, +- GLX_BLUE_SIZE, +- 4, +- GLX_DEPTH_SIZE, +- 4, +- None}; + +- int screen = DefaultScreen(_display); + _hasAlpha = [self wantsAlphaVisual]; + [self createXWindow]; -+ + +- if ((_visualInfo = glXChooseVisual(_display, screen, att)) == NULL) { +- NSLog(@"glXChooseVisual failed at %s %d", __FILE__, __LINE__); +- } + /* Chromium windows consume cross-process CA frames. League's native Metal window owns its + * drawable; a same-size client surface must never be raised above that drawable. */ + if (!X11IsLeagueGameWindow(_display, None, delegate)) + _remoteFrameTimer = [[NSTimer scheduledTimerWithTimeInterval: 1.0 / 60.0 + target: self selector: @selector(presentRemoteCAFrame:) + userInfo: nil repeats: YES] retain]; -+ + +- Colormap cmap = +- XCreateColormap(_display, RootWindow(_display, _visualInfo->screen), +- _visualInfo->visual, AllocNone); + return self; +} -+ + +- if (cmap < 0) { +- NSLog(@"XCreateColormap failed"); +- [self release]; +- return nil; +- } +- (void) createXWindow { + NSWindow *delegate = _delegate; + X11Display* x11disp = (X11Display *) [NSDisplay currentDisplay]; @@ -5551,7 +5334,7 @@ diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m [self syncDelegateProperties]; -@@ -217,7 +346,20 @@ +@@ -217,7 +346,20 @@ static NSData *makeWindowIcon() { .res_class = (char *) name}; XSetClassHint(_display, _window, &classHint); @@ -5573,7 +5356,7 @@ diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m _isModal = [delegate isSheet] || [NSApp modalWindow] == delegate; // FIXME: There should be no need for this. -@@ -254,19 +396,96 @@ +@@ -254,19 +396,96 @@ static NSData *makeWindowIcon() { XNFocusWindow, _window, NULL); @@ -5590,10 +5373,9 @@ diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m [self setStyleMaskInternal: _styleMask force: YES]; [self setWindowIcon]; -- -- return self; +} -+ + +- return self; +// Only when the background can show through (a pattern image may): League's game window is +// non-opaque on an opaque background, and a 32-bit parent costs Xwayland a copy of its GL child. +- (BOOL) isTranslucent { @@ -5677,10 +5459,26 @@ diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m } - (void) dealloc { -@@ -279,7 +498,106 @@ +@@ -279,24 +498,59 @@ 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. */ @@ -5695,7 +5493,7 @@ diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m + if (!NSIsEmptyRect([screen frame]) && + NSEqualRects([display rootRectForFrame: [screen frame]], frame)) + return YES; -+ } + } + return NO; +} + @@ -5727,48 +5525,52 @@ diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m + _layerDrawableOwner = nil; + [self updateLayerPresentationHints: _fullscreenRequested]; +} -+ + +- (void) requestFullscreen: (BOOL) fullscreen force: (BOOL) force { + if (fullscreen) { + unsigned long bypass = 2; + XChangeProperty(_display, _window, XInternAtom(_display,"_NET_WM_BYPASS_COMPOSITOR",False), + XA_CARDINAL,32,PropModeReplace,(unsigned char *)&bypass,1); + } -+ if (!_mapped) { -+ long states[2]; -+ int states_cnt = 0; -+ if (_isModal) { -+ states[states_cnt++] = -+ (long) XInternAtom(_display, "_NET_WM_STATE_MODAL", False); -+ } + if (!_mapped) { + long states[2]; + int states_cnt = 0; +@@ -304,7 +558,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); -+ } -+ XChangeProperty(_display, _window, -+ XInternAtom(_display, "_NET_WM_STATE", False), XA_ATOM, -+ 32, PropModeReplace, (const unsigned char *) states, -+ states_cnt); + states[states_cnt++] = (long) XInternAtom( + _display, "_NET_WM_STATE_FULLSCREEN", False); + } +@@ -312,14 +566,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; + 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; -+ XSendEvent(_display, DefaultRootWindow(_display), False, -+ SubstructureNotifyMask | SubstructureRedirectMask, -+ (XEvent *) &event); -+ } + event.data.l[1] = + (long) XInternAtom(_display, "_NET_WM_STATE_FULLSCREEN", False); + event.data.l[3] = 1; +@@ -327,6 +580,48 @@ static NSData *makeWindowIcon() { + SubstructureNotifyMask | SubstructureRedirectMask, + (XEvent *) &event); + } + _fullscreenRequested = fullscreen; + [self updateLayerPresentationHints: fullscreen]; + XFlush(_display); +} + - - (void) setStyleMaskInternal: (NSUInteger) mask force: (BOOL) force { ++- (void) setStyleMaskInternal: (NSUInteger) mask force: (BOOL) force { + // Cocoa chrome may change after the native window already exists. A hidden, + // transparent titlebar is client-drawn chrome, even with the titled bit set. + // NSWindowTitleHidden is 1; this SDK exposes titleVisibility as NSInteger. @@ -5781,17 +5583,13 @@ diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m + XDeleteProperty(_display, _window, + XInternAtom(_display, "_MOTIF_WM_HINTS", False)); + } - if (force || (mask & NSWindowStyleMaskResizable) != - (_styleMask & NSWindowStyleMaskResizable)) { - XSizeHints *sh = XAllocSizeHints(); -@@ -287,46 +605,23 @@ - // 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; ++ if (force || (mask & NSWindowStyleMaskResizable) != ++ (_styleMask & NSWindowStyleMaskResizable)) { ++ XSizeHints *sh = XAllocSizeHints(); ++ if (mask & NSWindowStyleMaskResizable) { ++ // Make resizable ++ sh->flags = 0; ++ } else { + if (X11IsLeagueGameWindow(_display, _window, _delegate)) { + // A fixed-size window cannot be made EWMH fullscreen by Mutter, + // and a stale minimum would block switching to a smaller mode. @@ -5802,47 +5600,17 @@ diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m + 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); - } - -- if (!_mapped) { -- long states[2]; -- int states_cnt = 0; -- if (_isModal) { -- states[states_cnt++] = -- (long) XInternAtom(_display, "_NET_WM_STATE_MODAL", False); -- } -- if (mask & NSWindowStyleMaskFullScreen) { -- states[states_cnt++] = (long) XInternAtom( -- _display, "_NET_WM_STATE_FULLSCREEN", False); -- } -- XChangeProperty(_display, _window, -- XInternAtom(_display, "_NET_WM_STATE", False), XA_ATOM, -- 32, PropModeReplace, (const unsigned char *) states, -- states_cnt); -- } else if (force || ((mask & NSWindowStyleMaskFullScreen) != -- (_styleMask & NSWindowStyleMaskFullScreen))) { -- 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[1] = -- (long) XInternAtom(_display, "_NET_WM_STATE_FULLSCREEN", False); -- event.data.l[3] = 1; -- XSendEvent(_display, DefaultRootWindow(_display), False, -- SubstructureNotifyMask | SubstructureRedirectMask, -- (XEvent *) &event); -- } ++ } ++ ++ 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 +633,6 @@ +@@ -338,7 +633,6 @@ static NSData *makeWindowIcon() { } + (void) removeDecorationForWindow: (Window) w onDisplay: (Display *) dpy { @@ -5850,7 +5618,7 @@ diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m struct { unsigned long flags; unsigned long functions; -@@ -356,9 +650,23 @@ +@@ -356,11 +650,25 @@ static NSData *makeWindowIcon() { - (void) ensureMapped { if (!_mapped) { @@ -5861,21 +5629,22 @@ diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m XMapWindow(_display, _window); + _wasMapped = YES; _mapped = YES; -- } + _remoteFrameTargetRefresh = 0; -+ } -+} -+ + } + } + +/* darling-lol: only map for a window AppKit considers visible. NSWindow sets _isVisible before + * ordering, so ordered windows still map; a hidden one (like the client's splash) must not be + * revived by a later restack or makeKey. */ +- (void) ensureMappedIfVisible { + if (_delegate == nil || [_delegate isVisible]) + [self ensureMapped]; - } - ++} ++ - (void) showWindowWithoutActivation { -@@ -377,14 +685,26 @@ + [self ensureMapped]; + } +@@ -377,15 +685,27 @@ static NSData *makeWindowIcon() { - (void) syncDelegateProperties { long mask = KeyPressMask | KeyReleaseMask | ExposureMask | StructureNotifyMask | EnterWindowMask | LeaveWindowMask | @@ -5891,7 +5660,7 @@ diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m + /* Motion also restores a cursor hidden while typing. App-level motion + * delivery is still filtered by acceptsMouseMovedEvents in X11Display. */ XSelectInput(_display, _window, mask); -+ + + /* Mouse-transparent windows must not steal crossing events from the + * view below them. Filtering dispatched events leaves X11 hit testing + * unchanged and can repeatedly hide/recreate tooltip windows. */ @@ -5904,10 +5673,11 @@ diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m + 0, 0, None, ShapeSet); + XFlush(_display); + } - ++ // TODO: background color } -@@ -393,6 +713,28 @@ + +@@ -393,6 +713,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. @@ -5936,12 +5706,10 @@ diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m [_context release]; _context = nil; -@@ -419,6 +761,392 @@ - XDestroyWindow(_display, _window); - _window = 0; +@@ -421,6 +763,392 @@ static NSData *makeWindowIcon() { } -+} -+ + } + +- (void) setRemoteFrameTimerSlow: (BOOL) slow { + if (_remoteFrameTimerSlow == slow) return; + _remoteFrameTimerSlow = slow; @@ -6326,10 +6094,12 @@ diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m + _remoteFrameValid = NO; + [self presentRemoteCAFrame: nil]; + return YES; - } - ++} ++ - (Window) windowHandle { -@@ -470,14 +1198,32 @@ + return _window; + } +@@ -470,14 +1198,32 @@ static NSData *makeWindowIcon() { const char *text = [title cString]; XStringListToTextProperty((char **) &text, 1, &prop); XSetWMName(_display, _window, &prop); @@ -6363,12 +6133,10 @@ diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m } - (void) setHasShadow: (BOOL) value { -@@ -486,6 +1232,25 @@ - - - (void) setLevel: (int) value { +@@ -488,6 +1234,25 @@ static NSData *makeWindowIcon() { _level = value; -+} -+ + } + +/* NSApplication's attention timer calls this for requestUserAttention:. The + * CGWindow base class raises an abstract-invocation exception, which aborted + * the whole LeagueClientUx process. Ask the window manager to mark the window @@ -6386,10 +6154,12 @@ diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m + SubstructureNotifyMask | SubstructureRedirectMask, + (XEvent *) &event); + XFlush(_display); - } - ++} ++ - (void) setOpaque: (BOOL) value { -@@ -518,7 +1283,7 @@ + _isOpaque = value; + } +@@ -518,7 +1283,7 @@ static NSData *makeWindowIcon() { - (void) placeAboveWindow: (NSInteger) otherNumber { X11Window *other = [X11Window windowWithWindowNumber: otherNumber]; @@ -6398,7 +6168,7 @@ diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m if (!other) { XRaiseWindow(_display, _window); -@@ -530,7 +1295,7 @@ +@@ -530,7 +1295,7 @@ static NSData *makeWindowIcon() { - (void) placeBelowWindow: (NSInteger) otherNumber { X11Window *other = [X11Window windowWithWindowNumber: otherNumber]; @@ -6407,7 +6177,7 @@ diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m if (!other) { XLowerWindow(_display, _window); -@@ -541,8 +1306,23 @@ +@@ -541,8 +1306,23 @@ static NSData *makeWindowIcon() { } - (void) makeKey { @@ -6432,7 +6202,7 @@ diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m } - (void) makeMain { -@@ -559,7 +1339,29 @@ +@@ -559,7 +1339,29 @@ static NSData *makeWindowIcon() { XRaiseWindow(_display, _window); } @@ -6462,7 +6232,7 @@ diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m Atom wmState = XInternAtom(_display, "_NET_WM_STATE", FALSE); Atom hiddenState = XInternAtom(_display, "_NET_WM_STATE_HIDDEN", FALSE); -@@ -593,8 +1395,10 @@ +@@ -593,8 +1395,10 @@ static NSData *makeWindowIcon() { if (_cglContext == NULL) { CGLError error; @@ -6475,7 +6245,7 @@ diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m NSLog(@"CGLCreateContext failed at %s %d with error %d", __FILE__, __LINE__, error); -@@ -615,6 +1419,14 @@ +@@ -615,6 +1419,14 @@ static NSData *makeWindowIcon() { } - (void) openGLFlushBuffer { @@ -6490,7 +6260,7 @@ diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m CGLError error; CGLContextObj prevContext = CGLGetCurrentContext(); -@@ -652,7 +1464,7 @@ +@@ -652,7 +1464,7 @@ static NSData *makeWindowIcon() { } - (O2Rect) frame { @@ -6499,7 +6269,7 @@ diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m } static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) { -@@ -666,6 +1478,7 @@ +@@ -666,6 +1478,7 @@ static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) { } - (void) frameChanged { @@ -6507,7 +6277,7 @@ diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m XErrorHandler previousHandler = XSetErrorHandler(ignoreBadWindow); @try { Window root, parent; -@@ -710,19 +1523,13 @@ +@@ -710,19 +1523,13 @@ static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) { [_deviceDictionary addEntriesFromDictionary: entries]; }