Unverified Commit c8147b1e authored by Elijah Semyonov's avatar Elijah Semyonov Committed by GitHub

Speculative fix for a crash (#959)

Usage of main-thread AppKit APIs in other thread is assumed to lead to
the crash.
Second attempt for https://github.com/JetBrains/skiko/pull/958.

Structure display link control reactively around Wake/Sleep and Screen
change notifications.

---------
Co-authored-by: 's avatarIgor Demin <igordmn@users.noreply.github.com>
parent e7d2dd3f
...@@ -2,8 +2,8 @@ package org.jetbrains.skiko.redrawer ...@@ -2,8 +2,8 @@ package org.jetbrains.skiko.redrawer
import org.jetbrains.skiko.Library import org.jetbrains.skiko.Library
internal class DisplayLinkThrottler { internal class DisplayLinkThrottler(windowPtr: Long) {
private val implPtr = create() private val implPtr = create(windowPtr)
internal fun dispose() = dispose(implPtr) internal fun dispose() = dispose(implPtr)
...@@ -11,13 +11,13 @@ internal class DisplayLinkThrottler { ...@@ -11,13 +11,13 @@ internal class DisplayLinkThrottler {
* Creates a DisplayLink if needed with refresh rate matching NSScreen of NSWindow passed in [windowPtr]. * Creates a DisplayLink if needed with refresh rate matching NSScreen of NSWindow passed in [windowPtr].
* If DisplayLink is already active, blocks until next vsync for physical screen of NSWindow passed in [windowPtr]. * If DisplayLink is already active, blocks until next vsync for physical screen of NSWindow passed in [windowPtr].
*/ */
internal fun waitVSync(windowPtr: Long) = waitVSync(implPtr, windowPtr) private external fun create(windowPtr: Long): Long
private external fun create(): Long fun waitVSync() = waitVSync(implPtr)
private external fun dispose(implPtr: Long) private external fun dispose(implPtr: Long)
private external fun waitVSync(implPtr: Long, windowPtr: Long) private external fun waitVSync(implPtr: Long)
companion object { companion object {
init { init {
......
...@@ -60,7 +60,7 @@ internal class MetalRedrawer( ...@@ -60,7 +60,7 @@ internal class MetalRedrawer(
} }
private val adapter = chooseMetalAdapter(properties.adapterPriority) private val adapter = chooseMetalAdapter(properties.adapterPriority)
private val displayLinkThrottler = DisplayLinkThrottler() private val displayLinkThrottler = DisplayLinkThrottler(layer.windowHandle)
private val windowOcclusionStateChannel = Channel<Boolean>(Channel.CONFLATED) private val windowOcclusionStateChannel = Channel<Boolean>(Channel.CONFLATED)
@Volatile private var isWindowOccluded = false @Volatile private var isWindowOccluded = false
...@@ -159,7 +159,7 @@ internal class MetalRedrawer( ...@@ -159,7 +159,7 @@ internal class MetalRedrawer(
// Wait for vsync because: // Wait for vsync because:
// - macOS drops the second/next drawables if they are sent in the same vsync // - macOS drops the second/next drawables if they are sent in the same vsync
// - it makes frames consistent and limits FPS // - it makes frames consistent and limits FPS
displayLinkThrottler.waitVSync(windowPtr = layer.windowHandle) displayLinkThrottler.waitVSync()
autoreleasepool { autoreleasepool {
contextHandler.draw() contextHandler.draw()
......
...@@ -5,6 +5,33 @@ ...@@ -5,6 +5,33 @@
#import <QuartzCore/QuartzCore.h> #import <QuartzCore/QuartzCore.h>
#import <AppKit/AppKit.h> #import <AppKit/AppKit.h>
#import <stdatomic.h> #import <stdatomic.h>
#import <objc/runtime.h>
typedef void (^OnScreenChangeCallback)(void);
@interface NSWindow (OnScreenChangeCallbackExtension)
- (void)skiko_setupOnScreenChangeCallback:(OnScreenChangeCallback)callback;
@end
static void *OnScreenChangeCallbackKey = &OnScreenChangeCallbackKey;
@implementation NSWindow (OnScreenChangeCallbackExtension)
- (void)skiko_setupOnScreenChangeCallback:(OnScreenChangeCallback)callback {
objc_setAssociatedObject(self, OnScreenChangeCallbackKey, [callback copy], OBJC_ASSOCIATION_COPY_NONATOMIC);
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self selector:@selector(skiko_onScreenChangeCallback) name:NSWindowDidChangeScreenNotification object:nil];
}
- (void)skiko_onScreenChangeCallback {
OnScreenChangeCallback callback = objc_getAssociatedObject(self, OnScreenChangeCallbackKey);
callback();
}
@end
@interface DisplayLinkThrottler : NSObject @interface DisplayLinkThrottler : NSObject
...@@ -21,125 +48,184 @@ static CVReturn displayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt ...@@ -21,125 +48,184 @@ static CVReturn displayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
} }
@implementation DisplayLinkThrottler { @implementation DisplayLinkThrottler {
NSScreen *_displayLinkScreen; // CGDirectDisplayID is an alias for uint32_t, we'll use int64_t to keep -1 in case there is no screen
int64_t _currentScreenID;
CGDirectDisplayID _displayLinkScreenID;
CVDisplayLinkRef _displayLink; CVDisplayLinkRef _displayLink;
// Lock to protect internal state
NSLock *_lock;
NSConditionLock *_vsyncConditionLock; NSConditionLock *_vsyncConditionLock;
BOOL _isSleeping; BOOL _isSleeping;
__weak NSWindow *_window;
} }
- (instancetype)init { - (instancetype)initWithWindow:(NSWindow *)window {
self = [super init]; self = [super init];
if (self) { if (self) {
_displayLinkScreen = nil; _currentScreenID = -1;
_window = window;
_displayLink = nil; _displayLink = nil;
_vsyncConditionLock = [[NSConditionLock alloc] initWithCondition: 1]; _vsyncConditionLock = [[NSConditionLock alloc] initWithCondition: 1];
_lock = [NSLock new];
_isSleeping = NO; _isSleeping = NO;
NSNotificationCenter *notificationCenter = [[NSWorkspace sharedWorkspace] notificationCenter]; NSNotificationCenter *notificationCenter = [[NSWorkspace sharedWorkspace] notificationCenter];
[notificationCenter addObserver:self [notificationCenter addObserver:self
selector:@selector(systemWillSleep:) selector:@selector(systemWillSleep)
name:NSWorkspaceWillSleepNotification name:NSWorkspaceWillSleepNotification
object:nil]; object:nil];
[notificationCenter addObserver:self [notificationCenter addObserver:self
selector:@selector(systemDidWake:) selector:@selector(systemDidWake)
name:NSWorkspaceDidWakeNotification name:NSWorkspaceDidWakeNotification
object:nil]; object:nil];
__weak DisplayLinkThrottler *weakSelf = self;
[window skiko_setupOnScreenChangeCallback:^{
[weakSelf onScreenDidChange];
}];
[self onScreenDidChange];
} }
return self; return self;
} }
- (void)systemWillSleep:(NSNotification *)notification { - (void)onScreenDidChange {
[_lock lock];
assert(NSThread.currentThread.isMainThread);
NSScreen *screen = _window.screen;
NSDictionary* screenDescription = [screen deviceDescription];
NSNumber* screenNumber = [screenDescription objectForKey:@"NSScreenNumber"];
if (screenNumber) {
_currentScreenID = [screenNumber longValue];
} else {
_currentScreenID = -1;
}
[self updateDisplayLink];
[_lock unlock];
}
- (void)updateDisplayLink {
// Assumed to be run inside _lock
if (_isSleeping) {
// invalidate or do nothing if no display link is present
[self invalidateDisplayLink];
} else {
if (_displayLink) {
// if display link is present, check if it's for the correct screen, avoid conversion shenanigans
int64_t displayLinkScreenID = _displayLinkScreenID;
if (displayLinkScreenID != _currentScreenID) {
[self createDisplayLink];
}
} else {
// no display link, create one
[self createDisplayLink];
}
}
}
- (void)systemWillSleep {
[_lock lock];
_isSleeping = YES; _isSleeping = YES;
[self invalidateDisplayLink]; [self updateDisplayLink];
[self onVSync]; [self onVSync];
[_lock unlock];
} }
- (void)systemDidWake:(NSNotification *)notification { - (void)systemDidWake {
[_lock lock];
_isSleeping = NO; _isSleeping = NO;
[self updateDisplayLink];
[_lock unlock];
} }
- (void)onVSync { - (void)onVSync {
/// Lock condition lock and immediately unlock setting condition variable to 1 (can render now) // Lock condition lock and immediately unlock setting condition variable to 1 (can render now)
[_vsyncConditionLock lock]; [_vsyncConditionLock lock];
[_vsyncConditionLock unlockWithCondition:1]; [_vsyncConditionLock unlockWithCondition:1];
} }
- (void)waitVSync { - (void)waitVSync {
/// If display link is not constructed (due to failure or explicit opt-out), don't perform any waiting BOOL hasDisplayLink;
if (!_displayLink) {
[_lock lock];
hasDisplayLink = _displayLink != nil;
[_lock unlock];
// No display link to wait for
if (!hasDisplayLink) {
return; return;
} }
/// Wait until `onVSync` signals 1, then immediately lock, set it to 0 and unlock again. // Wait until `onVSync` signals 1, then immediately lock, set it to 0 and unlock again.
[_vsyncConditionLock lockWhenCondition:1]; [_vsyncConditionLock lockWhenCondition:1];
[_vsyncConditionLock unlockWithCondition:0]; [_vsyncConditionLock unlockWithCondition:0];
} }
- (void)invalidateDisplayLink { - (void)invalidateDisplayLink {
// Assumed to be run inside _lock, except for dealloc
if (_displayLink) { if (_displayLink) {
CVDisplayLinkStop(_displayLink); CVDisplayLinkStop(_displayLink);
CVDisplayLinkRelease(_displayLink); CVDisplayLinkRelease(_displayLink);
_displayLink = nil; _displayLink = nil;
_displayLinkScreen = nil;
}
}
- (void)setupDisplayLinkForWindow:(NSWindow *)window {
if (_isSleeping) {
/// System is sleeping, don't setup display link
return;
} }
}
NSScreen *screen = window.screen; - (void)createDisplayLink {
// Assumed to be run inside _lock
if (!screen) { [self invalidateDisplayLink];
/// window is not yet attached to screen (or window is nil)
return;
}
if ([screen isEqualTo: _displayLinkScreen]) { if (_currentScreenID < 0) {
/// display link is already active for this screen, do nothing
return; return;
} }
[self invalidateDisplayLink];
NSDictionary* screenDescription = [screen deviceDescription];
NSNumber* screenID = [screenDescription objectForKey:@"NSScreenNumber"];
_displayLink = [self createDisplayLinkForScreen:screenID];
_displayLinkScreen = screen;
}
- (CVDisplayLinkRef)createDisplayLinkForScreen:(NSNumber *)screenID {
CVReturn result; CVReturn result;
CVDisplayLinkRef displayLink;
result = CVDisplayLinkCreateWithCGDisplay([screenID unsignedIntValue], &displayLink); result = CVDisplayLinkCreateWithCGDisplay(
_currentScreenID /* truncated to uint32_t aka CGDirectDisplayID */,
&_displayLink
);
if (result != kCVReturnSuccess) { if (result != kCVReturnSuccess) {
return nil; _displayLink = nil;
return;
} }
result = CVDisplayLinkSetOutputCallback(displayLink, &displayLinkCallback, (__bridge void *)(self)); result = CVDisplayLinkSetOutputCallback(_displayLink, &displayLinkCallback, (__bridge void *)(self));
if (result != kCVReturnSuccess) { if (result != kCVReturnSuccess) {
CVDisplayLinkRelease(displayLink); CVDisplayLinkRelease(_displayLink);
return nil; _displayLink = nil;
return;
} }
result = CVDisplayLinkStart(displayLink); result = CVDisplayLinkStart(_displayLink);
if (result != kCVReturnSuccess) { if (result != kCVReturnSuccess) {
CVDisplayLinkRelease(displayLink); CVDisplayLinkRelease(_displayLink);
return nil; _displayLink = nil;
return;
} }
return displayLink; _displayLinkScreenID = _currentScreenID;
} }
- (void)dealloc { - (void)dealloc {
...@@ -150,24 +236,21 @@ static CVReturn displayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt ...@@ -150,24 +236,21 @@ static CVReturn displayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
extern "C" { extern "C" {
JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_redrawer_DisplayLinkThrottler_create(JNIEnv *env, jobject obj) { JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_redrawer_DisplayLinkThrottler_create(JNIEnv *env, jobject obj, jlong windowPtr) {
DisplayLinkThrottler *throttler = [[DisplayLinkThrottler alloc] init]; NSWindow *window = (__bridge NSWindow *) (void *) windowPtr;
DisplayLinkThrottler *throttler = [[DisplayLinkThrottler alloc] initWithWindow:window];
return (jlong) (__bridge_retained void *) throttler; return (jlong) (__bridge_retained void *) throttler;
} }
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_DisplayLinkThrottler_dispose(JNIEnv *env, jobject obj, jlong throttlerPtr) { JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_DisplayLinkThrottler_dispose(JNIEnv *env, jobject obj, jlong throttlerPtr) {
DisplayLinkThrottler *throttler = (__bridge_transfer DisplayLinkThrottler *) (void *) throttlerPtr; DisplayLinkThrottler *throttler = (__bridge_transfer DisplayLinkThrottler *) (void *) throttlerPtr;
/// throttler will be released by ARC and deallocated in the end of this scope. // throttler will be released by ARC and deallocated in the end of this scope.
} }
JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_DisplayLinkThrottler_waitVSync(JNIEnv *env, jobject obj, jlong throttlerPtr, jlong windowPtr) { JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_DisplayLinkThrottler_waitVSync(JNIEnv *env, jobject obj, jlong throttlerPtr) {
DisplayLinkThrottler *throttler = (__bridge DisplayLinkThrottler *) (void *) throttlerPtr; DisplayLinkThrottler *throttler = (__bridge DisplayLinkThrottler *) (void *) throttlerPtr;
NSWindow *window = (__bridge NSWindow *) (void *) windowPtr;
// CVDisplayLink is conditionally set up on each draw request to track window.screen change to match throttling
// with the refresh rate of the actual screen the window is shown on.
[throttler setupDisplayLinkForWindow:window];
[throttler waitVSync]; [throttler waitVSync];
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment