Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Speculative fix for a crash #959

Merged
merged 7 commits into from
Jul 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,22 @@ package org.jetbrains.skiko.redrawer

import org.jetbrains.skiko.Library

internal class DisplayLinkThrottler {
private val implPtr = create()
internal class DisplayLinkThrottler(windowPtr: Long) {
private val implPtr = create(windowPtr)

internal fun dispose() = dispose(implPtr)

/*
* 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].
*/
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 waitVSync(implPtr: Long, windowPtr: Long)
private external fun waitVSync(implPtr: Long)

companion object {
init {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ internal class MetalRedrawer(
}

private val adapter = chooseMetalAdapter(properties.adapterPriority)
private val displayLinkThrottler = DisplayLinkThrottler()
private val displayLinkThrottler = DisplayLinkThrottler(layer.windowHandle)

private val windowOcclusionStateChannel = Channel<Boolean>(Channel.CONFLATED)
@Volatile private var isWindowOccluded = false
Expand Down Expand Up @@ -159,7 +159,7 @@ internal class MetalRedrawer(
// Wait for vsync because:
// - macOS drops the second/next drawables if they are sent in the same vsync
// - it makes frames consistent and limits FPS
displayLinkThrottler.waitVSync(windowPtr = layer.windowHandle)
displayLinkThrottler.waitVSync()

autoreleasepool {
contextHandler.draw()
Expand Down
191 changes: 137 additions & 54 deletions skiko/src/awtMain/objectiveC/macos/DisplayLinkThrottler.mm
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,33 @@
#import <QuartzCore/QuartzCore.h>
#import <AppKit/AppKit.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

Expand All @@ -21,125 +48,184 @@ static CVReturn displayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt
}

@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;

// Lock to protect internal state
NSLock *_lock;
NSConditionLock *_vsyncConditionLock;
BOOL _isSleeping;
__weak NSWindow *_window;
}

- (instancetype)init {
- (instancetype)initWithWindow:(NSWindow *)window {
self = [super init];

if (self) {
_displayLinkScreen = nil;
_currentScreenID = -1;
_window = window;
_displayLink = nil;
_vsyncConditionLock = [[NSConditionLock alloc] initWithCondition: 1];
_lock = [NSLock new];
_isSleeping = NO;

NSNotificationCenter *notificationCenter = [[NSWorkspace sharedWorkspace] notificationCenter];

[notificationCenter addObserver:self
selector:@selector(systemWillSleep:)
selector:@selector(systemWillSleep)
name:NSWorkspaceWillSleepNotification
object:nil];

[notificationCenter addObserver:self
selector:@selector(systemDidWake:)
selector:@selector(systemDidWake)
name:NSWorkspaceDidWakeNotification
object:nil];

__weak DisplayLinkThrottler *weakSelf = self;
[window skiko_setupOnScreenChangeCallback:^{
[weakSelf onScreenDidChange];
}];

[self onScreenDidChange];
}

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;
[self invalidateDisplayLink];
[self updateDisplayLink];
[self onVSync];

[_lock unlock];
}

- (void)systemDidWake:(NSNotification *)notification {
- (void)systemDidWake {
[_lock lock];

_isSleeping = NO;
[self updateDisplayLink];

[_lock unlock];
}

- (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 unlockWithCondition:1];
}

- (void)waitVSync {
/// If display link is not constructed (due to failure or explicit opt-out), don't perform any waiting
if (!_displayLink) {
BOOL hasDisplayLink;

[_lock lock];
hasDisplayLink = _displayLink != nil;
[_lock unlock];

// No display link to wait for
if (!hasDisplayLink) {
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 unlockWithCondition:0];
}

- (void)invalidateDisplayLink {
// Assumed to be run inside _lock, except for dealloc
if (_displayLink) {
CVDisplayLinkStop(_displayLink);
CVDisplayLinkRelease(_displayLink);
_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) {
/// window is not yet attached to screen (or window is nil)
return;
}
[self invalidateDisplayLink];

if ([screen isEqualTo: _displayLinkScreen]) {
/// display link is already active for this screen, do nothing
if (_currentScreenID < 0) {
return;
}

[self invalidateDisplayLink];

NSDictionary* screenDescription = [screen deviceDescription];
NSNumber* screenID = [screenDescription objectForKey:@"NSScreenNumber"];

_displayLink = [self createDisplayLinkForScreen:screenID];
_displayLinkScreen = screen;
}

- (CVDisplayLinkRef)createDisplayLinkForScreen:(NSNumber *)screenID {
CVReturn result;
CVDisplayLinkRef displayLink;

result = CVDisplayLinkCreateWithCGDisplay([screenID unsignedIntValue], &displayLink);
result = CVDisplayLinkCreateWithCGDisplay(
_currentScreenID /* truncated to uint32_t aka CGDirectDisplayID */,
&_displayLink
);

if (result != kCVReturnSuccess) {
return nil;
_displayLink = nil;
igordmn marked this conversation as resolved.
Show resolved Hide resolved
return;
}

result = CVDisplayLinkSetOutputCallback(displayLink, &displayLinkCallback, (__bridge void *)(self));
result = CVDisplayLinkSetOutputCallback(_displayLink, &displayLinkCallback, (__bridge void *)(self));

if (result != kCVReturnSuccess) {
CVDisplayLinkRelease(displayLink);
return nil;
CVDisplayLinkRelease(_displayLink);
_displayLink = nil;
return;
}

result = CVDisplayLinkStart(displayLink);
result = CVDisplayLinkStart(_displayLink);

if (result != kCVReturnSuccess) {
CVDisplayLinkRelease(displayLink);
return nil;
CVDisplayLinkRelease(_displayLink);
_displayLink = nil;
return;
}

return displayLink;
_displayLinkScreenID = _currentScreenID;

}

- (void)dealloc {
Expand All @@ -150,24 +236,21 @@ - (void)dealloc {

extern "C" {

JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_redrawer_DisplayLinkThrottler_create(JNIEnv *env, jobject obj) {
DisplayLinkThrottler *throttler = [[DisplayLinkThrottler alloc] init];
JNIEXPORT jlong JNICALL Java_org_jetbrains_skiko_redrawer_DisplayLinkThrottler_create(JNIEnv *env, jobject obj, jlong windowPtr) {
NSWindow *window = (__bridge NSWindow *) (void *) windowPtr;
DisplayLinkThrottler *throttler = [[DisplayLinkThrottler alloc] initWithWindow:window];

return (jlong) (__bridge_retained void *) throttler;
}

JNIEXPORT void JNICALL Java_org_jetbrains_skiko_redrawer_DisplayLinkThrottler_dispose(JNIEnv *env, jobject obj, jlong 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;
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];
}

Expand Down
Loading