mirror of
https://github.com/lucannez64/darling-lol.git
synced 2026-10-26 19:47:42 +00:00
Fix Metal VSync propagation to window swap intervals
This commit is contained in:
1 parent
32ed82e9f2
commit
3142128575
6 files changed
+243
-39
No files matched your search
+19
-1
@@ -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.
|
||||
@@ -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.
|
||||
+35
-9
@@ -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 <Foundation/NSString.h>
|
||||
+#import <Foundation/NSMutableArray.h>
|
||||
#import <QuartzCore/CALayer.h>
|
||||
+#import <QuartzCore/CAMetalLayer.h>
|
||||
#import <QuartzCore/CALayerContext.h>
|
||||
#import <QuartzCore/CARenderer.h>
|
||||
#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
|
||||
|
||||
+29
-29
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <cassert>
|
||||
#include <cstdio>
|
||||
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 <cassert>
|
||||
#include <vector>
|
||||
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<CALayer*> 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)
|
||||
@@ -35,6 +35,7 @@ int main() {
|
||||
id<MTLDevice> 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<CAMetalDrawable> 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();
|
||||
|
||||
Reference in new issue
Block a user