Fix Metal VSync propagation to window swap intervals

This commit is contained in:
lucannez64 committed 2026-10-19 01:10:03 +02:00
1 parent 32ed82e9f2
commit 3142128575
6 files changed
+243 -39

No files matched your search

+19 -1
View File
@@ -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.
+15
View File
@@ -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
View File
@@ -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
View File
@@ -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;
}
+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)
+6
View File
@@ -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();