Merge pull request #88 from lucannez64/fix/x11-vrr

Fix Metal VSync and VRR presentation together
This commit is contained in:
Kalah authored and GitHub committed 2026-10-19 13:00:57 +02:00
commit ca821262e2
10 files changed
+743 -82

No files matched your search

+73
View File
@@ -623,3 +623,76 @@ launch, window placement, click alignment and edge panning before rollout.
After installing the tested X11 and CoreGraphics binaries locally, the user
reported that everything was working (2026-10-16). Fullscreen policy was unchanged.
## VRR presentation regression (#46)
```sh
python3 -m unittest discover -s tests -p 'test_x11_vrr.py'
```
The host fixtures exercise the patched eligibility, X11 property updates and
EGL surface ownership transitions. They cover hidden/titled/partial views,
multiple views, parent recreation, child fallback and current-context handling.
Build `build-riot-coregraphics-runtime.py`, `build-riot-x11-runtime.py` and
`build-riot-metal-presentation.py` together. Stage all three updated frameworks
(CoreGraphics, AppKit's X11 backend, QuartzCore) when testing isolated binaries.
Run `riot-metal-presentation-check` with `RIOT_PRESENTATION_PARENT=1` to create
an owned borderless game-like window. It verifies the parent drawable is
borrowed. Add `RIOT_PRESENTATION_RESIZE=1` to verify fallback to a child at
half width and borrowing the parent again when restored. Add
`RIOT_PRESENTATION_NESTED=1` for nested layers.
The default probe still exercises the independent child-window path.
For physical VRR validation, enable adaptive sync in the monitor and desktop,
then manually test fullscreen and borderless League with a frame cap inside
the monitor's VRR range (for example 157 FPS on a 165 Hz display), with VSync
off. Check the monitor's live refresh display as well as the game FPS counter.
Also test leaving fullscreen, restoring it, resizing, and alt-tab. Report GPU,
driver, compositor and session type. A stable FPS counter or the X11 property
alone does not prove that the monitor is using variable refresh.
Local native validation on 2026-10-19 passed all three modes (child, parent
with resize, nested parent with resize), including Vulkan-to-GL pixel readback
and ten presentations per mode, using staged frameworks in a private prefix.
Physical VRR and in-game performance on the reporter's Mutter setup remain
unverified.
## 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.
+37
View File
@@ -541,3 +541,40 @@ Recovery requires every matching client to be at least 60 seconds old and a
complete Hyprland window record (`class`, `title`, `mapped`, `hidden`). A second
process scan after window inspection must find the same client identities and
no game. Missing information or shutdown failure leaves normal launch in charge.
## Variable refresh rate (VRR)
Full-window borderless game layers now use the opaque managed X11 parent
surface for presentation. Previously they swapped an independent child while
the managed parent remained static. Using one surface lets the compositor
track the game's updates for VRR and avoids scanning out an empty parent.
Partial views, titled windows and other applications retain child drawables.
The child fallback retains the compositor-bypass prohibition that prevents
black screens. The fullscreen parent path advertises `_VARIABLE_REFRESH=1`;
leaving fullscreen clears that request. Desktop/driver policy still decides
whether to enable adaptive sync.
With the game and both clients closed, rebuild with `./scripts/update.sh`;
CoreGraphics, QuartzCore and the X11 backend must be updated together. Enable
VRR in the desktop and monitor, and compare the monitor's live refresh reading
to a frame cap below its maximum refresh, initially with VSync off. VRR and
VSync are separate: this change does not implement an in-game frame limiter.
See [the VRR probe and manual checks](TESTING.md#vrr-presentation-regression-46).
Standalone child, parent and nested-parent presentation probes passed locally;
monitor-level VRR and the reporter's in-game behavior still need confirmation.
## 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.
+251 -41
View File
@@ -5031,7 +5031,7 @@ index 0000000..31fa646
+
+#endif
diff --git a/AppKit/X11.backend/X11Window.h b/AppKit/X11.backend/X11Window.h
index bfe3839a..48cfac4c 100644
index bfe3839a..bc592711 100644
--- a/AppKit/X11.backend/X11Window.h
+++ b/AppKit/X11.backend/X11Window.h
@@ -26,7 +26,7 @@
@@ -5043,7 +5043,7 @@ index bfe3839a..48cfac4c 100644
@interface X11Window : CGWindow {
int _level; // TODO: care about this value
@@ -47,8 +47,32 @@
@@ -47,8 +47,33 @@
BOOL _mapped;
CGPoint _lastMotionPos;
BOOL _isModal;
@@ -5053,6 +5053,7 @@ index bfe3839a..48cfac4c 100644
+ BOOL _hasAlpha; // created with the 32-bit ARGB visual
+ BOOL _wasMapped; // the visual is settled at the first map
BOOL _hasShadow;
+ id _layerDrawableOwner;
+ NSTimer *_remoteFrameTimer;
+ Window _remoteFrameWindow;
+ GC _remoteFrameGC;
@@ -5076,19 +5077,22 @@ index bfe3839a..48cfac4c 100644
@public
XIC _xic;
@@ -60,9 +84,9 @@
@@ -60,9 +85,12 @@
- (Visual *) visual;
- (Drawable) drawable;
- (NSPoint) transformPoint: (NSPoint) pos;
-- (O2Rect) transformFrame: (O2Rect) frame;
- (Window) windowHandle;
+- (void *) layerDrawableFor: (id) owner;
+- (void) acquireLayerDrawable: (id) owner;
+- (void) releaseLayerDrawable: (id) owner;
+- (BOOL) handleRemoteFrameExpose: (Window) window;
- (void) frameChanged;
- (void) setLastKnownCursorPosition: (CGPoint) point;
diff --git a/AppKit/X11.backend/X11Window.m b/AppKit/X11.backend/X11Window.m
index f4925b66..e458d96f 100644
index f4925b6..b565b91 100644
--- a/AppKit/X11.backend/X11Window.m
+++ b/AppKit/X11.backend/X11Window.m
@@ -18,11 +18,14 @@
@@ -5455,7 +5459,7 @@ index f4925b66..e458d96f 100644
}
- (void) dealloc {
@@ -279,24 +498,30 @@ static NSData *makeWindowIcon() {
@@ -279,24 +498,59 @@ static NSData *makeWindowIcon() {
return _styleMask;
}
@@ -5492,6 +5496,35 @@ index f4925b66..e458d96f 100644
}
+ return NO;
+}
+
+/* Keep the game on one opaque managed surface. Mutter's VRR frame clock
+ * follows that surface's repaint events, not an independently swapped child.
+ * _VARIABLE_REFRESH also advertises eligibility to X11 drivers that use it. */
+- (void) updateLayerPresentationHints: (BOOL) fullscreen {
+ if (!X11IsLeagueGameWindow(_display, _window, _delegate)) return;
+ unsigned long adaptive = fullscreen && _layerDrawableOwner != nil;
+ unsigned long bypass = fullscreen && !_layerDrawableOwner ? 2 : 0;
+ XChangeProperty(_display, _window, XInternAtom(_display, "_VARIABLE_REFRESH", False),
+ XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&adaptive, 1);
+ XChangeProperty(_display, _window, XInternAtom(_display, "_NET_WM_BYPASS_COMPOSITOR", False),
+ XA_CARDINAL, 32, PropModeReplace, (unsigned char *)&bypass, 1);
+}
+
+- (void *) layerDrawableFor: (id) owner {
+ return (!_layerDrawableOwner || _layerDrawableOwner == owner) && !_hasAlpha && X11IsLeagueGameWindow(_display, _window, _delegate) ? _cglWindow : NULL;
+}
+
+- (void) acquireLayerDrawable: (id) owner {
+ if (_layerDrawableOwner && _layerDrawableOwner != owner) return;
+ _layerDrawableOwner = owner;
+ [self updateLayerPresentationHints: _fullscreenRequested];
+}
+
+- (void) releaseLayerDrawable: (id) owner {
+ if (_layerDrawableOwner != owner) return;
+ _layerDrawableOwner = nil;
+ [self updateLayerPresentationHints: _fullscreenRequested];
+}
+- (void) requestFullscreen: (BOOL) fullscreen force: (BOOL) force {
+ if (fullscreen) {
@@ -5502,7 +5535,7 @@ index f4925b66..e458d96f 100644
if (!_mapped) {
long states[2];
int states_cnt = 0;
@@ -304,7 +529,7 @@ static NSData *makeWindowIcon() {
@@ -304,7 +558,7 @@ static NSData *makeWindowIcon() {
states[states_cnt++] =
(long) XInternAtom(_display, "_NET_WM_STATE_MODAL", False);
}
@@ -5511,7 +5544,7 @@ index f4925b66..e458d96f 100644
states[states_cnt++] = (long) XInternAtom(
_display, "_NET_WM_STATE_FULLSCREEN", False);
}
@@ -312,14 +537,13 @@ static NSData *makeWindowIcon() {
@@ -312,14 +566,13 @@ static NSData *makeWindowIcon() {
XInternAtom(_display, "_NET_WM_STATE", False), XA_ATOM,
32, PropModeReplace, (const unsigned char *) states,
states_cnt);
@@ -5528,11 +5561,12 @@ index f4925b66..e458d96f 100644
event.data.l[1] =
(long) XInternAtom(_display, "_NET_WM_STATE_FULLSCREEN", False);
event.data.l[3] = 1;
@@ -327,6 +551,47 @@ static NSData *makeWindowIcon() {
@@ -327,6 +580,48 @@ static NSData *makeWindowIcon() {
SubstructureNotifyMask | SubstructureRedirectMask,
(XEvent *) &event);
}
+ _fullscreenRequested = fullscreen;
+ [self updateLayerPresentationHints: fullscreen];
+ XFlush(_display);
+}
+
@@ -5576,7 +5610,7 @@ index f4925b66..e458d96f 100644
}
- (void) setStyleMask: (NSUInteger) mask {
@@ -338,7 +603,6 @@ static NSData *makeWindowIcon() {
@@ -338,7 +633,6 @@ static NSData *makeWindowIcon() {
}
+ (void) removeDecorationForWindow: (Window) w onDisplay: (Display *) dpy {
@@ -5584,7 +5618,7 @@ index f4925b66..e458d96f 100644
struct {
unsigned long flags;
unsigned long functions;
@@ -356,11 +620,25 @@ static NSData *makeWindowIcon() {
@@ -356,11 +650,25 @@ static NSData *makeWindowIcon() {
- (void) ensureMapped {
if (!_mapped) {
@@ -5610,7 +5644,7 @@ index f4925b66..e458d96f 100644
- (void) showWindowWithoutActivation {
[self ensureMapped];
}
@@ -377,15 +655,27 @@ static NSData *makeWindowIcon() {
@@ -377,15 +685,27 @@ static NSData *makeWindowIcon() {
- (void) syncDelegateProperties {
long mask = KeyPressMask | KeyReleaseMask | ExposureMask |
StructureNotifyMask | EnterWindowMask | LeaveWindowMask |
@@ -5643,7 +5677,7 @@ index f4925b66..e458d96f 100644
// TODO: background color
}
@@ -393,6 +683,28 @@ static NSData *makeWindowIcon() {
@@ -393,6 +713,28 @@ static NSData *makeWindowIcon() {
// This is essentially dealloc; we release our contexts
// and windows, but unlike dealloc, this method can be called
// several times, so set everything to nil/NULL/0.
@@ -5672,7 +5706,7 @@ index f4925b66..e458d96f 100644
[_context release];
_context = nil;
@@ -421,6 +733,392 @@ static NSData *makeWindowIcon() {
@@ -421,6 +763,392 @@ static NSData *makeWindowIcon() {
}
}
@@ -6065,7 +6099,7 @@ index f4925b66..e458d96f 100644
- (Window) windowHandle {
return _window;
}
@@ -470,14 +1168,32 @@ static NSData *makeWindowIcon() {
@@ -470,14 +1198,32 @@ static NSData *makeWindowIcon() {
const char *text = [title cString];
XStringListToTextProperty((char **) &text, 1, &prop);
XSetWMName(_display, _window, &prop);
@@ -6099,7 +6133,7 @@ index f4925b66..e458d96f 100644
}
- (void) setHasShadow: (BOOL) value {
@@ -488,6 +1204,25 @@ static NSData *makeWindowIcon() {
@@ -488,6 +1234,25 @@ static NSData *makeWindowIcon() {
_level = value;
}
@@ -6125,7 +6159,7 @@ index f4925b66..e458d96f 100644
- (void) setOpaque: (BOOL) value {
_isOpaque = value;
}
@@ -518,7 +1253,7 @@ static NSData *makeWindowIcon() {
@@ -518,7 +1283,7 @@ static NSData *makeWindowIcon() {
- (void) placeAboveWindow: (NSInteger) otherNumber {
X11Window *other = [X11Window windowWithWindowNumber: otherNumber];
@@ -6134,7 +6168,7 @@ index f4925b66..e458d96f 100644
if (!other) {
XRaiseWindow(_display, _window);
@@ -530,7 +1265,7 @@ static NSData *makeWindowIcon() {
@@ -530,7 +1295,7 @@ static NSData *makeWindowIcon() {
- (void) placeBelowWindow: (NSInteger) otherNumber {
X11Window *other = [X11Window windowWithWindowNumber: otherNumber];
@@ -6143,7 +6177,7 @@ index f4925b66..e458d96f 100644
if (!other) {
XLowerWindow(_display, _window);
@@ -541,8 +1276,23 @@ static NSData *makeWindowIcon() {
@@ -541,8 +1306,23 @@ static NSData *makeWindowIcon() {
}
- (void) makeKey {
@@ -6168,7 +6202,7 @@ index f4925b66..e458d96f 100644
}
- (void) makeMain {
@@ -559,7 +1309,29 @@ static NSData *makeWindowIcon() {
@@ -559,7 +1339,29 @@ static NSData *makeWindowIcon() {
XRaiseWindow(_display, _window);
}
@@ -6198,7 +6232,7 @@ index f4925b66..e458d96f 100644
Atom wmState = XInternAtom(_display, "_NET_WM_STATE", FALSE);
Atom hiddenState = XInternAtom(_display, "_NET_WM_STATE_HIDDEN", FALSE);
@@ -593,8 +1365,10 @@ static NSData *makeWindowIcon() {
@@ -593,8 +1395,10 @@ static NSData *makeWindowIcon() {
if (_cglContext == NULL) {
CGLError error;
@@ -6211,10 +6245,12 @@ index f4925b66..e458d96f 100644
NSLog(@"CGLCreateContext failed at %s %d with error %d", __FILE__,
__LINE__, error);
@@ -615,6 +1389,12 @@ static NSData *makeWindowIcon() {
@@ -615,6 +1419,14 @@ static NSData *makeWindowIcon() {
}
- (void) openGLFlushBuffer {
+ // QuartzCore owns presentation while a full-window game layer borrows it.
+ if (_layerDrawableOwner) return;
+ /* A remote compositor surface owns this drawable while it is present.
+ * Swapping the local Onyx surface would erase it until the next timer
+ * tick. Keep local presentation for windows without a usable remote frame. */
@@ -6224,7 +6260,7 @@ index f4925b66..e458d96f 100644
CGLError error;
CGLContextObj prevContext = CGLGetCurrentContext();
@@ -652,7 +1432,7 @@ static NSData *makeWindowIcon() {
@@ -652,7 +1464,7 @@ static NSData *makeWindowIcon() {
}
- (O2Rect) frame {
@@ -6233,7 +6269,7 @@ index f4925b66..e458d96f 100644
}
static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) {
@@ -666,6 +1446,7 @@ static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) {
@@ -666,6 +1478,7 @@ static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) {
}
- (void) frameChanged {
@@ -6241,7 +6277,7 @@ index f4925b66..e458d96f 100644
XErrorHandler previousHandler = XSetErrorHandler(ignoreBadWindow);
@try {
Window root, parent;
@@ -710,19 +1491,13 @@ static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) {
@@ -710,19 +1523,13 @@ static int ignoreBadWindow(Display *display, XErrorEvent *errorEvent) {
[_deviceDictionary addEntriesFromDictionary: entries];
}
@@ -9629,13 +9665,13 @@ index 5dcac115..5de4f03f 100644
+
@end
diff --git a/QuartzCore/CALayerContext.m b/QuartzCore/CALayerContext.m
index 5dce8c5..3a7f937 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"
@@ -9657,6 +9693,17 @@ index 5dce8c5..3a7f937 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. */
@@ -9700,10 +9747,11 @@ index 5dce8c5..3a7f937 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;
-
@@ -9722,14 +9770,15 @@ index 5dce8c5..3a7f937 100644
+ [_deleteTextureIds release];
[_renderer release];
CGLReleaseContext(_glContext);
- CGLDestroyWindow(_cglWindow);
+ CGLReleasePixelFormat(_pixelFormat);
CGLDestroyWindow(_cglWindow);
+ if (_ownsCglWindow) CGLDestroyWindow(_cglWindow);
[_subwindow release];
- [_layer release];
[super dealloc];
}
@@ -51,12 +81,22 @@
@@ -51,12 +93,22 @@
}
- (void) setLayer: (CALayer *) layer {
@@ -9752,12 +9801,18 @@ index 5dce8c5..3a7f937 100644
}
- (void) setSubwindow: (CGSubWindow*) subwindow
@@ -68,16 +108,50 @@
@@ -64,20 +116,55 @@
CGSubWindow* oldSubwindow = _subwindow;
if (_cglWindow) {
- CGLDestroyWindow(_cglWindow);
+ if (_ownsCglWindow) CGLDestroyWindow(_cglWindow);
}
_subwindow = [subwindow retain];
- _cglWindow = CGLGetWindow([_subwindow nativeWindow]);
+ _cglWindow = NULL;
+ _ownsCglWindow = NO;
+ if (_subwindow != nil) {
+ if (_glContext == NULL) {
+ CGLPixelFormatAttribute attributes[] = { 0 };
@@ -9776,7 +9831,7 @@ index 5dce8c5..3a7f937 100644
+ }
+ }
+ if (_glContext != NULL)
+ _cglWindow = CGLGetWindow([_subwindow nativeWindow]);
+ [self updateDrawable];
+ }
[_subwindow show];
@@ -9804,7 +9859,7 @@ index 5dce8c5..3a7f937 100644
}
- (void) assignTextureIdsToLayerTree: (CALayer *) layer {
@@ -93,8 +167,27 @@
@@ -93,8 +180,43 @@
[self assignTextureIdsToLayerTree: child];
}
@@ -9821,6 +9876,22 @@ index 5dce8c5..3a7f937 100644
+ for (CALayer *child in layer.sublayers)
+ [self prepareMetalLayers: child];
+}
+
+- (void) updateDrawable {
+ if (!_subwindow || !_glContext) return;
+ [_subwindow setAllowsParentDrawable: hasMetalLayer(_layer)];
+ void *shared = [_subwindow sharedCGLWindow];
+ if (shared && shared == _cglWindow && !_ownsCglWindow) return;
+ if (!shared && _cglWindow && _ownsCglWindow) return;
+ CGLContextObj previous = CGLGetCurrentContext();
+ if (previous == _glContext) CGLSetCurrentContext(NULL);
+ if (_ownsCglWindow) CGLDestroyWindow(_cglWindow);
+ _cglWindow = shared ? shared : CGLGetWindow([_subwindow nativeWindow]);
+ _ownsCglWindow = !shared && _cglWindow != NULL;
+ // Update CGL's surface binding even when another context was current.
+ CGLContextMakeCurrentAndAttachToWindow(_glContext, _cglWindow);
+ if (previous != _glContext) CGLSetCurrentContext(previous);
+}
+
- (void) renderLayer: (CALayer *) layer {
+ CFTimeInterval renderLayerStart = CACurrentMediaTime();
@@ -9832,28 +9903,31 @@ index 5dce8c5..3a7f937 100644
glEnable(GL_DEPTH_TEST);
glShadeModel(GL_SMOOTH);
@@ -118,27 +211,62 @@
@@ -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);
}
- (void) render {
+ [self updateDrawable];
+ if (_cglWindow == NULL || _layer == nil)
+ return;
+ if(skipLocalRender(_layer, _frame.size))return;
@@ -9900,7 +9974,7 @@ index 5dce8c5..3a7f937 100644
}
- (void) startTimerIfNeeded {
@@ -147,7 +275,7 @@
@@ -147,7 +305,7 @@
target: self
selector: @selector(timer:)
userInfo: nil
@@ -9909,14 +9983,25 @@ index 5dce8c5..3a7f937 100644
}
- (void) deleteTextureId: (NSNumber *) textureId {
@@ -155,7 +283,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
@@ -11242,10 +11327,14 @@ index 49b03483..5ebbf535 100644
- (void) display;
- (void) displayIfNeeded;
diff --git a/QuartzCore/include/QuartzCore/CALayerContext.h b/QuartzCore/include/QuartzCore/CALayerContext.h
index e4b0c342..d61c37ba 100644
index e4b0c342..69b39250 100644
--- a/QuartzCore/include/QuartzCore/CALayerContext.h
+++ b/QuartzCore/include/QuartzCore/CALayerContext.h
@@ -20,6 +20,7 @@
@@ -17,9 +17,11 @@
NSTimer *_timer;
CGSubWindow* _subwindow;
void* _cglWindow;
+ BOOL _ownsCglWindow;
}
@property(readonly) CGLContextObj glContext;
@@ -11510,3 +11599,124 @@ index 53bacc46..3c6adfb7 100644
- (void) beginSheetForDirectory: (NSString *) path
file: (NSString *) name
modalForWindow: (NSWindow *) docWindow
diff --git a/CoreGraphics/CGSubWindow.m b/CoreGraphics/CGSubWindow.m
index 008ccd74..6692deb5 100644
--- a/CoreGraphics/CGSubWindow.m
+++ b/CoreGraphics/CGSubWindow.m
@@ -8,6 +8,10 @@
return NULL;
}
+// Backends may lend an existing EGL surface for full-window layer rendering.
+- (void *) sharedCGLWindow { return NULL; }
+- (void) setAllowsParentDrawable: (BOOL) allowed {}
+
- (void) show {
O2InvalidAbstractInvocation();
}
diff --git a/CoreGraphics/include/CoreGraphics/CGSubWindow.h b/CoreGraphics/include/CoreGraphics/CGSubWindow.h
index de7a3496..79ee0d31 100644
--- a/CoreGraphics/include/CoreGraphics/CGSubWindow.h
+++ b/CoreGraphics/include/CoreGraphics/CGSubWindow.h
@@ -6,6 +6,9 @@
@interface CGSubWindow : NSObject
- (void *) nativeWindow;
+// Borrowed surface; the subwindow/backend retains ownership.
+- (void *) sharedCGLWindow;
+- (void) setAllowsParentDrawable: (BOOL) allowed;
- (void) show;
- (void) hide;
diff --git a/AppKit/X11.backend/X11SubWindow.m b/AppKit/X11.backend/X11SubWindow.m
index 738ab457..6596dab5 100644
--- a/AppKit/X11.backend/X11SubWindow.m
+++ b/AppKit/X11.backend/X11SubWindow.m
@@ -1,6 +1,7 @@
#import "X11SubWindow.h"
#import "X11Display.h"
#import "X11Window.h"
+#import <AppKit/NSWindow.h>
@implementation X11SubWindow
@@ -15,8 +16,9 @@
}
- (id) initWithParentWindow: (X11Window *) parent frame: (CGRect) frame {
- _parent = parent;
+ _parent = [parent retain];
frame = [self convertFrame: frame];
+ _drawableFrame = frame;
_display = [(X11Display *) [NSDisplay currentDisplay] display];
@@ -31,7 +33,9 @@
}
- (void) dealloc {
+ if (_usingParentDrawable) [_parent releaseLayerDrawable: self];
XDestroyWindow(_display, _window);
+ [_parent release];
[super dealloc];
}
@@ -39,16 +43,41 @@
return (void *) _window;
}
+- (void) setAllowsParentDrawable: (BOOL) allowed {
+ _allowsParentDrawable = allowed;
+}
+
+- (void *) sharedCGLWindow {
+ // Only a full-window borderless game view may draw into the managed parent.
+ // Other views keep independent child drawables and their own coordinates.
+ CGRect parentFrame = [_parent frame];
+ BOOL coversParent = _allowsParentDrawable && _visible && !([_parent styleMask] & NSTitledWindowMask) &&
+ CGRectEqualToRect(_drawableFrame, CGRectMake(0, 0, parentFrame.size.width, parentFrame.size.height));
+ void *surface = coversParent ? [_parent layerDrawableFor: self] : NULL;
+ BOOL usingParent = surface != NULL;
+ if (usingParent != _usingParentDrawable) {
+ if (usingParent) [_parent acquireLayerDrawable: self];
+ else [_parent releaseLayerDrawable: self];
+ _usingParentDrawable = usingParent;
+ }
+ if (_visible && !usingParent) XMapWindow(_display, _window);
+ else XUnmapWindow(_display, _window);
+ return surface;
+}
+
- (void) show {
- XMapWindow(_display, _window);
+ _visible = YES;
+ [self sharedCGLWindow];
}
- (void) hide {
- XUnmapWindow(_display, _window);
+ _visible = NO;
+ [self sharedCGLWindow];
}
- (void) setFrame: (CGRect) frame {
frame = [self convertFrame: frame];
+ _drawableFrame = frame;
XMoveResizeWindow(_display, _window, frame.origin.x, frame.origin.y,
frame.size.width, frame.size.height);
diff --git a/AppKit/X11.backend/X11SubWindow.h b/AppKit/X11.backend/X11SubWindow.h
index 65b63a3e..e5b913b1 100644
--- a/AppKit/X11.backend/X11SubWindow.h
+++ b/AppKit/X11.backend/X11SubWindow.h
@@ -7,6 +7,10 @@
X11Window *_parent;
Display *_display;
Window _window;
+ CGRect _drawableFrame;
+ BOOL _visible;
+ BOOL _allowsParentDrawable;
+ BOOL _usingParentDrawable;
}
- initWithParentWindow: (X11Window *) parent frame: (CGRect) frame;
+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)
+164
View File
@@ -0,0 +1,164 @@
"""Compile the patched VRR eligibility and drawable ownership methods with inert fixtures."""
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
import test_runtime_compatibility as sources
def body(source, signature):
return source.split(signature + ' {', 1)[1].split('\n}', 1)[0]
@unittest.skipUnless(shutil.which('c++'), 'C++ compiler required')
class VrrChecks(unittest.TestCase):
patched = sources.CompatibilityChecks.patched
def run_cpp(self, source):
with tempfile.TemporaryDirectory() as directory:
path = Path(directory)
(path/'check.cpp').write_text('#include <cstddef>\n' + source)
result = subprocess.run(['c++', '-std=c++17', str(path/'check.cpp'), '-o', str(path/'check')], capture_output=True, text=True)
self.assertEqual(result.returncode, 0, result.stderr)
subprocess.run([str(path/'check')], check=True, timeout=10)
def test_hints_only_allow_vrr_for_fullscreen_parent_presentation(self):
source = self.patched('cocotron', 'AppKit/X11.backend/X11Window.m')
method = body(source, '- (void) updateLayerPresentationHints: (BOOL) fullscreen')
getter = body(source, '- (void *) layerDrawableFor: (id) owner')
acquire = body(source, '- (void) acquireLayerDrawable: (id) owner').replace('[self updateLayerPresentationHints: _fullscreenRequested]', 'hints(_fullscreenRequested)')
release = body(source, '- (void) releaseLayerDrawable: (id) owner').replace('[self updateLayerPresentationHints: _fullscreenRequested]', 'hints(_fullscreenRequested)')
self.run_cpp(r'''
#include <cassert>
#include <cstring>
using BOOL=bool;
constexpr auto nil=nullptr;
constexpr int False=0, XA_CARDINAL=6, PropModeReplace=0;
static bool game=true;
static int adaptive=-1,bypass=-1,calls=0;
static bool X11IsLeagueGameWindow(int,int,int) { return game; }
static const char* XInternAtom(int,const char* name,int) { return name; }
static int XChangeProperty(int,int,const char* name,int type,int bits,int mode,unsigned char* data,int count) {
assert(type==XA_CARDINAL && bits==32 && mode==PropModeReplace && count==1);
int value=*(unsigned long*)data; ++calls;
if(!strcmp(name,"_VARIABLE_REFRESH")) adaptive=value;
else { assert(!strcmp(name,"_NET_WM_BYPASS_COMPOSITOR")); bypass=value; }
return 0;
}
struct Window {
int _display=1,_window=1,_delegate=1;
void* _layerDrawableOwner=nullptr;
void* _cglWindow=(void*)3; bool _hasAlpha=false,_fullscreenRequested=true;
void hints(BOOL fullscreen) {
''' + method + '\n }\n void* drawable(void* owner) {' + getter + '\n}\n void acquire(void* owner) {' + acquire + '\n}\n void release(void* owner) {' + release + r'''
}
};
int main() {
Window w;
w.hints(true); assert(adaptive==0 && bypass==2);
w._layerDrawableOwner=&w;
w.hints(true); assert(adaptive==1 && bypass==0);
w.hints(false); assert(adaptive==0 && bypass==0);
w._layerDrawableOwner=nullptr;
w.hints(true); assert(adaptive==0 && bypass==2);
int before=calls; game=false; w.hints(true); assert(calls==before);
assert(!w.drawable(&w)); game=true;
int a,b;
assert(w.drawable(&a)==(void*)3);
w.acquire(&a); assert(w.drawable(&a)==(void*)3 && !w.drawable(&b));
w.acquire(&b); assert(w._layerDrawableOwner==&a);
w.release(&b); assert(w._layerDrawableOwner==&a);
w.release(&a); assert(!w._layerDrawableOwner && w.drawable(&b)==(void*)3);
w._hasAlpha=true; assert(!w.drawable(&a));
}
''')
def test_subwindows_borrow_only_when_visible_borderless_and_covering_parent(self):
source = self.patched('cocotron', 'AppKit/X11.backend/X11SubWindow.m')
method = body(source, '- (void *) sharedCGLWindow')
replacements = {'[_parent frame]':'_parent->frame', '[_parent styleMask]':'_parent->style',
'[_parent layerDrawableFor: self]':'_parent->drawable(this)',
'[_parent acquireLayerDrawable: self]':'_parent->acquire(this)',
'[_parent releaseLayerDrawable: self]':'_parent->release(this)'}
for old,new in replacements.items():
method=method.replace(old,new)
self.run_cpp(r'''
#include <cassert>
using BOOL=bool;
struct CGRect { struct {double x,y;} origin; struct {double width,height;} size; };
static CGRect CGRectMake(double x,double y,double w,double h) {return {{x,y},{w,h}};}
static bool CGRectEqualToRect(CGRect a,CGRect b) {return a.origin.x==b.origin.x && a.origin.y==b.origin.y && a.size.width==b.size.width && a.size.height==b.size.height;}
constexpr int NSTitledWindowMask=1;
static bool mapped;
static void XMapWindow(int,int) {mapped=true;}
static void XUnmapWindow(int,int) {mapped=false;}
struct Parent {
CGRect frame=CGRectMake(0,0,800,600); int style=0; bool eligible=true;
void* owner=nullptr; void* surface=this;
void* drawable(void* requester) {return eligible && (!owner || owner==requester) ? surface : nullptr;}
void acquire(void* requester) {assert(!owner); owner=requester;}
void release(void* requester) {assert(owner==requester); owner=nullptr;}
};
struct Child {
Parent* _parent; CGRect _drawableFrame=CGRectMake(0,0,800,600);
BOOL _visible=true,_usingParentDrawable=false,_allowsParentDrawable=true; int _display=1,_window=2;
void* shared() {
''' + method + r'''
}
};
int main() {
Parent p; Child a{&p}; Child b{&p};
assert(a.shared()==p.surface && !mapped && p.owner==&a);
assert(a.shared()==p.surface); // Repeated lookup does not reacquire.
assert(b.shared()==nullptr && mapped); // A second view cannot share ownership.
a._visible=false; assert(!a.shared() && !mapped && !p.owner);
a._visible=true; assert(a.shared()==p.surface);
a._drawableFrame.size.width=400;
assert(!a.shared() && mapped && !p.owner);
a._drawableFrame=CGRectMake(0,0,800,600); p.style=NSTitledWindowMask;
assert(!a.shared() && mapped);
p.style=0; p.eligible=false; assert(!a.shared() && mapped);
p.eligible=true; a._allowsParentDrawable=false; assert(!a.shared() && mapped);
a._allowsParentDrawable=true; a._drawableFrame.origin.x=1; assert(!a.shared() && mapped);
a._drawableFrame.origin.x=0; assert(a.shared()==p.surface && !mapped);
}
''')
def test_switching_surfaces_destroys_only_owned_child_drawables(self):
source = self.patched('cocotron', 'QuartzCore/CALayerContext.m')
method = body(source, '- (void) updateDrawable')
method=method.replace('[_subwindow setAllowsParentDrawable: hasMetalLayer(_layer)]', '_subwindow->allow(hasMetalLayer(_layer))')
method=method.replace('[_subwindow sharedCGLWindow]', '_subwindow->shared').replace('[_subwindow nativeWindow]', '_subwindow->native')
self.run_cpp(r'''
#include <cassert>
using BOOL=bool;
using CGLContextObj=int;
static int current=0,destroyed=0,created=0;
static void* destroyedSurface;
static int CGLGetCurrentContext() {return current;}
static void CGLSetCurrentContext(int c) {current=c;}
static void CGLDestroyWindow(void* surface) {assert(surface==(void*)10); destroyedSurface=surface; ++destroyed;}
static void* CGLGetWindow(void*) {++created; return (void*)10;}
static void CGLContextMakeCurrentAndAttachToWindow(int c,void*) {current=c;}
static bool hasMetalLayer(int layer) {return layer!=0;}
struct Subwindow {void* shared=nullptr; void* native=(void*)2; void allow(bool enabled) {assert(enabled);} };
struct Context {
Subwindow* _subwindow; int _layer=1; int _glContext=1; void* _cglWindow=nullptr; bool _ownsCglWindow=false;
void update() {
''' + method.replace('CGLSetCurrentContext(NULL)', 'CGLSetCurrentContext(0)') + r'''
}
};
int main() {
Subwindow sub; Context c{&sub};
c.update(); assert(created==1 && c._ownsCglWindow); // Ordinary clients still get a child.
c.update(); assert(created==1 && destroyed==0);
current=1; sub.shared=(void*)20;
c.update(); assert(destroyed==1 && destroyedSurface==(void*)10 && !c._ownsCglWindow && current==1);
c.update(); assert(destroyed==1);
sub.shared=(void*)30; c.update(); assert(destroyed==1 && c._cglWindow==(void*)30);
sub.shared=nullptr; c.update(); assert(created==2 && destroyed==1 && c._ownsCglWindow);
current=2; sub.shared=(void*)20; c.update(); assert(current==2 && destroyed==2);
c._subwindow=nullptr; c.update(); assert(destroyed==2);
}
''')
+7 -6
View File
@@ -10,12 +10,13 @@ for line in (folder / 'CMakeFiles/CoreGraphics.dir/flags.make').read_text().spli
if line.startswith('C_') and ' = ' in line:
key, value = line.split(' = ', 1)
flags[key] = shlex.split(value)
obj = root / 'build/riot-CGDirectDisplay.o'
subprocess.run(['clang', *flags['C_DEFINES'], *flags['C_INCLUDES'], *flags['C_FLAGS'],
'-O2', '-Wno-macro-redefined', '-c',
str(root / 'build/darling-src/src/external/cocotron/CoreGraphics/CGDirectDisplay.m'),
'-o', str(obj)], check=True)
link = shlex.split((folder / 'CMakeFiles/CoreGraphics.dir/link.txt').read_text())
link[link.index('CMakeFiles/CoreGraphics.dir/CGDirectDisplay.m.o')] = str(obj)
for name in ('CGDirectDisplay', 'CGSubWindow'):
obj = root / f'build/riot-{name}.o'
subprocess.run(['clang', *flags['C_DEFINES'], *flags['C_INCLUDES'], *flags['C_FLAGS'],
'-O2', '-Wno-macro-redefined', '-c',
str(root / f'build/darling-src/src/external/cocotron/CoreGraphics/{name}.m'),
'-o', str(obj)], check=True)
link[link.index(f'CMakeFiles/CoreGraphics.dir/{name}.m.o')] = str(obj)
link[link.index('-o') + 1] = str(root / 'build/riot-CoreGraphics')
subprocess.run(link, cwd=folder, check=True)
+1 -1
View File
@@ -14,7 +14,7 @@ for line in (framework / 'CMakeFiles/X11_backend.dir/flags.make').read_text().sp
key, value = line.split(' = ', 1)
flags[key] = shlex.split(value)
link = shlex.split((framework / 'CMakeFiles/X11_backend.dir/link.txt').read_text())
for name in ('X11Display', 'X11Window'):
for name in ('X11Display', 'X11Window', 'X11SubWindow'):
obj = project / f'build/riot-{name}.o'
subprocess.run(['clang', *flags['C_DEFINES'], *flags['C_INCLUDES'], *flags['C_FLAGS'], '-O3', '-march=native', '-mtune=generic', '-Wno-macro-redefined', '-c', str(project / f'build/darling-src/src/external/cocotron/AppKit/X11.backend/{name}.m'), '-o', str(obj)], check=True)
link[link.index(f'CMakeFiles/X11_backend.dir/X11.backend/{name}.m.o')] = str(obj)
+41 -4
View File
@@ -8,6 +8,14 @@
#import <OpenGL/gl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
extern "C" void _dispatch_main_queue_callback_4CF(void *);
@interface PresentationCheckWindow : NSWindow
@end
@implementation PresentationCheckWindow
- (BOOL)canBecomeKeyWindow { return YES; }
@end
static void verifyPresentation(bool value, const char* message) {
if (!value) { fprintf(stderr, "FAIL: %s\n", message); exit(1); }
@@ -18,7 +26,12 @@ static void pump(NSApplication* app) {
NSEvent* event = [app nextEventMatchingMask:NSAnyEventMask untilDate:[NSDate distantPast]
inMode:NSDefaultRunLoopMode dequeue:YES];
if (event) [app sendEvent:event];
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.005]];
// A timed NSRunLoop wait can miss its deadline on Darling when the
// X connection is quiet. Parent presentation no longer supplies child
// Expose events to wake it; service sources without a blocking wait.
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.0, true);
_dispatch_main_queue_callback_4CF(nullptr);
usleep(1000);
}
}
int main() {
@@ -35,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);
@@ -52,12 +66,17 @@ int main() {
} else {
view.layer = layer;
}
NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(50,50,160,96)
styleMask:NSTitledWindowMask backing:NSBackingStoreBuffered defer:NO];
window.title = @"Metal presentation check";
const bool parentDrawable = getenv("RIOT_PRESENTATION_PARENT") != nullptr;
NSWindow* window = [[PresentationCheckWindow alloc] initWithContentRect:NSMakeRect(50,50,160,96)
styleMask:(parentDrawable ? NSBorderlessWindowMask : NSTitledWindowMask) backing:NSBackingStoreBuffered defer:parentDrawable];
window.title = parentDrawable ? @"Presentation (TM) Client" : @"Metal presentation check";
window.contentView = view;
[window makeKeyAndOrderFront:nil];
pump(app);
if (parentDrawable) {
void *borrowed = [[(CALayerContext*)[layer _context] subwindow] sharedCGLWindow];
verifyPresentation(borrowed != nullptr, "full-window game layer borrows managed parent drawable");
}
id<MTLCommandQueue> queue = [device newCommandQueue];
const unsigned frameCount = captureTest ? 130u : getenv("RIOT_PRESENTATION_STRESS") ? 120u : 10u;
for (unsigned frame = 0; frame < frameCount; ++frame) {
@@ -66,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");
@@ -82,6 +102,23 @@ 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");
if (frame == 3 && getenv("RIOT_PRESENTATION_RESIZE")) {
CALayerContext* context = (CALayerContext*)[layer _context];
CGRect original = [view frame];
[context setFrame:CGRectMake(0,0,original.size.width / 2,original.size.height)];
[context render]; [context flush];
verifyPresentation([[context subwindow] sharedCGLWindow] == nullptr, "partial view falls back to child drawable");
[context setFrame:original];
[context render]; [context flush];
verifyPresentation([[context subwindow] sharedCGLWindow] != nullptr, "restored view borrows parent drawable again");
}
}
}
}
CGLContextObj previous = CGLGetCurrentContext();
+1 -1
View File
@@ -57,7 +57,7 @@
"revision": "c8d38d16a9f613d300157bebbab2b9501bc0c274",
"repository": "https://github.com/darlinghq/darling-cocotron.git",
"patch": "patches/cocotron.patch",
"modified_files": 104,
"modified_files": 115,
"new_files": [
"QuartzCore/include/QuartzCore/CATransformLayer.h",
"AppKit/NSGeometryTrace.h"