app/internal/window: [macOS,iOS] reduce display link starting and stopping

Recent changes to the macOS threading exposed a problem where a
window's display link may fail to start after being started and stopped
in rapid succession.

Introduce a displayLink type that waits a while after the last stop
request before stopping its display link. That seems to be the way
other projects are using display links.

As a bonus, the new implementation avoids the potentially expensive
overhead of frequent starting and stopping the underlying OS thread.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2020-05-29 19:01:17 +02:00
parent 3817941175
commit 1377bea3cd
6 changed files with 218 additions and 103 deletions
+1 -36
View File
@@ -19,39 +19,14 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
gio_onMouse((__bridge CFTypeRef)view, typ, [NSEvent pressedMouseButtons], p.x, p.y, dx, dy, [event timestamp], [event modifierFlags]);
}
static CVReturn displayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext) {
CFTypeRef view = (CFTypeRef *)displayLinkContext;
gio_onFrameCallback(view);
return kCVReturnSuccess;
}
@interface GioView : NSOpenGLView
@end
@implementation GioView {
CVDisplayLinkRef displayLink;
}
- (instancetype)initWithFrame:(NSRect)frameRect
pixelFormat:(NSOpenGLPixelFormat *)format {
self = [super initWithFrame:frameRect pixelFormat:format];
if (self) {
CVDisplayLinkCreateWithActiveCGDisplays(&displayLink);
CVDisplayLinkSetOutputCallback(displayLink, displayLinkCallback, (__bridge void*)self);
}
return self;
}
- (void)dealloc {
CVDisplayLinkRelease(displayLink);
}
- (void)setAnimating:(BOOL)anim {
if (anim) {
CVDisplayLinkStart(displayLink);
} else {
CVDisplayLinkStop(displayLink);
}
}
- (void)updateDisplay:(CGDirectDisplayID)dispID {
CVDisplayLinkSetCurrentCGDisplay(displayLink, dispID);
return [super initWithFrame:frameRect pixelFormat:format];
}
- (void)prepareOpenGL {
[super prepareOpenGL];
@@ -139,16 +114,6 @@ CFTypeRef gio_createGLView(void) {
}
}
void gio_updateDisplayLink(CFTypeRef viewRef, CGDirectDisplayID dispID) {
GioView *view = (__bridge GioView *)viewRef;
[view updateDisplay:dispID];
}
void gio_setAnimating(CFTypeRef viewRef, BOOL anim) {
GioView *view = (__bridge GioView *)viewRef;
[view setAnimating:anim];
}
CFTypeRef gio_contextForView(CFTypeRef viewRef) {
NSOpenGLView *view = (__bridge NSOpenGLView *)viewRef;
return (__bridge CFTypeRef)view.openGLContext;
+121
View File
@@ -8,13 +8,44 @@ package window
__attribute__ ((visibility ("hidden"))) void gio_wakeupMainThread(void);
__attribute__ ((visibility ("hidden"))) NSUInteger gio_nsstringLength(CFTypeRef str);
__attribute__ ((visibility ("hidden"))) void gio_nsstringGetCharacters(CFTypeRef str, unichar *chars, NSUInteger loc, NSUInteger length);
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_createDisplayLink(void);
__attribute__ ((visibility ("hidden"))) void gio_releaseDisplayLink(CFTypeRef dl);
__attribute__ ((visibility ("hidden"))) int gio_startDisplayLink(CFTypeRef dl);
__attribute__ ((visibility ("hidden"))) int gio_stopDisplayLink(CFTypeRef dl);
__attribute__ ((visibility ("hidden"))) void gio_setDisplayLinkDisplay(CFTypeRef dl, uint64_t did);
*/
import "C"
import (
"errors"
"sync"
"sync/atomic"
"time"
"unicode/utf16"
"unsafe"
)
// displayLink is the state for a display link (CVDisplayLinkRef on macOS,
// CADisplayLink on iOS). It runs a state-machine goroutine that keeps the
// display link running for a while after being stopped to avoid the thread
// start/stop overhead and because the CVDisplayLink sometimes fails to
// start, stop and start again within a short duration.
type displayLink struct {
callback func()
// states is for starting or stopping the display link.
states chan bool
// done is closed when the display link is destroyed.
done chan struct{}
// dids receives the display id when the callback owner is moved
// to a different screen.
dids chan uint64
// running tracks the desired state of the link. running is accessed
// with atomic.
running uint32
}
// displayLinks maps CFTypeRefs to *displayLinks.
var displayLinks sync.Map
var mainFuncs = make(chan func(), 1)
// runOnMain runs the function on the main thread.
@@ -47,3 +78,93 @@ func nsstringToString(str C.CFTypeRef) string {
utf8 := utf16.Decode(chars)
return string(utf8)
}
func NewDisplayLink(callback func()) (*displayLink, error) {
d := &displayLink{
callback: callback,
done: make(chan struct{}),
states: make(chan bool),
dids: make(chan uint64),
}
dl := C.gio_createDisplayLink()
if dl == 0 {
return nil, errors.New("app: failed to create display link")
}
go d.run(dl)
return d, nil
}
func (d *displayLink) run(dl C.CFTypeRef) {
defer C.gio_releaseDisplayLink(dl)
displayLinks.Store(dl, d)
defer displayLinks.Delete(dl)
var stopTimer *time.Timer
var tchan <-chan time.Time
started := false
for {
select {
case <-tchan:
tchan = nil
started = false
C.gio_stopDisplayLink(dl)
case start := <-d.states:
switch {
case !start && tchan == nil:
// stopTimeout is the delay before stopping the display link to
// avoid the overhead of frequently starting and stopping the
// link thread.
const stopTimeout = 500 * time.Millisecond
if stopTimer == nil {
stopTimer = time.NewTimer(stopTimeout)
} else {
// stopTimer is always drained when tchan == nil.
stopTimer.Reset(stopTimeout)
}
tchan = stopTimer.C
atomic.StoreUint32(&d.running, 0)
case start:
if tchan != nil && !stopTimer.Stop() {
<-tchan
}
tchan = nil
atomic.StoreUint32(&d.running, 1)
if !started {
started = true
if res := C.gio_startDisplayLink(dl); res != 0 {
panic("failed to start display link")
}
}
}
case did := <-d.dids:
C.gio_setDisplayLinkDisplay(dl, C.uint64_t(did))
case <-d.done:
return
}
}
}
func (d *displayLink) Start() {
d.states <- true
}
func (d *displayLink) Stop() {
d.states <- false
}
func (d *displayLink) Close() {
close(d.done)
}
func (d *displayLink) SetDisplayID(did uint64) {
d.dids <- did
}
//export gio_onFrameCallback
func gio_onFrameCallback(dl C.CFTypeRef) {
if d, exists := displayLinks.Load(dl); exists {
d := d.(*displayLink)
if atomic.LoadUint32(&d.running) != 0 {
d.callback()
}
}
}
+14 -16
View File
@@ -22,7 +22,6 @@ __attribute__ ((visibility ("hidden"))) void gio_hideTextInput(CFTypeRef viewRef
__attribute__ ((visibility ("hidden"))) void gio_addLayerToView(CFTypeRef viewRef, CFTypeRef layerRef);
__attribute__ ((visibility ("hidden"))) void gio_updateView(CFTypeRef viewRef, CFTypeRef layerRef);
__attribute__ ((visibility ("hidden"))) void gio_removeLayer(CFTypeRef layerRef);
__attribute__ ((visibility ("hidden"))) void gio_setAnimating(CFTypeRef viewRef, int anim);
__attribute__ ((visibility ("hidden"))) struct drawParams gio_viewDrawParams(CFTypeRef viewRef);
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_readClipboard(void);
__attribute__ ((visibility ("hidden"))) void gio_writeClipboard(unichar *chars, NSUInteger length);
@@ -46,8 +45,9 @@ import (
)
type window struct {
view C.CFTypeRef
w Callbacks
view C.CFTypeRef
w Callbacks
displayLink *displayLink
layer C.CFTypeRef
visible atomic.Value
@@ -71,6 +71,13 @@ func onCreate(view C.CFTypeRef) {
w := &window{
view: view,
}
dl, err := NewDisplayLink(func() {
w.draw(false)
})
if err != nil {
panic(err)
}
w.displayLink = dl
wopts := <-mainWindow.out
w.w = wopts.window
w.w.SetDriver(w)
@@ -81,12 +88,6 @@ func onCreate(view C.CFTypeRef) {
w.w.Event(system.StageEvent{Stage: system.StagePaused})
}
//export gio_onFrameCallback
func gio_onFrameCallback(view C.CFTypeRef) {
w := views[view]
w.draw(false)
}
//export gio_onDraw
func gio_onDraw(view C.CFTypeRef) {
w := views[view]
@@ -139,6 +140,7 @@ func onDestroy(view C.CFTypeRef) {
w := views[view]
delete(views, view)
w.w.Event(system.DestroyEvent{})
w.displayLink.Close()
C.gio_removeLayer(w.layer)
C.CFRelease(w.layer)
w.layer = 0
@@ -240,15 +242,11 @@ func (w *window) SetAnimating(anim bool) {
if v == 0 {
return
}
var animi C.int
if anim {
animi = 1
w.displayLink.Start()
} else {
w.displayLink.Stop()
}
C.CFRetain(v)
runOnMain(func() {
defer C.CFRelease(v)
C.gio_setAnimating(v, animi)
})
}
func (w *window) onKeyCommand(name string) {
+32 -29
View File
@@ -9,7 +9,6 @@
#include "framework_ios.h"
@interface GioView: UIView <UIKeyInput>
- (void)setAnimating:(BOOL)anim;
@end
@interface GioViewController : UIViewController
@@ -134,20 +133,9 @@ static void handleTouches(int last, UIView *view, NSSet<UITouch *> *touches, UIE
}
@implementation GioView
CADisplayLink *displayLink;
NSArray<UIKeyCommand *> *_keyCommands;
- (void)onFrameCallback:(CADisplayLink *)link {
gio_onFrameCallback((__bridge CFTypeRef)self);
}
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
__weak id weakSelf = self;
displayLink = [CADisplayLink displayLinkWithTarget:weakSelf selector:@selector(onFrameCallback:)];
}
return self;
+ (void)onFrameCallback:(CADisplayLink *)link {
gio_onFrameCallback((__bridge CFTypeRef)link);
}
- (void)willMoveToWindow:(UIWindow *)newWindow {
@@ -182,16 +170,6 @@ NSArray<UIKeyCommand *> *_keyCommands;
}
- (void)dealloc {
[displayLink invalidate];
}
- (void)setAnimating:(BOOL)anim {
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
if (anim) {
[displayLink addToRunLoop:runLoop forMode:[runLoop currentMode]];
} else {
[displayLink removeFromRunLoop:runLoop forMode:[runLoop currentMode]];
}
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
@@ -281,11 +259,6 @@ CFTypeRef gio_readClipboard(void) {
}
}
void gio_setAnimating(CFTypeRef viewRef, int anim) {
GioView *view = (__bridge GioView *)viewRef;
[view setAnimating:(anim ? YES : NO)];
}
void gio_showTextInput(CFTypeRef viewRef) {
UIView *view = (__bridge UIView *)viewRef;
[view becomeFirstResponder];
@@ -335,3 +308,33 @@ struct drawParams gio_viewDrawParams(CFTypeRef viewRef) {
params.left = insets.left*scale;
return params;
}
CFTypeRef gio_createDisplayLink(void) {
CADisplayLink *dl = [CADisplayLink displayLinkWithTarget:[GioView class] selector:@selector(onFrameCallback:)];
dl.paused = YES;
NSRunLoop *runLoop = [NSRunLoop mainRunLoop];
[dl addToRunLoop:runLoop forMode:[runLoop currentMode]];
return (__bridge_retained CFTypeRef)dl;
}
int gio_startDisplayLink(CFTypeRef dlref) {
CADisplayLink *dl = (__bridge CADisplayLink *)dlref;
dl.paused = NO;
return 0;
}
int gio_stopDisplayLink(CFTypeRef dlref) {
CADisplayLink *dl = (__bridge CADisplayLink *)dlref;
dl.paused = YES;
return 0;
}
void gio_releaseDisplayLink(CFTypeRef dlref) {
CADisplayLink *dl = (__bridge CADisplayLink *)dlref;
[dl invalidate];
CFRelease(dlref);
}
void gio_setDisplayLinkDisplay(CFTypeRef dl, uint64_t did) {
// Nothing to do on iOS.
}
+21 -21
View File
@@ -34,8 +34,6 @@ import (
__attribute__ ((visibility ("hidden"))) void gio_main(CFTypeRef viewRef, const char *title, CGFloat width, CGFloat height);
__attribute__ ((visibility ("hidden"))) CGFloat gio_viewWidth(CFTypeRef viewRef);
__attribute__ ((visibility ("hidden"))) CGFloat gio_viewHeight(CFTypeRef viewRef);
__attribute__ ((visibility ("hidden"))) void gio_setAnimating(CFTypeRef viewRef, BOOL anim);
__attribute__ ((visibility ("hidden"))) void gio_updateDisplayLink(CFTypeRef viewRef, CGDirectDisplayID dispID);
__attribute__ ((visibility ("hidden"))) CGFloat gio_getViewBackingScale(CFTypeRef viewRef);
__attribute__ ((visibility ("hidden"))) CFTypeRef gio_readClipboard(void);
__attribute__ ((visibility ("hidden"))) void gio_writeClipboard(unichar *chars, NSUInteger length);
@@ -48,9 +46,10 @@ func init() {
}
type window struct {
view C.CFTypeRef
w Callbacks
stage system.Stage
view C.CFTypeRef
w Callbacks
stage system.Stage
displayLink *displayLink
// mu protect the following fields
mu sync.Mutex
@@ -116,16 +115,11 @@ func (w *window) WriteClipboard(s string) {
func (w *window) ShowTextInput(show bool) {}
func (w *window) SetAnimating(anim bool) {
var animb C.BOOL
if anim {
animb = 1
w.displayLink.Start()
} else {
w.displayLink.Stop()
}
v := w.view
C.CFRetain(v)
runOnMain(func() {
defer C.CFRelease(v)
C.gio_setAnimating(v, animb)
})
}
func (w *window) setStage(stage system.Stage) {
@@ -136,14 +130,6 @@ func (w *window) setStage(stage system.Stage) {
w.w.Event(system.StageEvent{Stage: stage})
}
//export gio_onFrameCallback
func gio_onFrameCallback(view C.CFTypeRef) {
w, exists := lookupView(view)
if exists {
w.draw(false)
}
}
//export gio_onKeys
func gio_onKeys(view C.CFTypeRef, cstr *C.char, ti C.double, mods C.NSUInteger) {
str := C.GoString(cstr)
@@ -222,6 +208,12 @@ func gio_onFocus(view C.CFTypeRef, focus C.BOOL) {
w.w.Event(key.FocusEvent{Focus: focus == C.YES})
}
//export gio_onChangeScreen
func gio_onChangeScreen(view C.CFTypeRef, did uint64) {
w := mustView(view)
w.displayLink.SetDisplayID(did)
}
func (w *window) draw(sync bool) {
w.mu.Lock()
wf, hf, scale := w.width, w.height, w.scale
@@ -256,6 +248,7 @@ func configFor(scale float32) config {
//export gio_onTerminate
func gio_onTerminate(view C.CFTypeRef) {
w := mustView(view)
w.displayLink.Close()
deleteView(view)
w.w.Event(system.DestroyEvent{})
}
@@ -279,6 +272,13 @@ func gio_onCreate(view C.CFTypeRef) {
view: view,
scale: scale,
}
dl, err := NewDisplayLink(func() {
w.draw(false)
})
if err != nil {
panic(err)
}
w.displayLink = dl
wopts := <-mainWindow.out
w.w = wopts.window
w.w.SetDriver(w)
+29 -1
View File
@@ -31,7 +31,7 @@
- (void)windowDidChangeScreen:(NSNotification *)notification {
CGDirectDisplayID dispID = [[[self.window screen] deviceDescription][@"NSScreenNumber"] unsignedIntValue];
CFTypeRef view = (__bridge CFTypeRef)self.window.contentView;
gio_updateDisplayLink(view, dispID);
gio_onChangeScreen(view, dispID);
}
- (void)windowDidBecomeKey:(NSNotification *)notification {
gio_onFocus((__bridge CFTypeRef)self.window.contentView, YES);
@@ -81,6 +81,34 @@ CGFloat gio_getViewBackingScale(CFTypeRef viewRef) {
return [view.window backingScaleFactor];
}
static CVReturn displayLinkCallback(CVDisplayLinkRef dl, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext) {
gio_onFrameCallback(dl);
return kCVReturnSuccess;
}
CFTypeRef gio_createDisplayLink(void) {
CVDisplayLinkRef dl;
CVDisplayLinkCreateWithActiveCGDisplays(&dl);
CVDisplayLinkSetOutputCallback(dl, displayLinkCallback, nil);
return dl;
}
int gio_startDisplayLink(CFTypeRef dl) {
return CVDisplayLinkStart((CVDisplayLinkRef)dl);
}
int gio_stopDisplayLink(CFTypeRef dl) {
return CVDisplayLinkStop((CVDisplayLinkRef)dl);
}
void gio_releaseDisplayLink(CFTypeRef dl) {
CVDisplayLinkRelease((CVDisplayLinkRef)dl);
}
void gio_setDisplayLinkDisplay(CFTypeRef dl, uint64_t did) {
CVDisplayLinkSetCurrentCGDisplay((CVDisplayLinkRef)dl, (CGDirectDisplayID)did);
}
void gio_main(CFTypeRef viewRef, const char *title, CGFloat width, CGFloat height) {
@autoreleasepool {
NSView *view = (NSView *)CFBridgingRelease(viewRef);