diff --git a/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift b/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift index 4801f6c86..473e81aad 100644 --- a/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift +++ b/OSBindings/Mac/Clock Signal/Documents/MachineDocument.swift @@ -35,19 +35,29 @@ class MachineDocument: NSDocument, CSOpenGLViewDelegate, CSOpenGLViewResponderDe var intendedCyclesPerSecond: Int64 = 0 private var cycleCountError: Int64 = 0 private var lastTime: CVTimeStamp? - final func openGLView(view: CSOpenGLView, didUpdateToTime time: CVTimeStamp) { + private var skippedFrames = 0 + final func openGLView(view: CSOpenGLView, didUpdateToTime time: CVTimeStamp, didSkipPreviousUpdate : Bool, frequency : Double) { if let lastTime = lastTime { // perform (time passed in seconds) * (intended cycles per second), converting and // maintaining an error count to deal with underflow let videoTimeScale64 = Int64(time.videoTimeScale) let videoTimeCount = ((time.videoTime - lastTime.videoTime) * intendedCyclesPerSecond) + cycleCountError cycleCountError = videoTimeCount % videoTimeScale64 - let numberOfCycles = videoTimeCount / videoTimeScale64 + var numberOfCycles = videoTimeCount / videoTimeScale64 - // if the emulation has fallen too far behind then silently limit the request; + // if the emulation has fallen behind then silently limit the request; // some actions — e.g. the host computer waking after sleep — may give us a // prohibitive backlog - runForNumberOfCycles(min(Int32(numberOfCycles), Int32(intendedCyclesPerSecond / 25))) + if didSkipPreviousUpdate { + skippedFrames++ + } else { + skippedFrames = 0 + } + + if skippedFrames > 4 { + numberOfCycles = min(numberOfCycles, Int64(Double(intendedCyclesPerSecond) * frequency)) + } + runForNumberOfCycles(Int32(numberOfCycles)) } lastTime = time } diff --git a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h index 250689c1b..f5c821fe8 100644 --- a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h +++ b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.h @@ -13,12 +13,12 @@ @protocol CSOpenGLViewDelegate /*! - Tells the delegate that time has advanced. This method will always be called on the same queue - as the @c CSOpenGLViewResponderDelegate methods. + Tells the delegate that time has advanced. @param view The view sending the message. @param time The time to which time has advanced. + @param didSkipPreviousUpdate @c YES if the previous update that would have occurred was skipped because a didUpdateToTime: call prior to that was still ongoing; @c NO otherwise. */ -- (void)openGLView:(nonnull CSOpenGLView *)view didUpdateToTime:(CVTimeStamp)time; +- (void)openGLView:(nonnull CSOpenGLView *)view didUpdateToTime:(CVTimeStamp)time didSkipPreviousUpdate:(BOOL)didSkipPreviousUpdate frequency:(double)frequency; /*! Requests that the delegate produce an image of its current output state. May be called on diff --git a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m index 797b0f1b0..8e41cc50e 100644 --- a/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m +++ b/OSBindings/Mac/Clock Signal/Views/CSOpenGLView.m @@ -13,6 +13,7 @@ @implementation CSOpenGLView { CVDisplayLinkRef _displayLink; uint32_t _updateIsOngoing; + BOOL _hasSkipped; } - (void)prepareOpenGL @@ -23,7 +24,7 @@ // Create a display link capable of being used with all active displays CVDisplayLinkCreateWithActiveCGDisplays(&_displayLink); - + // Set the renderer output callback function CVDisplayLinkSetOutputCallback(_displayLink, DisplayLinkCallback, (__bridge void * __nullable)(self)); @@ -42,25 +43,36 @@ static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *now, const CVTimeStamp *outputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext) { - CSOpenGLView *view = (__bridge CSOpenGLView *)displayLinkContext; - [view drawAtTime:now]; + CSOpenGLView *const view = (__bridge CSOpenGLView *)displayLinkContext; + [view drawAtTime:now frequency:CVDisplayLinkGetActualOutputVideoRefreshPeriod(displayLink)]; return kCVReturnSuccess; } -- (void)drawAtTime:(const CVTimeStamp *)now +- (void)drawAtTime:(const CVTimeStamp *)now frequency:(double)frequency { + const uint32_t processingMask = 0x01; + const uint32_t drawingMask = 0x02; + // Always post a -openGLView:didUpdateToTime:. This is the hook upon which the substantial processing occurs. - [self.delegate openGLView:self didUpdateToTime:*now]; + if(!OSAtomicTestAndSet(processingMask, &_updateIsOngoing)) + { + CVTimeStamp time = *now; + BOOL didSkip = _hasSkipped; + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ + [self.delegate openGLView:self didUpdateToTime:time didSkipPreviousUpdate:didSkip frequency:frequency]; + OSAtomicTestAndClear(processingMask, &_updateIsOngoing); + }); + _hasSkipped = NO; + } else _hasSkipped = YES; // Draw the display only if a previous draw is not still ongoing. -drawViewOnlyIfDirty: is guaranteed // to be safe to call concurrently with -openGLView:updateToTime: so there's no need to worry about // the above interrupting the below or vice versa. - const uint32_t activityMask = 0x01; - if(!OSAtomicTestAndSet(activityMask, &_updateIsOngoing)) + if(!OSAtomicTestAndSet(drawingMask, &_updateIsOngoing)) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ [self drawViewOnlyIfDirty:YES]; - OSAtomicTestAndClear(activityMask, &_updateIsOngoing); + OSAtomicTestAndClear(drawingMask, &_updateIsOngoing); }); } }