1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-07-26 11:29:09 +00:00
CLK/OSBindings/Mac/Clock Signal/Views/CSScanTargetView.m
Thomas Harte 645c29f853 Adds an intermediate buffer to correct inter-frame smoothing.
Also goes someway back to the old scan output scheduling, albeit presently with limited thread safety.
2020-08-15 21:24:10 -04:00

415 lines
12 KiB
Objective-C

//
// CSScanTargetView
// CLK
//
// Created by Thomas Harte on 16/07/2015.
// Copyright 2015 Thomas Harte. All rights reserved.
//
#import "CSScanTargetView.h"
#import "CSApplication.h"
#import "CSScanTarget.h"
@import CoreVideo;
@import GLKit;
#include <stdatomic.h>
@interface CSScanTargetView () <NSDraggingDestination, CSApplicationEventDelegate>
@end
@implementation CSScanTargetView {
CVDisplayLinkRef _displayLink;
CGSize _backingSize;
NSNumber *_currentScreenNumber;
NSTrackingArea *_mouseTrackingArea;
NSTimer *_mouseHideTimer;
BOOL _mouseIsCaptured;
atomic_int _isDrawingFlag;
BOOL _isInvalid;
CSScanTarget *_scanTarget;
}
//- (void)prepareOpenGL {
// [super prepareOpenGL];
//
// // Prepare the atomic int.
// atomic_init(&_isDrawingFlag, 0);
//
// // Set the clear colour.
// [self.openGLContext makeCurrentContext];
// glClearColor(0.0, 0.0, 0.0, 1.0);
//
// // Setup the [initial] display link.
// [self setupDisplayLink];
//}
- (void)setupDisplayLink {
// Kill the existing link if there is one.
if(_displayLink) {
[self stopDisplayLink];
CVDisplayLinkRelease(_displayLink);
}
// Create a display link for the display the window is currently on.
NSNumber *const screenNumber = self.window.screen.deviceDescription[@"NSScreenNumber"];
_currentScreenNumber = screenNumber;
CVDisplayLinkCreateWithCGDisplay(screenNumber.unsignedIntValue, &_displayLink);
// Set the renderer output callback function.
CVDisplayLinkSetOutputCallback(_displayLink, DisplayLinkCallback, (__bridge void * __nullable)(self));
// Set the display link for the current renderer.
// CGLContextObj cglContext = [[self openGLContext] CGLContextObj];
// CGLPixelFormatObj cglPixelFormat = [[self pixelFormat] CGLPixelFormatObj];
// CVDisplayLinkSetCurrentCGDisplayFromOpenGLContext(_displayLink, cglContext, cglPixelFormat);
// Activate the display link.
CVDisplayLinkStart(_displayLink);
}
static CVReturn DisplayLinkCallback(__unused CVDisplayLinkRef displayLink, const CVTimeStamp *now, const CVTimeStamp *outputTime, __unused CVOptionFlags flagsIn, __unused CVOptionFlags *flagsOut, void *displayLinkContext) {
CSScanTargetView *const view = (__bridge CSScanTargetView *)displayLinkContext;
// Schedule an opportunity to check that the display link is still linked to the correct display.
dispatch_async(dispatch_get_main_queue(), ^{
[view checkDisplayLink];
});
// Ensure _isDrawingFlag has value 1 when drawing, 0 otherwise.
atomic_store(&view->_isDrawingFlag, 1);
[view.displayLinkDelegate openGLViewDisplayLinkDidFire:view now:now outputTime:outputTime];
/*
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,
it doesn't wait for completion of any current calls. So I've set up a usleep for one callback's duration,
so code in here gets one callback's duration to access the display link.
In practice, it should do so only upon entry, and before calling into the view. The view promises not to
access the display link itself as part of -drawAtTime:frequency:.
*/
atomic_store(&view->_isDrawingFlag, 0);
return kCVReturnSuccess;
}
- (void)checkDisplayLink {
// Don't do anything if this view has already been invalidated.
if(_isInvalid) {
return;
}
// 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
// feels fine.
NSNumber *const screenNumber = self.window.screen.deviceDescription[@"NSScreenNumber"];
if(![_currentScreenNumber isEqual:screenNumber]) {
// Issue a reshape, in case a switch to/from a Retina display has
// happened, changing the results of -convertSizeToBacking:, etc.
// [self reshape];
// Also switch display links, to make sure synchronisation is with the display
// the window is actually on, and at its rate.
[self setupDisplayLink];
}
}
//- (void)drawAtTime:(const CVTimeStamp *)now frequency:(double)frequency {
// [self redrawWithEvent:CSScanTargetViewRedrawEventTimer];
//}
//- (void)drawRect:(NSRect)dirtyRect {
// [self redrawWithEvent:CSScanTargetViewRedrawEventAppKit];
// NSLog(@"...");
//}
//- (void)redrawWithEvent:(CSScanTargetViewRedrawEvent)event {
// [self performWithGLContext:^{
//// [self.delegate openGLViewRedraw:self event:event];
// } flushDrawable:YES];
//}
- (void)invalidate {
_isInvalid = YES;
[self stopDisplayLink];
}
- (void)stopDisplayLink {
const double duration = CVDisplayLinkGetActualOutputVideoRefreshPeriod(_displayLink);
CVDisplayLinkStop(_displayLink);
// This is a workaround; CVDisplayLinkStop does not wait for any existing call to the
// display-link callback to stop. Furthermore there's a race condition between a callback
// and any ability by me to set state.
//
// So: wait for a whole display link tick to avoid the second race condition. Then spin
// on an atomic flag.
usleep((useconds_t)ceil(duration * 1000000.0));
// Spin until _isDrawingFlag is 0 (and leave it as 0).
int expected_value = 0;
while(!atomic_compare_exchange_weak(&_isDrawingFlag, &expected_value, 0)) {
expected_value = 0;
}
}
- (void)dealloc {
// Stop and release the display link
CVDisplayLinkStop(_displayLink);
CVDisplayLinkRelease(_displayLink);
}
- (CSScanTarget *)scanTarget {
return _scanTarget;
}
- (CGSize)backingSize {
@synchronized(self) {
return _backingSize;
}
}
- (void)updateBacking {
[_scanTarget updateFrameBuffer];
}
- (void)awakeFromNib {
// Use the preferred device if available.
if(@available(macOS 10.15, *)) {
self.device = self.preferredDevice;
} else {
self.device = MTLCreateSystemDefaultDevice();
}
// Create the scan target.
_scanTarget = [[CSScanTarget alloc] initWithView:self];
self.delegate = _scanTarget;
// Register to receive dragged and dropped file URLs.
[self registerForDraggedTypes:@[(__bridge NSString *)kUTTypeFileURL]];
}
#pragma mark - NSResponder
- (BOOL)acceptsFirstResponder {
return YES;
}
- (void)keyDown:(NSEvent *)event {
[self.responderDelegate keyDown:event];
}
- (void)keyUp:(NSEvent *)event {
[self.responderDelegate keyUp:event];
}
- (void)flagsChanged:(NSEvent *)event {
// Release the mouse upon a control + command.
if(_mouseIsCaptured &&
event.modifierFlags & NSEventModifierFlagControl &&
event.modifierFlags & NSEventModifierFlagCommand) {
[self releaseMouse];
}
[self.responderDelegate flagsChanged:event];
}
- (BOOL)application:(nonnull CSApplication *)application shouldSendEvent:(nonnull NSEvent *)event {
switch(event.type) {
default: return YES;
case NSEventTypeKeyUp: [self keyUp:event]; return NO;
case NSEventTypeKeyDown: [self keyDown:event]; return NO;
case NSEventTypeFlagsChanged: [self flagsChanged:event]; return NO;
}
}
- (void)paste:(id)sender {
[self.responderDelegate paste:sender];
}
#pragma mark - NSDraggingDestination
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender {
for(NSPasteboardItem *item in [[sender draggingPasteboard] pasteboardItems]) {
NSURL *URL = [NSURL URLWithString:[item stringForType:(__bridge NSString *)kUTTypeFileURL]];
[self.responderDelegate openGLView:self didReceiveFileAtURL:URL];
}
return YES;
}
- (NSDragOperation)draggingEntered:(id < NSDraggingInfo >)sender {
return NSDragOperationLink;
}
#pragma mark - Mouse hiding
- (void)setShouldCaptureMouse:(BOOL)shouldCaptureMouse {
_shouldCaptureMouse = shouldCaptureMouse;
}
- (void)updateTrackingAreas {
[super updateTrackingAreas];
if(_mouseTrackingArea) {
[self removeTrackingArea:_mouseTrackingArea];
}
_mouseTrackingArea =
[[NSTrackingArea alloc]
initWithRect:self.bounds
options:NSTrackingMouseEnteredAndExited | NSTrackingMouseMoved | NSTrackingActiveWhenFirstResponder
owner:self
userInfo:nil];
[self addTrackingArea:_mouseTrackingArea];
}
- (void)scheduleMouseHide {
if(!self.shouldCaptureMouse) {
[_mouseHideTimer invalidate];
_mouseHideTimer = [NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(__unused NSTimer * _Nonnull timer) {
[NSCursor setHiddenUntilMouseMoves:YES];
[self.responderDelegate openGLViewWillHideOSMouseCursor:self];
}];
}
}
- (void)mouseEntered:(NSEvent *)event {
[self.responderDelegate openGLViewDidShowOSMouseCursor:self];
[super mouseEntered:event];
[self scheduleMouseHide];
}
- (void)mouseExited:(NSEvent *)event {
[super mouseExited:event];
[_mouseHideTimer invalidate];
_mouseHideTimer = nil;
[self.responderDelegate openGLViewWillHideOSMouseCursor:self];
}
- (void)releaseMouse {
if(_mouseIsCaptured) {
_mouseIsCaptured = NO;
CGAssociateMouseAndMouseCursorPosition(true);
[NSCursor unhide];
[self.responderDelegate openGLViewDidReleaseMouse:self];
[self.responderDelegate openGLViewDidShowOSMouseCursor:self];
((CSApplication *)[NSApplication sharedApplication]).eventDelegate = nil;
}
}
#pragma mark - Mouse motion
- (void)applyMouseMotion:(NSEvent *)event {
if(!self.shouldCaptureMouse) {
// Mouse capture is off, so don't play games with the cursor, just schedule it to
// hide in the near future.
[self scheduleMouseHide];
[self.responderDelegate openGLViewDidShowOSMouseCursor:self];
} else {
if(_mouseIsCaptured) {
// Mouse capture is on, so move the cursor back to the middle of the window, and
// forward the deltas to the listener.
//
// TODO: should I really need to invert the y coordinate myself? It suggests I
// might have an error in mapping here.
const NSPoint windowCentre = [self convertPoint:CGPointMake(self.bounds.size.width * 0.5, self.bounds.size.height * 0.5) toView:nil];
const NSPoint screenCentre = [self.window convertPointToScreen:windowCentre];
const CGRect screenFrame = self.window.screen.frame;
CGWarpMouseCursorPosition(NSMakePoint(
screenFrame.origin.x + screenCentre.x,
screenFrame.origin.y + screenFrame.size.height - screenCentre.y
));
[self.responderDelegate mouseMoved:event];
} else {
[self.responderDelegate openGLViewDidShowOSMouseCursor:self];
}
}
}
- (void)mouseDragged:(NSEvent *)event {
[self applyMouseMotion:event];
[super mouseDragged:event];
}
- (void)rightMouseDragged:(NSEvent *)event {
[self applyMouseMotion:event];
[super rightMouseDragged:event];
}
- (void)otherMouseDragged:(NSEvent *)event {
[self applyMouseMotion:event];
[super otherMouseDragged:event];
}
- (void)mouseMoved:(NSEvent *)event {
[self applyMouseMotion:event];
[super mouseMoved:event];
}
#pragma mark - Mouse buttons
- (void)applyButtonDown:(NSEvent *)event {
if(self.shouldCaptureMouse) {
if(!_mouseIsCaptured) {
_mouseIsCaptured = YES;
[NSCursor hide];
CGAssociateMouseAndMouseCursorPosition(false);
[self.responderDelegate openGLViewWillHideOSMouseCursor:self];
[self.responderDelegate openGLViewDidCaptureMouse:self];
if(self.shouldUsurpCommand) {
((CSApplication *)[NSApplication sharedApplication]).eventDelegate = self;
}
// Don't report the first click to the delegate; treat that as merely
// an invitation to capture the cursor.
return;
}
[self.responderDelegate mouseDown:event];
}
}
- (void)applyButtonUp:(NSEvent *)event {
if(self.shouldCaptureMouse) {
[self.responderDelegate mouseUp:event];
}
}
- (void)mouseDown:(NSEvent *)event {
[self applyButtonDown:event];
[super mouseDown:event];
}
- (void)rightMouseDown:(NSEvent *)event {
[self applyButtonDown:event];
[super rightMouseDown:event];
}
- (void)otherMouseDown:(NSEvent *)event {
[self applyButtonDown:event];
[super otherMouseDown:event];
}
- (void)mouseUp:(NSEvent *)event {
[self applyButtonUp:event];
[super mouseUp:event];
}
- (void)rightMouseUp:(NSEvent *)event {
[self applyButtonUp:event];
[super rightMouseUp:event];
}
- (void)otherMouseUp:(NSEvent *)event {
[self applyButtonUp:event];
[super otherMouseUp:event];
}
@end