mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
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:
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user