Merge VSync and VRR fixes for combined testing

This commit is contained in:
lucannez64 committed 2026-10-19 12:15:09 +02:00
commit 86cfbb1fd0
6 files changed
+265 -42

No files matched your search

+39
View File
@@ -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.
+15
View File
@@ -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.
+38 -13
View File
@@ -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 <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"
@@ -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
+29 -29
View File
@@ -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;
}
+139
View File
@@ -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)
+5
View File
@@ -48,6 +48,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);
@@ -84,6 +85,7 @@ int main() {
verifyPresentation(flag != nullptr, "request second bounded capture");
fclose(flag);
}
layer.displaySyncEnabled = (frame % 3) != 1;
@autoreleasepool {
id<CAMetalDrawable> 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");