diff --git a/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift b/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift index bd4048923..72bfef8c2 100644 --- a/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift +++ b/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift @@ -177,9 +177,7 @@ class MachineDocument: if let machine = self.machine, let openGLView = self.openGLView { // Establish the output aspect ratio and audio. let aspectRatio = self.aspectRatio() - openGLView.perform(glContext: { - machine.setView(openGLView, aspectRatio: Float(aspectRatio.width / aspectRatio.height)) - }) + machine.setView(openGLView, aspectRatio: Float(aspectRatio.width / aspectRatio.height)) // Attach an options panel if one is available. if let optionsPanelNibName = self.machineDescription?.optionsPanelNibName { diff --git a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm index 15fc721fe..b4780eec1 100644 --- a/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm +++ b/OSBindings/Mac/Clock Signal/Machine/CSMachine.mm @@ -33,7 +33,7 @@ #include "../../../../Outputs/OpenGL/ScanTarget.hpp" #include "../../../../Outputs/OpenGL/Screenshot.hpp" -@interface CSMachine() +@interface CSMachine() - (void)speaker:(Outputs::Speaker::Speaker *)speaker didCompleteSamples:(const int16_t *)samples length:(int)length; - (void)speakerDidChangeInputClock:(Outputs::Speaker::Speaker *)speaker; - (void)addLED:(NSString *)led; @@ -151,6 +151,7 @@ struct ActivityObserver: public Activity::Observer { NSMutableArray *_leds; CSHighPrecisionTimer *_timer; + CGSize _pixelSize; std::unique_ptr _scanTarget; } @@ -318,9 +319,10 @@ struct ActivityObserver: public Activity::Observer { - (void)setView:(CSOpenGLView *)view aspectRatio:(float)aspectRatio { _view = view; + _view.displayLinkDelegate = self; [view performWithGLContext:^{ [self setupOutputWithAspectRatio:aspectRatio]; - }]; + } flushDrawable:NO]; } - (void)setupOutputWithAspectRatio:(float)aspectRatio { @@ -329,7 +331,7 @@ struct ActivityObserver: public Activity::Observer { } - (void)updateViewForPixelSize:(CGSize)pixelSize { - self->_scanTarget->update((int)pixelSize.width, (int)pixelSize.height); +// _pixelSize = pixelSize; // @synchronized(self) { // const auto scan_status = _machine->crt_machine()->get_scan_status(); @@ -699,9 +701,31 @@ struct ActivityObserver: public Activity::Observer { #pragma mark - Timer +- (void)openGLView:(CSOpenGLView *)view didUpdateDisplayLink:(CVDisplayLinkRef)displayLink { +} + +- (void)openGLViewDisplayLinkDidFire:(CSOpenGLView *)view { + @synchronized(self) { + _pixelSize = view.backingSize; + } + [self.view performWithGLContext:^{ + self->_scanTarget->draw((int)self->_pixelSize.width, (int)self->_pixelSize.height); + } flushDrawable:YES]; +} + - (void)start { _timer = [[CSHighPrecisionTimer alloc] initWithTask:^{ - self->_machine->crt_machine()->run_for(2500000.0 / 1000000000.0); + CGSize pixelSize; + @synchronized(self) { + self->_machine->crt_machine()->run_for(2500000.0 / 1000000000.0); + pixelSize = self->_pixelSize; + } + + dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{ + [self.view performWithGLContext:^{ + self->_scanTarget->update((int)pixelSize.width, (int)pixelSize.height); + } flushDrawable:NO]; + }); } interval:2500000]; } diff --git a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h index 25920e578..a46d64752 100644 --- a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h +++ b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h @@ -101,6 +101,29 @@ typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) { @end +/*! + Although I'm still on the fence about this as a design decision, CSOpenGLView is itself responsible + for creating and destroying a CVDisplayLink. There's a practical reason for this: you'll get real synchronisation + only if a link is explicitly tied to a particular display, and the CSOpenGLView therefore owns the knowledge + necessary to decide when to create and modify them. It doesn't currently just propagate "did change screen"-type + messages because I haven't yet found a way to track that other than polling, in which case I might as well put + that into the display link callback. +*/ +@protocol CSOpenGLViewDisplayLinkDelegate + +/*! + Informs the delegate that from now on, the display link @c displayLink will be used for update notifications + and/or that the frequency or phase or @c displayLink has changed. +*/ +- (void)openGLView:(nonnull CSOpenGLView *)view didUpdateDisplayLink:(nonnull CVDisplayLinkRef)displayLink; + +/*! + Informs the delegate that the display link has fired. +*/ +- (void)openGLViewDisplayLinkDidFire:(nonnull CSOpenGLView *)view; + +@end + /*! Provides an OpenGL canvas with a refresh-linked update timer that can forward a subset of typical first-responder actions. @@ -109,6 +132,7 @@ typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) { @property (atomic, weak, nullable) id delegate; @property (nonatomic, weak, nullable) id responderDelegate; +@property (atomic, weak, nullable) id displayLinkDelegate; /// Determines whether the view offers mouse capturing — i.e. if the user clicks on the view then /// then the system cursor is disabled and the mouse events defined by CSOpenGLViewResponderDelegate @@ -139,6 +163,7 @@ typedef NS_ENUM(NSInteger, CSOpenGLViewRedrawEvent) { Locks this view's OpenGL context and makes it current, performs @c action and then unlocks the context. @c action is performed on the calling queue. */ +- (void)performWithGLContext:(nonnull dispatch_block_t)action flushDrawable:(BOOL)flushDrawable; - (void)performWithGLContext:(nonnull dispatch_block_t)action; /*! diff --git a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m index c1a57dee0..eb783f3a1 100644 --- a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m +++ b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m @@ -30,11 +30,6 @@ // Note the initial screen. _currentScreen = self.window.screen; - // Synchronize buffer swaps with vertical refresh rate. - // TODO: discard this, once scheduling is sufficiently intelligent? - GLint swapInt = 1; - [[self openGLContext] setValues:&swapInt forParameter:NSOpenGLCPSwapInterval]; - // set the clear colour [self.openGLContext makeCurrentContext]; glClearColor(0.0, 0.0, 0.0, 1.0); @@ -68,6 +63,9 @@ CGLPixelFormatObj cglPixelFormat = [[self pixelFormat] CGLPixelFormatObj]; CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(_displayLink, cglContext, cglPixelFormat); + // Give a shout-out. + [self.displayLinkDelegate openGLView:self didUpdateDisplayLink:_displayLink]; + // Activate the display link CVDisplayLinkStart(_displayLink); } @@ -75,7 +73,8 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *now, const CVTimeStamp *outputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext) { CSOpenGLView *const view = (__bridge CSOpenGLView *)displayLinkContext; - [view drawAtTime:now frequency:CVDisplayLinkGetActualOutputVideoRefreshPeriod(displayLink)]; + [view checkDisplayLink]; + [view.displayLinkDelegate openGLViewDisplayLinkDidFire:view]; /* Do not touch the display link from after this call; there's a bit of a race condition with setupDisplayLink. Specifically: Apple provides CVDisplayLinkStop but a call to that merely prevents future calls to the callback, @@ -89,7 +88,7 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt return kCVReturnSuccess; } -- (void)drawAtTime:(const CVTimeStamp *)now frequency:(double)frequency { +- (void)checkDisplayLink { // Test now whether the screen this view is on has changed since last time it was checked. // There's likely a callback available for this, on NSWindow if nowhere else, or an NSNotification, // but since this method is going to be called repeatedly anyway, and the test is cheap, polling @@ -105,7 +104,9 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt // the window is actually on, and at its rate. [self setupDisplayLink]; } +} +- (void)drawAtTime:(const CVTimeStamp *)now frequency:(double)frequency { [self redrawWithEvent:CSOpenGLViewRedrawEventTimer]; } @@ -113,11 +114,10 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt [self redrawWithEvent:CSOpenGLViewRedrawEventAppKit]; } -- (void)redrawWithEvent:(CSOpenGLViewRedrawEvent)event { +- (void)redrawWithEvent:(CSOpenGLViewRedrawEvent)event { [self performWithGLContext:^{ [self.delegate openGLViewRedraw:self event:event]; - CGLFlushDrawable([[self openGLContext] CGLContextObj]); - }]; + } flushDrawable:YES]; } - (void)invalidate { @@ -145,7 +145,7 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt [self performWithGLContext:^{ CGSize viewSize = [self backingSize]; glViewport(0, 0, (GLsizei)viewSize.width, (GLsizei)viewSize.height); - }]; + } flushDrawable:NO]; } - (void)awakeFromNib { @@ -178,11 +178,17 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeSt [self registerForDraggedTypes:@[(__bridge NSString *)kUTTypeFileURL]]; } -- (void)performWithGLContext:(dispatch_block_t)action { +- (void)performWithGLContext:(dispatch_block_t)action flushDrawable:(BOOL)flushDrawable { CGLLockContext([[self openGLContext] CGLContextObj]); [self.openGLContext makeCurrentContext]; action(); CGLUnlockContext([[self openGLContext] CGLContextObj]); + + if(flushDrawable) CGLFlushDrawable([[self openGLContext] CGLContextObj]); +} + +- (void)performWithGLContext:(nonnull dispatch_block_t)action { + [self performWithGLContext:action flushDrawable:NO]; } #pragma mark - NSResponder