diff --git a/docs/TESTING.md b/docs/TESTING.md index 86be75d..87fc14e 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -567,3 +567,42 @@ 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. + +## 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. + +## Combined VSync and VRR testing + +The `test/vsync-vrr` branch combines PRs #81 and #88, resolving their shared +compositor-patch and presentation-probe changes. The probe toggles +`displaySyncEnabled` and checks the CGL interval in child, parent and nested +parent modes; the parent modes also support resize fallback/restoration. +All three native modes passed locally in a private prefix, together with ten +host regression checks and validation of all 31 component patches. + +After closing the game and both clients: + +```sh +git fetch origin +git switch --track origin/test/vsync-vrr +./scripts/update.sh +``` + +Test VSync with desktop VRR disabled, then test VRR with VSync off and a cap +inside the monitor's adaptive range. Finally test both enabled. Report game +FPS and the monitor's live refresh reading separately. diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 033ca2d..a655648 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -563,3 +563,18 @@ 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. + +## 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 167ddb9..fe78f0d 100644 --- a/patches/cocotron.patch +++ b/patches/cocotron.patch @@ -9525,13 +9525,13 @@ index 5dcac115..5de4f03f 100644 + @end diff --git a/QuartzCore/CALayerContext.m b/QuartzCore/CALayerContext.m -index 5dce8c56..77b729c7 100644 --- a/QuartzCore/CALayerContext.m +++ b/QuartzCore/CALayerContext.m -@@ -1,47 +1,77 @@ +@@ -1,47 +1,89 @@ #import +#import #import ++#import #import #import #import "CALayerInternal.h" @@ -9553,6 +9553,17 @@ index 5dce8c56..77b729c7 100644 + return NO; +} + ++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. */ @@ -9596,10 +9607,11 @@ index 5dce8c56..77b729c7 100644 - kCGLNoError) - NSLog(@"CGLCreateContext failed with %d in %s %d", error, __FILE__, - __LINE__); +- + self = [super init]; + if (self == nil) + return nil; - ++ + _deleteTextureIds = [[NSMutableArray alloc] init]; _frame = rect; - @@ -9626,7 +9638,7 @@ index 5dce8c56..77b729c7 100644 [super dealloc]; } -@@ -51,12 +81,22 @@ +@@ -51,12 +93,22 @@ } - (void) setLayer: (CALayer *) layer { @@ -9649,7 +9661,7 @@ index 5dce8c56..77b729c7 100644 } - (void) setSubwindow: (CGSubWindow*) subwindow -@@ -64,20 +104,55 @@ +@@ -64,20 +116,55 @@ CGSubWindow* oldSubwindow = _subwindow; if (_cglWindow) { @@ -9707,7 +9719,7 @@ index 5dce8c56..77b729c7 100644 } - (void) assignTextureIdsToLayerTree: (CALayer *) layer { -@@ -93,8 +168,43 @@ +@@ -93,8 +180,43 @@ [self assignTextureIdsToLayerTree: child]; } @@ -9751,22 +9763,24 @@ index 5dce8c56..77b729c7 100644 glEnable(GL_DEPTH_TEST); glShadeModel(GL_SMOOTH); -@@ -118,27 +228,63 @@ +@@ -118,27 +240,63 @@ [_deleteTextureIds removeAllObjects]; + CFTimeInterval assignStart = CACurrentMediaTime(); + darlingQCTiming("renderLayerSetup", assignStart - renderLayerStart); [self assignTextureIdsToLayerTree: layer]; -+ darlingQCTiming("assignTextureIds", CACurrentMediaTime() - assignStart); - +- - // this is where the Metal layer renders to an internal texture for us to use - if ([[layer class] isSubclassOfClass: [CAMetalLayerInternal class]]) { - CAMetalLayerInternal* mtl = (CAMetalLayerInternal*)layer; - [mtl prepareRender]; - } +- ++ darlingQCTiming("assignTextureIds", CACurrentMediaTime() - assignStart); ++ + [self prepareMetalLayers: layer]; - ++ + CFTimeInterval rendererStart = CACurrentMediaTime(); [_renderer render]; + darlingQCTiming("rendererRender", CACurrentMediaTime() - rendererStart); @@ -9820,7 +9834,7 @@ index 5dce8c56..77b729c7 100644 } - (void) startTimerIfNeeded { -@@ -147,7 +293,7 @@ +@@ -147,7 +305,7 @@ target: self selector: @selector(timer:) userInfo: nil @@ -9829,14 +9843,25 @@ index 5dce8c56..77b729c7 100644 } - (void) deleteTextureId: (NSNumber *) textureId { -@@ -155,7 +301,9 @@ +@@ -155,7 +313,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 ae2cfd8..a4d2178 100644 --- a/patches/darling.patch +++ b/patches/darling.patch @@ -6765,7 +6765,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 @@ @@ -6866,7 +6866,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; @@ -6876,6 +6876,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; @@ -6885,7 +6886,7 @@ index ffd7fe054..f25fd9aa5 100644 }; struct _CGLPixelFormatObj { -@@ -59,6 +143,24 @@ struct _CGLPixelFormatObj { +@@ -59,6 +144,24 @@ struct _CGLPixelFormatObj { CGLPixelFormatAttribute *attributes; }; @@ -6910,7 +6911,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: @@ -6920,7 +6921,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) { @@ -6928,7 +6929,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) { @@ -7008,7 +7009,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) { @@ -7017,7 +7018,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; @@ -7026,7 +7027,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; @@ -7035,7 +7036,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; } @@ -7091,7 +7092,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); } @@ -7139,7 +7140,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 ) { @@ -7154,7 +7155,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 ) { @@ -7165,7 +7166,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; } @@ -7227,6 +7228,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; @@ -7267,7 +7269,7 @@ index ffd7fe054..f25fd9aa5 100644 return context; } -@@ -342,11 +609,17 @@ void CGLReleaseContext(CGLContextObj context) { +@@ -342,11 +611,17 @@ void CGLReleaseContext(CGLContextObj context) { return; } @@ -7288,7 +7290,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)); @@ -7301,12 +7303,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; @@ -7315,11 +7314,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; +} @@ -7634,18 +7631,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 0bae3b1..65f281f 100644 --- a/tools/riot-metal-presentation-check.mm +++ b/tools/riot-metal-presentation-check.mm @@ -48,6 +48,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); @@ -84,6 +85,7 @@ int main() { verifyPresentation(flag != nullptr, "request second bounded capture"); fclose(flag); } + layer.displaySyncEnabled = (frame % 3) != 1; @autoreleasepool { id drawable = [layer nextDrawable]; verifyPresentation(drawable != nil, "drawable after late window/context attachment"); @@ -100,6 +102,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"); if (parentDrawable) { void *borrowed = [[(CALayerContext*)[layer _context] subwindow] sharedCGLWindow]; verifyPresentation(borrowed != nullptr, "parent drawable remains borrowed");