From 1377bea3cdc596ae0b458d6906bc1ffa9990329d Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Fri, 29 May 2020 19:01:17 +0200 Subject: [PATCH] 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 --- app/internal/window/gl_macos.m | 37 +--------- app/internal/window/os_darwin.go | 121 +++++++++++++++++++++++++++++++ app/internal/window/os_ios.go | 30 ++++---- app/internal/window/os_ios.m | 61 ++++++++-------- app/internal/window/os_macos.go | 42 +++++------ app/internal/window/os_macos.m | 30 +++++++- 6 files changed, 218 insertions(+), 103 deletions(-) diff --git a/app/internal/window/gl_macos.m b/app/internal/window/gl_macos.m index 65fb84b6..2023f1da 100644 --- a/app/internal/window/gl_macos.m +++ b/app/internal/window/gl_macos.m @@ -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; diff --git a/app/internal/window/os_darwin.go b/app/internal/window/os_darwin.go index 7ccb7b1d..97ac15e6 100644 --- a/app/internal/window/os_darwin.go +++ b/app/internal/window/os_darwin.go @@ -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() + } + } +} diff --git a/app/internal/window/os_ios.go b/app/internal/window/os_ios.go index dfa1bf8c..fa710fa6 100644 --- a/app/internal/window/os_ios.go +++ b/app/internal/window/os_ios.go @@ -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) { diff --git a/app/internal/window/os_ios.m b/app/internal/window/os_ios.m index 23b5bdb5..f25f8a65 100644 --- a/app/internal/window/os_ios.m +++ b/app/internal/window/os_ios.m @@ -9,7 +9,6 @@ #include "framework_ios.h" @interface GioView: UIView -- (void)setAnimating:(BOOL)anim; @end @interface GioViewController : UIViewController @@ -134,20 +133,9 @@ static void handleTouches(int last, UIView *view, NSSet *touches, UIE } @implementation GioView -CADisplayLink *displayLink; NSArray *_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 *_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 *)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. +} diff --git a/app/internal/window/os_macos.go b/app/internal/window/os_macos.go index fbc8b224..d5161a0a 100644 --- a/app/internal/window/os_macos.go +++ b/app/internal/window/os_macos.go @@ -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) diff --git a/app/internal/window/os_macos.m b/app/internal/window/os_macos.m index 19705f8e..42c35b06 100644 --- a/app/internal/window/os_macos.m +++ b/app/internal/window/os_macos.m @@ -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);