From e02d781b9964ed45b99c16dbdd86f20566060f9a Mon Sep 17 00:00:00 2001 From: Tom Butterworth Date: Sat, 28 Sep 2019 16:05:24 +0100 Subject: [PATCH] Initial refactor to permit user subclasses - Existing public API is unchanged - New SyphonServerBase, SyphonClientBase allow any IOSurface-based subclass to be added outside the framework - SyphonImage likewise refactored - Additions to the framework can be contained in new files without touching existing code - More work to follow --- Exported_Symbols.exp | 2 + Syphon.xcodeproj/project.pbxproj | 22 +- SyphonClient.h | 3 +- SyphonClient.m | 161 +++----------- SyphonClientBase.h | 63 ++++++ SyphonClientBase.m | 177 +++++++++++++++ SyphonClientConnectionManager.h | 3 +- SyphonClientConnectionManager.m | 21 +- SyphonIOSurfaceImage.h | 8 +- SyphonIOSurfaceImage.m | 11 +- SyphonIOSurfaceImageCore.h | 2 +- SyphonIOSurfaceImageCore.m | 12 +- SyphonIOSurfaceImageLegacy.h | 2 +- SyphonIOSurfaceImageLegacy.m | 12 +- SyphonServer.h | 5 +- SyphonServer.m | 334 +++------------------------- SyphonServerBase.h | 68 ++++++ SyphonServerBase.m | 370 +++++++++++++++++++++++++++++++ 18 files changed, 805 insertions(+), 471 deletions(-) create mode 100644 SyphonClientBase.h create mode 100644 SyphonClientBase.m create mode 100644 SyphonServerBase.h create mode 100644 SyphonServerBase.m diff --git a/Exported_Symbols.exp b/Exported_Symbols.exp index 6c0d92e..d6d60f7 100755 --- a/Exported_Symbols.exp +++ b/Exported_Symbols.exp @@ -31,7 +31,9 @@ SYPHON_UNIQUE_CLASS_SYMBOL(SyphonClient) +SYPHON_UNIQUE_CLASS_SYMBOL(SyphonClientBase) SYPHON_UNIQUE_CLASS_SYMBOL(SyphonServer) +SYPHON_UNIQUE_CLASS_SYMBOL(SyphonServerBase) SYPHON_UNIQUE_CLASS_SYMBOL(SyphonServerDirectory) SYPHON_UNIQUE_CLASS_SYMBOL(SyphonImage) diff --git a/Syphon.xcodeproj/project.pbxproj b/Syphon.xcodeproj/project.pbxproj index 5ceabd9..7ea0f09 100644 --- a/Syphon.xcodeproj/project.pbxproj +++ b/Syphon.xcodeproj/project.pbxproj @@ -61,6 +61,8 @@ E218B67718436D3B004FD9E3 /* SyphonClient.h in Headers */ = {isa = PBXBuildFile; fileRef = 1B0906C411CBB1C100BCBE41 /* SyphonClient.h */; }; E218B67818436D3F004FD9E3 /* SyphonImage.h in Headers */ = {isa = PBXBuildFile; fileRef = BD038870122EA9FF007725FF /* SyphonImage.h */; }; E218B67918436D41004FD9E3 /* SyphonServerDirectory.h in Headers */ = {isa = PBXBuildFile; fileRef = BD606D6611D2842D00E02702 /* SyphonServerDirectory.h */; }; + E219FB59233CDC3B00FB7F63 /* SyphonClientBase.h in Headers */ = {isa = PBXBuildFile; fileRef = E219FB57233CDC3B00FB7F63 /* SyphonClientBase.h */; }; + E219FB5A233CDC3B00FB7F63 /* SyphonClientBase.m in Sources */ = {isa = PBXBuildFile; fileRef = E219FB58233CDC3B00FB7F63 /* SyphonClientBase.m */; }; E28D0C9A1D8F43380036DF26 /* SyphonServerRendererCore.h in Headers */ = {isa = PBXBuildFile; fileRef = E28D0C981D8F43380036DF26 /* SyphonServerRendererCore.h */; }; E28D0C9B1D8F43380036DF26 /* SyphonServerRendererCore.m in Sources */ = {isa = PBXBuildFile; fileRef = E28D0C991D8F43380036DF26 /* SyphonServerRendererCore.m */; }; E28D0CA81D9296520036DF26 /* SyphonShader.h in Headers */ = {isa = PBXBuildFile; fileRef = E28D0CA61D9296520036DF26 /* SyphonShader.h */; }; @@ -71,6 +73,8 @@ E28D0CB11D9542B30036DF26 /* SyphonVertices.m in Sources */ = {isa = PBXBuildFile; fileRef = E28D0CAF1D9542B30036DF26 /* SyphonVertices.m */; }; E28D0CB41D95770C0036DF26 /* SyphonServerVertices.h in Headers */ = {isa = PBXBuildFile; fileRef = E28D0CB21D95770C0036DF26 /* SyphonServerVertices.h */; }; E28D0CB51D95770C0036DF26 /* SyphonServerVertices.m in Sources */ = {isa = PBXBuildFile; fileRef = E28D0CB31D95770C0036DF26 /* SyphonServerVertices.m */; }; + E2CF04FE227398E600B8CD19 /* SyphonServerBase.h in Headers */ = {isa = PBXBuildFile; fileRef = E2CF04FC227398E600B8CD19 /* SyphonServerBase.h */; }; + E2CF04FF227398E600B8CD19 /* SyphonServerBase.m in Sources */ = {isa = PBXBuildFile; fileRef = E2CF04FD227398E600B8CD19 /* SyphonServerBase.m */; }; E2D6C8881D8B470E00108260 /* SyphonCGL.c in Sources */ = {isa = PBXBuildFile; fileRef = E2D6C8861D8B470E00108260 /* SyphonCGL.c */; }; E2D6C8891D8B470E00108260 /* SyphonCGL.h in Headers */ = {isa = PBXBuildFile; fileRef = E2D6C8871D8B470E00108260 /* SyphonCGL.h */; }; E2D6C88C1D8B4A6D00108260 /* SyphonServerRenderer.h in Headers */ = {isa = PBXBuildFile; fileRef = E2D6C88A1D8B4A6D00108260 /* SyphonServerRenderer.h */; }; @@ -146,6 +150,8 @@ E21003C51D85F9320066E934 /* SyphonIOSurfaceImageLegacy.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SyphonIOSurfaceImageLegacy.m; sourceTree = ""; }; E21003C81D85FAD00066E934 /* SyphonIOSurfaceImageCore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SyphonIOSurfaceImageCore.h; sourceTree = ""; }; E21003C91D85FAD00066E934 /* SyphonIOSurfaceImageCore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SyphonIOSurfaceImageCore.m; sourceTree = ""; }; + E219FB57233CDC3B00FB7F63 /* SyphonClientBase.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SyphonClientBase.h; sourceTree = ""; }; + E219FB58233CDC3B00FB7F63 /* SyphonClientBase.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SyphonClientBase.m; sourceTree = ""; }; E234B23F2257668400FBDC0C /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; E26877562199C4E600B8E495 /* Building Syphon.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = "Building Syphon.md"; sourceTree = ""; }; E28D0C981D8F43380036DF26 /* SyphonServerRendererCore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SyphonServerRendererCore.h; sourceTree = ""; }; @@ -158,6 +164,8 @@ E28D0CAF1D9542B30036DF26 /* SyphonVertices.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SyphonVertices.m; sourceTree = ""; }; E28D0CB21D95770C0036DF26 /* SyphonServerVertices.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SyphonServerVertices.h; sourceTree = ""; }; E28D0CB31D95770C0036DF26 /* SyphonServerVertices.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SyphonServerVertices.m; sourceTree = ""; }; + E2CF04FC227398E600B8CD19 /* SyphonServerBase.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SyphonServerBase.h; sourceTree = ""; }; + E2CF04FD227398E600B8CD19 /* SyphonServerBase.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SyphonServerBase.m; sourceTree = ""; }; E2D6C8861D8B470E00108260 /* SyphonCGL.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = SyphonCGL.c; sourceTree = ""; }; E2D6C8871D8B470E00108260 /* SyphonCGL.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SyphonCGL.h; sourceTree = ""; }; E2D6C88A1D8B4A6D00108260 /* SyphonServerRenderer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SyphonServerRenderer.h; sourceTree = ""; }; @@ -280,6 +288,7 @@ E28D0CAF1D9542B30036DF26 /* SyphonVertices.m */, E28D0CB21D95770C0036DF26 /* SyphonServerVertices.h */, E28D0CB31D95770C0036DF26 /* SyphonServerVertices.m */, + E2CF04FD227398E600B8CD19 /* SyphonServerBase.m */, ); name = Server; sourceTree = ""; @@ -288,6 +297,7 @@ isa = PBXGroup; children = ( 1B0906C511CBB1C100BCBE41 /* SyphonClient.m */, + E219FB58233CDC3B00FB7F63 /* SyphonClientBase.m */, 1B09098511CD9A1C00BCBE41 /* SyphonClientConnectionManager.h */, 1B09098611CD9A1C00BCBE41 /* SyphonClientConnectionManager.m */, ); @@ -336,7 +346,9 @@ children = ( 1B0906C811CBB1CE00BCBE41 /* Syphon.h */, 1B0906BE11CBB0F500BCBE41 /* SyphonServer.h */, + E2CF04FC227398E600B8CD19 /* SyphonServerBase.h */, 1B0906C411CBB1C100BCBE41 /* SyphonClient.h */, + E219FB57233CDC3B00FB7F63 /* SyphonClientBase.h */, BD038870122EA9FF007725FF /* SyphonImage.h */, BD606D6611D2842D00E02702 /* SyphonServerDirectory.h */, ); @@ -422,6 +434,7 @@ E218B67618436D38004FD9E3 /* SyphonServer.h in Headers */, E218B67718436D3B004FD9E3 /* SyphonClient.h in Headers */, E28D0C9A1D8F43380036DF26 /* SyphonServerRendererCore.h in Headers */, + E219FB59233CDC3B00FB7F63 /* SyphonClientBase.h in Headers */, E218B67818436D3F004FD9E3 /* SyphonImage.h in Headers */, E218B67918436D41004FD9E3 /* SyphonServerDirectory.h in Headers */, E28D0CB01D9542B30036DF26 /* SyphonVertices.h in Headers */, @@ -434,6 +447,7 @@ E2D6C88C1D8B4A6D00108260 /* SyphonServerRenderer.h in Headers */, E2D6C8901D8B4DED00108260 /* SyphonServerRendererLegacy.h in Headers */, BDB8DA311211F59A0028D250 /* SyphonCFMessageReceiver.h in Headers */, + E2CF04FE227398E600B8CD19 /* SyphonServerBase.h in Headers */, BDB8DA351211F59A0028D250 /* SyphonMessageSender.h in Headers */, BDB8DA371211F59A0028D250 /* SyphonCFMessageSender.h in Headers */, BDB8DAF51211FA7F0028D250 /* SyphonMessaging.h in Headers */, @@ -547,6 +561,8 @@ "$(SRCROOT)/SyphonServerDirectory.h", "$(SRCROOT)/SyphonBuildMacros.h", "$(SRCROOT)/SyphonImage.h", + "$(SRCROOT)/SyphonServerBase.h", + "$(SRCROOT)/SyphonClientBase.h", ); name = "Generate Public Headers"; outputPaths = ( @@ -554,10 +570,12 @@ $BUILT_PRODUCTS_DIR/$CONTENTS_FOLDER_PATH/Headers/SyphonServer.h, $BUILT_PRODUCTS_DIR/$CONTENTS_FOLDER_PATH/Headers/SyphonServerDirectory.h, $BUILT_PRODUCTS_DIR/$CONTENTS_FOLDER_PATH/Headers/SyphonImage.h, + $BUILT_PRODUCTS_DIR/$CONTENTS_FOLDER_PATH/Headers/SyphonServerBase.h, + $BUILT_PRODUCTS_DIR/$CONTENTS_FOLDER_PATH/Headers/SyphonClientBase.h, ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "mkdir -p \"$DERIVED_FILE_DIR\"\ncombined=\"\"\nfor def in $GCC_PREPROCESSOR_DEFINITIONS\ndo\ncombined=\"${combined} -D${def}\"\ndone\nfor header in \"SyphonClient\" \"SyphonServer\" \"SyphonServerDirectory\" \"SyphonImage\"\ndo\nsed 's:#import://SYPHON_IMPORT_PLACEHOLDER:' \"$SRCROOT/${header}.h\" > \"$DERIVED_FILE_DIR/${header}_stage_1.h\"\nclang -E -C -P -nostdinc ${combined} -DSYPHON_HEADER_BUILD_PHASE -include \"$SRCROOT/SyphonBuildMacros.h\" \"$DERIVED_FILE_DIR/${header}_stage_1.h\" -o \"$DERIVED_FILE_DIR/${header}_stage_2.h\"\nsed 's://SYPHON_IMPORT_PLACEHOLDER:#import:' \"$DERIVED_FILE_DIR/${header}_stage_2.h\" > \"$DERIVED_FILE_DIR/${header}_stage_3.h\"\nsed '/./,$!d' \"$DERIVED_FILE_DIR/${header}_stage_3.h\" > \"$DERIVED_FILE_DIR/${header}_stage_4.h\"\ncat -s \"$DERIVED_FILE_DIR/${header}_stage_4.h\" > \"$DERIVED_FILE_DIR/${header}.h\"\nrm \"$DERIVED_FILE_DIR/${header}_stage_1.h\"\nrm \"$DERIVED_FILE_DIR/${header}_stage_2.h\"\nrm \"$DERIVED_FILE_DIR/${header}_stage_3.h\"\nrm \"$DERIVED_FILE_DIR/${header}_stage_4.h\"\ncp \"$DERIVED_FILE_DIR/${header}.h\" \"$BUILT_PRODUCTS_DIR/$CONTENTS_FOLDER_PATH/Headers/${header}.h\"\ndone\n"; + shellScript = "mkdir -p \"$DERIVED_FILE_DIR\"\ncombined=\"\"\nfor def in $GCC_PREPROCESSOR_DEFINITIONS\ndo\ncombined=\"${combined} -D${def}\"\ndone\nfor header in \"SyphonClient\" \"SyphonClientBase\" \"SyphonServer\" \"SyphonServerBase\" \"SyphonServerDirectory\" \"SyphonImage\"\ndo\nsed 's:#import://SYPHON_IMPORT_PLACEHOLDER:' \"$SRCROOT/${header}.h\" > \"$DERIVED_FILE_DIR/${header}_stage_1.h\"\nclang -E -C -P -nostdinc ${combined} -DSYPHON_HEADER_BUILD_PHASE -include \"$SRCROOT/SyphonBuildMacros.h\" \"$DERIVED_FILE_DIR/${header}_stage_1.h\" -o \"$DERIVED_FILE_DIR/${header}_stage_2.h\"\nsed 's://SYPHON_IMPORT_PLACEHOLDER:#import:' \"$DERIVED_FILE_DIR/${header}_stage_2.h\" > \"$DERIVED_FILE_DIR/${header}_stage_3.h\"\nsed '/./,$!d' \"$DERIVED_FILE_DIR/${header}_stage_3.h\" > \"$DERIVED_FILE_DIR/${header}_stage_4.h\"\ncat -s \"$DERIVED_FILE_DIR/${header}_stage_4.h\" > \"$DERIVED_FILE_DIR/${header}.h\"\nrm \"$DERIVED_FILE_DIR/${header}_stage_1.h\"\nrm \"$DERIVED_FILE_DIR/${header}_stage_2.h\"\nrm \"$DERIVED_FILE_DIR/${header}_stage_3.h\"\nrm \"$DERIVED_FILE_DIR/${header}_stage_4.h\"\ncp \"$DERIVED_FILE_DIR/${header}.h\" \"$BUILT_PRODUCTS_DIR/$CONTENTS_FOLDER_PATH/Headers/${header}.h\"\ndone\n"; }; BDFE6806122866C7009C2E21 /* Generate Exported Symbols File */ = { isa = PBXShellScriptBuildPhase; @@ -598,6 +616,7 @@ BDB8DA321211F59A0028D250 /* SyphonCFMessageReceiver.m in Sources */, BDB8DA361211F59A0028D250 /* SyphonMessageSender.m in Sources */, BDB8DA381211F59A0028D250 /* SyphonCFMessageSender.m in Sources */, + E219FB5A233CDC3B00FB7F63 /* SyphonClientBase.m in Sources */, E28D0CAD1D930B9F0036DF26 /* SyphonServerShader.m in Sources */, BDB8DAF61211FA7F0028D250 /* SyphonMessaging.m in Sources */, E21003C71D85F9320066E934 /* SyphonIOSurfaceImageLegacy.m in Sources */, @@ -606,6 +625,7 @@ E28D0CB11D9542B30036DF26 /* SyphonVertices.m in Sources */, E2DE7FD412495BF50081453B /* SyphonMessageQueue.m in Sources */, E2D6C88D1D8B4A6D00108260 /* SyphonServerRenderer.m in Sources */, + E2CF04FF227398E600B8CD19 /* SyphonServerBase.m in Sources */, BDFBD77E126F4D8800075A23 /* SyphonDispatch.c in Sources */, BDFAE527148CDA84008C9E6F /* SyphonOpenGLFunctions.c in Sources */, E21003CB1D85FAD00066E934 /* SyphonIOSurfaceImageCore.m in Sources */, diff --git a/SyphonClient.h b/SyphonClient.h index 2c9dcd2..3c3fb7a 100644 --- a/SyphonClient.h +++ b/SyphonClient.h @@ -29,6 +29,7 @@ #import #import +#import #define SYPHON_CLIENT_UNIQUE_CLASS_NAME SYPHON_UNIQUE_CLASS_NAME(SyphonClient) #define SYPHON_IMAGE_UNIQUE_CLASS_NAME SYPHON_UNIQUE_CLASS_NAME(SyphonImage) @@ -45,7 +46,7 @@ NS_ASSUME_NONNULL_BEGIN It is safe to access instances of this class across threads, with the usual limitatiions related to OpenGL. The calls to SyphonClient which may cause work to be done in a GL context are: -newFrameImage, -stop and -release. */ -@interface SYPHON_CLIENT_UNIQUE_CLASS_NAME : NSObject +@interface SYPHON_CLIENT_UNIQUE_CLASS_NAME : SyphonClientBase /*! Returns a new client instance for the described server. You should check the isValid property after initialization to ensure a connection was made to the server. diff --git a/SyphonClient.m b/SyphonClient.m index bf0f5d9..1a8e4bb 100644 --- a/SyphonClient.m +++ b/SyphonClient.m @@ -29,9 +29,12 @@ #import "SyphonClient.h" -#import "SyphonServerDirectory.h" -#import "SyphonPrivate.h" -#import "SyphonClientConnectionManager.h" +#import "SyphonPrivate.h" // TODO: using it? +#import "SyphonClientConnectionManager.h" // TODO: using it? +#import "SyphonCGL.h" +#import "SyphonIOSurfaceImageCore.h" +#import "SyphonIOSurfaceImageLegacy.h" + #import "SyphonCGL.h" #import @@ -39,31 +42,14 @@ @implementation SyphonClient { @private - id _connectionManager; - NSUInteger _lastFrameID; - void (^_handler)(id); - int32_t _status; - int32_t _lock; CGLContextObj _context; + int32_t _lock; CGLContextObj _shareContext; - SYPHON_IMAGE_UNIQUE_CLASS_NAME *_frame; + SyphonImage *_frame; int32_t _frameValid; - NSDictionary *_serverDescription; } -static void *SyphonClientServersContext = &SyphonClientServersContext; - -+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey -{ - if ([theKey isEqualToString:@"serverDescription"]) - { - return NO; - } - else - { - return [super automaticallyNotifiesObserversForKey:theKey]; - } -} +@dynamic isValid, serverDescription, hasNewFrame; #if SYPHON_DEBUG_NO_DRAWING + (void)load @@ -73,21 +59,11 @@ + (void)load } #endif -- (id)init -{ - [self doesNotRecognizeSelector:_cmd]; - return nil; -} - - (id)initWithServerDescription:(NSDictionary *)description context:(CGLContextObj)context options:(NSDictionary *)options newFrameHandler:(void (^)(SyphonClient *client))handler { - self = [super init]; + self = [super initWithServerDescription:description options:options newFrameHandler:handler]; if (self) { - _status = 1; - - _connectionManager = [[SyphonClientConnectionManager alloc] initWithServerDescription:description]; - _handler = [handler copy]; // copy don't retain _lock = OS_SPINLOCK_INIT; #ifdef SYPHON_CORE_SHARE _shareContext = CGLRetainContext(context); @@ -102,48 +78,20 @@ - (id)initWithServerDescription:(NSDictionary *)description context:(CGLContextO #else _context = CGLRetainContext(context); #endif - _serverDescription = [description retain]; - - [[SyphonServerDirectory sharedDirectory] addObserver:self - forKeyPath:@"servers" - options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew - context:SyphonClientServersContext]; - - NSNumber *dictionaryVersion = [description objectForKey:SyphonServerDescriptionDictionaryVersionKey]; - if (dictionaryVersion == nil - || [dictionaryVersion unsignedIntValue] > kSyphonDictionaryVersion - || _connectionManager == nil) - { - [self release]; - return nil; - } - - [(SyphonClientConnectionManager *)_connectionManager addInfoClient:(id )self - isFrameClient:handler != nil ? YES : NO]; } return self; } - (void) dealloc { - [[SyphonServerDirectory sharedDirectory] removeObserver:self forKeyPath:@"servers"]; [self stop]; - [_handler release]; - [_serverDescription release]; [super dealloc]; } - (void)stop { - OSSpinLockLock(&_lock); - if (_status == 1) - { - [(SyphonClientConnectionManager *)_connectionManager removeInfoClient:(id )self - isFrameClient:_handler != nil ? YES : NO]; - [(SyphonClientConnectionManager *)_connectionManager release]; - _connectionManager = nil; - _status = 0; - } + [super stop]; + OSSpinLockLock(&_lock); [_frame release]; _frame = nil; _frameValid = NO; @@ -169,22 +117,6 @@ - (CGLContextObj)context #endif } -- (BOOL)isValid -{ - OSSpinLockLock(&_lock); - BOOL result = ((SyphonClientConnectionManager *)_connectionManager).isValid; - OSSpinLockUnlock(&_lock); - return result; -} - -- (void)receiveNewFrame -{ - if (_handler) - { - _handler(self); - } -} - - (void)invalidateFrame { /* @@ -194,66 +126,35 @@ - (void)invalidateFrame OSAtomicTestAndClearBarrier(0, &_frameValid); } -#pragma mark Rendering frames -- (BOOL)hasNewFrame -{ - BOOL result; - OSSpinLockLock(&_lock); - result = _lastFrameID != ((SyphonClientConnectionManager *)_connectionManager).frameID; - OSSpinLockUnlock(&_lock); - return result; -} +#pragma mark Vending frames - (SyphonImage *)newFrameImage { OSSpinLockLock(&_lock); - _lastFrameID = [(SyphonClientConnectionManager *)_connectionManager frameID]; - if (_frameValid == 0) + if (_frameValid == 0) { [_frame release]; - _frame = [(SyphonClientConnectionManager *)_connectionManager newFrameForContext:_context]; + IOSurfaceRef surface = [self newSurface]; + if (surface) + { + if (SyphonOpenGLContextIsLegacy(_context)) + { + _frame = [[SyphonIOSurfaceImageLegacy alloc] initWithSurface:surface forContext:_context]; + } + else + { + _frame = [[SyphonIOSurfaceImageCore alloc] initWithSurface:surface forContext:_context]; + } + CFRelease(surface); + } + else + { + _frame = nil; + } OSAtomicTestAndSetBarrier(0, &_frameValid); } OSSpinLockUnlock(&_lock); return [_frame retain]; } -- (NSDictionary *)serverDescription -{ - OSSpinLockLock(&_lock); - NSDictionary *description = _serverDescription; - OSSpinLockUnlock(&_lock); - return description; -} - -#pragma mark Changes -- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context -{ - if (context == SyphonClientServersContext) - { - NSUInteger kind = [change[NSKeyValueChangeKindKey] unsignedIntegerValue]; - if (kind == NSKeyValueChangeSetting || kind == NSKeyValueChangeReplacement) - { - NSArray *servers = change[NSKeyValueChangeNewKey]; - NSString *uuid = _serverDescription[SyphonServerDescriptionUUIDKey]; - for (NSDictionary *description in servers) { - if ([description[SyphonServerDescriptionUUIDKey] isEqualToString:uuid] && - ![_serverDescription isEqualToDictionary:description]) - { - [self willChangeValueForKey:@"serverDescription"]; - description = [description copy]; - OSSpinLockLock(&_lock); - [_serverDescription release]; - _serverDescription = description; - OSSpinLockUnlock(&_lock); - [self didChangeValueForKey:@"serverDescription"]; - } - } - } - } - else - { - [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; - } -} @end diff --git a/SyphonClientBase.h b/SyphonClientBase.h new file mode 100644 index 0000000..05e2a4c --- /dev/null +++ b/SyphonClientBase.h @@ -0,0 +1,63 @@ +// +// SyphonIOSurfaceClient.h +// Syphon +// +// Created by Tom Butterworth on 26/09/2019. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +#define SYPHON_CLIENT_BASE_UNIQUE_CLASS_NAME SYPHON_UNIQUE_CLASS_NAME(SyphonClientBase) + +@interface SYPHON_CLIENT_BASE_UNIQUE_CLASS_NAME : NSObject +/*! + @param description + + */ +- (instancetype)initWithServerDescription:(NSDictionary *)description options:(nullable NSDictionary *)options newFrameHandler:(nullable void (^)(id client))handler; + +/*! + Returns a dictionary with a description of the server the client is attached to. See SyphonServerDirectory for the keys this dictionary contains +*/ +@property (readonly) NSDictionary *serverDescription; + +/*! + A client is valid if it has a working connection to a server. Once this returns NO, the SyphonClient will not yield any further frames. + */ +@property (readonly) BOOL isValid; + +/*! + Stops the client from receiving any further frames from the server. Use of this method is optional and releasing all references to the client has the same effect. + + This method may perform work in the OpenGL context. As with any other OpenGL calls, you must ensure no other threads use those contexts during calls to this method. + */ +- (void)stop; + +/*! + Returns YES if the server has output a new frame since the last time newFrameImage was called for this client, NO otherwise. +*/ +@property (readonly) BOOL hasNewFrame; +@end + +#if defined(SYPHON_USE_CLASS_ALIAS) +@compatibility_alias SyphonClientBase SYPHON_CLIENT_BASE_UNIQUE_CLASS_NAME; +#endif + +@interface SyphonClientBase (SyphonSubclassing) +/*! + Subclasses use this method to acquire an IOSurface representing the current output from the server. Subclasses may consider the returned value valid until + the next call to -invalidateFrame. + + @returns An IOSurface representing the live output from the server. YOU ARE RESPONSIBLE FOR RELEASING THIS OBJECT using CFRelease() when you + are finished with it. + */ +- (IOSurfaceRef)newSurface; + +/*! + Subclasses override this method to invalidate their output when the server's surface backing changes. + */ +- (void)invalidateFrame; +@end +NS_ASSUME_NONNULL_END diff --git a/SyphonClientBase.m b/SyphonClientBase.m new file mode 100644 index 0000000..34634ee --- /dev/null +++ b/SyphonClientBase.m @@ -0,0 +1,177 @@ +// +// SyphonIOSurfaceClient.m +// Syphon +// +// Created by Tom Butterworth on 26/09/2019. +// + +#import "SyphonClientBase.h" +#import "SyphonServerDirectory.h" +#import "SyphonClientConnectionManager.h" +#import "SyphonPrivate.h" + +// TODO: name? +static void *SyphonClientServersContext = &SyphonClientServersContext; + +@implementation SyphonClientBase { + // Once our minimum version reaches 10.12, replace + // this with os_unfair_lock + OSSpinLock _lock; + NSUInteger _lastFrameID; + SyphonClientConnectionManager *_connectionManager; + NSDictionary *_serverDescription; + void (^_handler)(id); +} + ++ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey +{ + if ([theKey isEqualToString:@"serverDescription"]) + { + return NO; + } + else + { + return [super automaticallyNotifiesObserversForKey:theKey]; + } +} + +- (id)init +{ + [self doesNotRecognizeSelector:_cmd]; + return nil; +} + +- (instancetype)initWithServerDescription:(NSDictionary *)description options:(nullable NSDictionary *)options newFrameHandler:(nullable void (^)(id client))handler +{ + self = [super init]; + if (self) + { + _lock = OS_SPINLOCK_INIT; + + _connectionManager = [[SyphonClientConnectionManager alloc] initWithServerDescription:description]; + + _handler = [handler copy]; // copy don't retain + _serverDescription = [description retain]; + + [[SyphonServerDirectory sharedDirectory] addObserver:self + forKeyPath:@"servers" + options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew + context:SyphonClientServersContext]; + + [_connectionManager addInfoClient:(id )self + isFrameClient:handler != nil ? YES : NO]; + + NSNumber *dictionaryVersion = [description objectForKey:SyphonServerDescriptionDictionaryVersionKey]; + if (dictionaryVersion == nil + || [dictionaryVersion unsignedIntValue] > kSyphonDictionaryVersion + || _connectionManager == nil) + { + [self release]; + return nil; + } + } + return self; +} + +- (BOOL)isValid +{ + OSSpinLockLock(&_lock); + BOOL result = _connectionManager.isValid; + OSSpinLockUnlock(&_lock); + return result; +} + +- (void) dealloc +{ + [[SyphonServerDirectory sharedDirectory] removeObserver:self forKeyPath:@"servers"]; + [self stop]; + [_handler release]; + [_serverDescription release]; + [super dealloc]; +} + +- (void)stop +{ + OSSpinLockLock(&_lock); + if (_connectionManager) + { + [_connectionManager removeInfoClient:(id )self + isFrameClient:_handler != nil ? YES : NO]; + [_connectionManager release]; + _connectionManager = nil; + } + OSSpinLockUnlock(&_lock); +} + +- (void)receiveNewFrame +{ + if (_handler) + { + _handler(self); + } +} + +- (void)invalidateFrame +{ + // Nothing for us to do, subclasses will usually override this +} + +- (BOOL)hasNewFrame +{ + BOOL result; + OSSpinLockLock(&_lock); + result = _lastFrameID != _connectionManager.frameID; + OSSpinLockUnlock(&_lock); + return result; +} + +- (NSDictionary *)serverDescription +{ + OSSpinLockLock(&_lock); + NSDictionary *description = _serverDescription; + OSSpinLockUnlock(&_lock); + return description; +} + +#pragma mark Changes +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +{ + if (context == SyphonClientServersContext) + { + NSUInteger kind = [change[NSKeyValueChangeKindKey] unsignedIntegerValue]; + if (kind == NSKeyValueChangeSetting || kind == NSKeyValueChangeReplacement) + { + NSArray *servers = change[NSKeyValueChangeNewKey]; + NSString *uuid = _serverDescription[SyphonServerDescriptionUUIDKey]; + for (NSDictionary *description in servers) { + if ([description[SyphonServerDescriptionUUIDKey] isEqualToString:uuid] && + ![_serverDescription isEqualToDictionary:description]) + { + [self willChangeValueForKey:@"serverDescription"]; + description = [description copy]; + OSSpinLockLock(&_lock); + [_serverDescription release]; + _serverDescription = description; + OSSpinLockUnlock(&_lock); + [self didChangeValueForKey:@"serverDescription"]; + } + } + } + } + else + { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } +} + +- (IOSurfaceRef)newSurface +{ + IOSurfaceRef surface; + OSSpinLockLock(&_lock); + _lastFrameID = [_connectionManager frameID]; + surface = [_connectionManager newSurface]; + OSSpinLockUnlock(&_lock); + return surface; +} + +@end diff --git a/SyphonClientConnectionManager.h b/SyphonClientConnectionManager.h index 762c9fd..aeb508f 100644 --- a/SyphonClientConnectionManager.h +++ b/SyphonClientConnectionManager.h @@ -29,7 +29,6 @@ #import -#import "SyphonImage.h" /* This object handles messaging to and from the server. @@ -61,7 +60,7 @@ @property (readonly) BOOL isValid; - (void)addInfoClient:(id )client isFrameClient:(BOOL)frameClient; // Must be - (void)removeInfoClient:(id )client isFrameClient:(BOOL)frameClient; // paired -- (SyphonImage *)newFrameForContext:(CGLContextObj)context; +- (IOSurfaceRef)newSurface; @property (readonly) NSUInteger frameID; @end diff --git a/SyphonClientConnectionManager.m b/SyphonClientConnectionManager.m index d0a7124..7427945 100644 --- a/SyphonClientConnectionManager.m +++ b/SyphonClientConnectionManager.m @@ -30,9 +30,6 @@ #import "SyphonClientConnectionManager.h" #import "SyphonPrivate.h" -#import "SyphonCGL.h" -#import "SyphonIOSurfaceImageCore.h" -#import "SyphonIOSurfaceImageLegacy.h" #import "SyphonMessaging.h" #import #import @@ -340,22 +337,14 @@ - (void)setSurfaceID:(IOSurfaceID)surfaceID OSSpinLockUnlock(&_lock); } -- (SyphonImage *)newFrameForContext:(CGLContextObj)context +- (IOSurfaceRef)newSurface { - SyphonImage *result; + IOSurfaceRef surface; OSSpinLockLock(&_lock); - - if (SyphonOpenGLContextIsLegacy(context)) - { - result = [[SyphonIOSurfaceImageLegacy alloc] initWithSurface:[self surfaceHavingLock] forContext:context]; - } - else - { - result = [[SyphonIOSurfaceImageCore alloc] initWithSurface:[self surfaceHavingLock] forContext:context]; - } - + surface = [self surfaceHavingLock]; OSSpinLockUnlock(&_lock); - return result; + if (surface) CFRetain(surface); + return surface; } - (NSUInteger)frameID diff --git a/SyphonIOSurfaceImage.h b/SyphonIOSurfaceImage.h index ad56e2b..8470ed0 100644 --- a/SyphonIOSurfaceImage.h +++ b/SyphonIOSurfaceImage.h @@ -35,12 +35,16 @@ @interface SYPHON_IOSURFACE_IMAGE_UNIQUE_CLASS_NAME : SyphonImage { @protected - CGLContextObj cgl_ctx; NSSize _size; } -- (id)initWithSurface:(IOSurfaceRef)surfaceRef forContext:(CGLContextObj)context; +- (id)initWithSurface:(IOSurfaceRef)surfaceRef; @end #if defined(SYPHON_USE_CLASS_ALIAS) @compatibility_alias SyphonIOSurfaceImage SYPHON_IOSURFACE_IMAGE_UNIQUE_CLASS_NAME; #endif + +@interface SyphonIOSurfaceImage (SyphonSubclassing) +// TODO: subclasses probably only need this at init, we might not want to expose it here +@property (readonly) IOSurfaceRef surface; +@end diff --git a/SyphonIOSurfaceImage.m b/SyphonIOSurfaceImage.m index 111fff9..4ea4250 100644 --- a/SyphonIOSurfaceImage.m +++ b/SyphonIOSurfaceImage.m @@ -36,18 +36,18 @@ @implementation SyphonIOSurfaceImage IOSurfaceRef _surface; } -- (id)initWithSurface:(IOSurfaceRef)surfaceRef forContext:(CGLContextObj)context +- (id)initWithSurface:(IOSurfaceRef)surfaceRef { self = [super init]; if (self) { - if (context == nil || surfaceRef == nil) + if (surfaceRef == nil) { [self release]; return nil; } _surface = (IOSurfaceRef)CFRetain(surfaceRef); - cgl_ctx = CGLRetainContext(context); + _size.width = IOSurfaceGetWidth(surfaceRef); _size.height = IOSurfaceGetHeight(surfaceRef); } @@ -57,7 +57,6 @@ - (id)initWithSurface:(IOSurfaceRef)surfaceRef forContext:(CGLContextObj)context - (void)dealloc { if (_surface) CFRelease(_surface); - if (cgl_ctx) CGLReleaseContext(cgl_ctx); [super dealloc]; } @@ -66,4 +65,8 @@ - (NSSize)textureSize return _size; } +- (IOSurfaceRef)surface +{ + return _surface; +} @end diff --git a/SyphonIOSurfaceImageCore.h b/SyphonIOSurfaceImageCore.h index 42445d6..6c6a825 100644 --- a/SyphonIOSurfaceImageCore.h +++ b/SyphonIOSurfaceImageCore.h @@ -30,5 +30,5 @@ #import "SyphonIOSurfaceImage.h" @interface SyphonIOSurfaceImageCore : SyphonIOSurfaceImage - +- (id)initWithSurface:(IOSurfaceRef)surface forContext:(CGLContextObj)context; @end diff --git a/SyphonIOSurfaceImageCore.m b/SyphonIOSurfaceImageCore.m index 7545396..2b2f44b 100644 --- a/SyphonIOSurfaceImageCore.m +++ b/SyphonIOSurfaceImageCore.m @@ -32,14 +32,23 @@ @implementation SyphonIOSurfaceImageCore { @private + CGLContextObj cgl_ctx; GLuint _texture; } - (id)initWithSurface:(IOSurfaceRef)surface forContext:(CGLContextObj)context { - self = [super initWithSurface:surface forContext:context]; + self = [super initWithSurface:surface]; if (self) { + if (!context) + { + [self release]; + return nil; + } + + cgl_ctx = CGLRetainContext(context); + CGLContextObj previous = CGLGetCurrentContext(); if (previous != context) { @@ -90,6 +99,7 @@ - (void)dealloc CGLSetCurrentContext(previous); } } + if (cgl_ctx) CGLReleaseContext(cgl_ctx); [super dealloc]; } diff --git a/SyphonIOSurfaceImageLegacy.h b/SyphonIOSurfaceImageLegacy.h index 87f9a1c..48bc415 100644 --- a/SyphonIOSurfaceImageLegacy.h +++ b/SyphonIOSurfaceImageLegacy.h @@ -30,5 +30,5 @@ #import "SyphonIOSurfaceImage.h" @interface SyphonIOSurfaceImageLegacy : SyphonIOSurfaceImage - +- (id)initWithSurface:(IOSurfaceRef)surface forContext:(CGLContextObj)context; @end diff --git a/SyphonIOSurfaceImageLegacy.m b/SyphonIOSurfaceImageLegacy.m index 60c9e30..9f79024 100644 --- a/SyphonIOSurfaceImageLegacy.m +++ b/SyphonIOSurfaceImageLegacy.m @@ -33,14 +33,23 @@ @implementation SyphonIOSurfaceImageLegacy { @private + CGLContextObj cgl_ctx; GLuint _texture; } - (id)initWithSurface:(IOSurfaceRef)surface forContext:(CGLContextObj)context { - self = [super initWithSurface:surface forContext:context]; + self = [super initWithSurface:surface]; if (self) { + if (!context) + { + [self release]; + return nil; + } + + cgl_ctx = CGLRetainContext(context); + glPushAttrib(GL_TEXTURE_BIT); // create the surface backed texture @@ -68,6 +77,7 @@ - (void)dealloc { glDeleteTextures(1, &_texture); } + if (cgl_ctx) CGLReleaseContext(cgl_ctx); [super dealloc]; } diff --git a/SyphonServer.h b/SyphonServer.h index ba3d5fc..4575b79 100644 --- a/SyphonServer.h +++ b/SyphonServer.h @@ -29,6 +29,7 @@ #import #import +#import NS_ASSUME_NONNULL_BEGIN @@ -76,7 +77,7 @@ extern NSString * const SyphonServerOptionStencilBufferResolution; @class SYPHON_IMAGE_UNIQUE_CLASS_NAME; -@interface SYPHON_SERVER_UNIQUE_CLASS_NAME : NSObject +@interface SYPHON_SERVER_UNIQUE_CLASS_NAME : SyphonServerBase /** @name Instantiation */ /** @{ */ @@ -91,7 +92,7 @@ extern NSString * const SyphonServerOptionStencilBufferResolution; @returns A newly intialized SyphonServer. Nil on failure. */ -- (id)initWithName:(nullable NSString*)serverName context:(CGLContextObj)context options:(nullable NSDictionary *)options; +- (instancetype)initWithName:(nullable NSString*)serverName context:(CGLContextObj)context options:(nullable NSDictionary *)options; /** @} */ diff --git a/SyphonServer.m b/SyphonServer.m index 0e2df74..93123eb 100644 --- a/SyphonServer.m +++ b/SyphonServer.m @@ -34,7 +34,6 @@ #import "SyphonServerRendererCore.h" #import "SyphonPrivate.h" #import "SyphonCGL.h" -#import "SyphonServerConnectionManager.h" #import #import @@ -43,65 +42,25 @@ #define SYPHON_GL_TEXTURE_RECT 0x84F5 #define SYPHON_GL_TEXTURE_2D 0x0DE1 -@interface SyphonServer (Private) -+ (void)retireRemainingServers; -@end - -__attribute__((destructor)) -static void finalizer() -{ - [SyphonServer retireRemainingServers]; -} - @implementation SyphonServer { @private - NSString *_name; - NSString *_uuid; - BOOL _broadcasts; - - id _connectionManager; - id _renderer; + SyphonServerRenderer * _renderer; CGLContextObj _shareContext; - void *_surfaceRef; BOOL _pushPending; SYPHON_IMAGE_UNIQUE_CLASS_NAME *_surfaceTexture; BOOL _wantsContextChanges; GLint _virtualScreen; - - int32_t _mdLock; - - id _activityToken; } -+ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey -{ - BOOL automatic; - if ([theKey isEqualToString:@"hasClients"]) - { - automatic=NO; - } - else - { - automatic=[super automaticallyNotifiesObserversForKey:theKey]; - } - return automatic; -} - -+ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key -{ - if ([key isEqualToString:@"serverDescription"]) - { - return [NSSet setWithObject:@"name"]; - } - else - { - return [super keyPathsForValuesAffectingValueForKey:key]; - } -} +// TODO: delete if we move these out of SyphonServer.h +// (they are redeclared from SyphonIOSurfaceServer.h) +@dynamic name; +@dynamic serverDescription; +@dynamic hasClients; + (GLuint)integerValueForKey:(NSString *)key fromOptions:(NSDictionary *)options { @@ -124,9 +83,9 @@ - (id)init return self; } -- (id)initWithName:(NSString*)serverName context:(CGLContextObj)context options:(NSDictionary *)options +- (instancetype)initWithName:(NSString*)serverName context:(CGLContextObj)context options:(NSDictionary *)options { - self = [super init]; + self = [super initWithName:serverName options:options]; if(self) { if (context == NULL) @@ -135,42 +94,6 @@ - (id)initWithName:(NSString*)serverName context:(CGLContextObj)context options: return nil; } - _mdLock = OS_SPINLOCK_INIT; - - if (serverName == nil) - { - serverName = @""; - } - _name = [serverName copy]; - _uuid = SyphonCreateUUIDString(); - - _connectionManager = [[SyphonServerConnectionManager alloc] initWithUUID:_uuid options:options]; - - [(SyphonServerConnectionManager *)_connectionManager addObserver:self forKeyPath:@"hasClients" options:NSKeyValueObservingOptionPrior context:nil]; - - if (![(SyphonServerConnectionManager *)_connectionManager start]) - { - [self release]; - return nil; - } - - NSNumber *isPrivate = [options objectForKey:SyphonServerOptionIsPrivate]; - if ([isPrivate respondsToSelector:@selector(boolValue)] - && [isPrivate boolValue] == YES) - { - _broadcasts = NO; - } - else - { - _broadcasts = YES; - } - - if (_broadcasts) - { - [[self class] addServerToRetireList:_uuid]; - [self startBroadcasts]; - } - // We check for changes to the context's virtual screen, so set it to an invalid value // so our first binding counts as a change _virtualScreen = -1; @@ -209,47 +132,17 @@ - (id)initWithName:(NSString*)serverName context:(CGLContextObj)context options: CGLReleaseContext(context); #endif } - - // Prevent this app from being suspended or terminated eg if it goes off-screen (MacOS 10.9+ only) - NSProcessInfo *processInfo = [NSProcessInfo processInfo]; - if ([processInfo respondsToSelector:@selector(beginActivityWithOptions:reason:)]) - { - NSActivityOptions options = NSActivityAutomaticTerminationDisabled | NSActivityBackground; - _activityToken = [[processInfo beginActivityWithOptions:options reason:_uuid] retain]; - } } return self; } - (void) shutDownServer -{ - if (_connectionManager) - { - [(SyphonServerConnectionManager *)_connectionManager removeObserver:self forKeyPath:@"hasClients"]; - [(SyphonServerConnectionManager *)_connectionManager stop]; - [(SyphonServerConnectionManager *)_connectionManager release]; - _connectionManager = nil; - } - +{ [self destroyIOSurface]; - - if (_broadcasts) - { - [self stopBroadcasts]; - [[self class] removeServerFromRetireList:_uuid]; - } - - if (_activityToken) - { - [[NSProcessInfo processInfo] endActivity:_activityToken]; - [_activityToken release]; - _activityToken = nil; - } } - (void) dealloc { - SYPHONLOG(@"Server deallocing, name: %@, UUID: %@", self.name, [self.serverDescription objectForKey:SyphonServerDescriptionUUIDKey]); [self shutDownServer]; #ifdef SYPHON_CORE_SHARE if (_shareContext) @@ -258,97 +151,22 @@ - (void) dealloc } #endif [_renderer release]; - [_name release]; - [_uuid release]; [super dealloc]; } -- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context -{ - if ([keyPath isEqualToString:@"hasClients"]) - { - if ([[change objectForKey:NSKeyValueChangeNotificationIsPriorKey] boolValue] == YES) - { - [self willChangeValueForKey:keyPath]; - } - else - { - [self didChangeValueForKey:keyPath]; - } - } - else - { - [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; - } -} - - (CGLContextObj)context { #ifdef SYPHON_CORE_SHARE return _shareContext; #else - return ((SyphonServerRenderer *)_renderer).context; + return (_renderer).context; #endif } -- (NSDictionary *)serverDescription -{ - NSDictionary *surface = ((SyphonServerConnectionManager *)_connectionManager).surfaceDescription; - if (!surface) surface = [NSDictionary dictionary]; - /* - Getting the app name: helper tasks, command-line tools, etc, don't have a NSRunningApplication instance, - so fall back to NSProcessInfo in those cases, then use an empty string as a last resort. - - http://developer.apple.com/library/mac/qa/qa1544/_index.html - - */ - NSString *appName = [[NSRunningApplication currentApplication] localizedName]; - if (!appName) appName = [[NSProcessInfo processInfo] processName]; - if (!appName) appName = [NSString string]; - - return [NSDictionary dictionaryWithObjectsAndKeys: - [NSNumber numberWithUnsignedInt:kSyphonDictionaryVersion], SyphonServerDescriptionDictionaryVersionKey, - self.name, SyphonServerDescriptionNameKey, - _uuid, SyphonServerDescriptionUUIDKey, - appName, SyphonServerDescriptionAppNameKey, - [NSArray arrayWithObject:surface], SyphonServerDescriptionSurfacesKey, - nil]; -} - -- (NSString*)name -{ - OSSpinLockLock(&_mdLock); - NSString *result = [_name retain]; - OSSpinLockUnlock(&_mdLock); - return [result autorelease]; -} - -- (void)setName:(NSString *)newName -{ - if (newName == nil) - { - newName = @""; - } - [newName copy]; - OSSpinLockLock(&_mdLock); - [_name release]; - _name = newName; - OSSpinLockUnlock(&_mdLock); - [(SyphonServerConnectionManager *)_connectionManager setName:newName]; - if (_broadcasts) - { - [self broadcastServerUpdate]; - } -} - - (void)stop { [self shutDownServer]; -} - -- (BOOL)hasClients -{ - return ((SyphonServerConnectionManager *)_connectionManager).hasClients; + [super stop]; } - (BOOL)bindToDrawFrameOfSize:(NSSize)size inContext:(BOOL)isInContext @@ -400,11 +218,9 @@ - (void)unbindAndPublish // which has no clean image. [_renderer flush]; #endif // SYPHON_DEBUG_NO_DRAWING - // Push the new surface ID to clients - [(SyphonServerConnectionManager *)_connectionManager setSurfaceID:IOSurfaceGetID(_surfaceRef)]; _pushPending = NO; } - [(SyphonServerConnectionManager *)_connectionManager publishNewFrame]; + [self publish]; } - (void)publishFrameTexture:(GLuint)texID textureTarget:(GLenum)target imageRegion:(NSRect)region textureDimensions:(NSSize)size flipped:(BOOL)isFlipped @@ -434,7 +250,7 @@ - (BOOL)capabilitiesDidChange { #if !SYPHON_DEBUG_NO_DRAWING GLint screen; - CGLGetVirtualScreen(((SyphonServerRenderer *)_renderer).context, &screen); + CGLGetVirtualScreen(_renderer.context, &screen); if (screen != _virtualScreen) { _virtualScreen = screen; @@ -457,10 +273,19 @@ - (void) setupIOSurfaceForSize:(NSSize)size [NSNumber numberWithUnsignedInteger:(NSUInteger)size.height], (NSString*)kIOSurfaceHeight, [NSNumber numberWithUnsignedInteger:4U], (NSString*)kIOSurfaceBytesPerElement, nil]; - _surfaceRef = IOSurfaceCreate((CFDictionaryRef) surfaceAttributes); + IOSurfaceRef surface = IOSurfaceCreate((CFDictionaryRef) surfaceAttributes); + + [self setSurface:surface]; + [surfaceAttributes release]; - _surfaceTexture = [_renderer newImageForSurface:_surfaceRef]; + _surfaceTexture = [_renderer newImageForSurface:surface]; + + if (surface) + { + CFRelease(surface); + } + if (_surfaceTexture) { [_renderer setupForBackingTexture:_surfaceTexture.textureName @@ -477,122 +302,13 @@ - (void) setupIOSurfaceForSize:(NSSize)size - (void) destroyIOSurface { #if !SYPHON_DEBUG_NO_DRAWING + [self setSurface:NULL]; [_renderer destroySizedResources]; - if (_surfaceRef != NULL) - { - CFRelease(_surfaceRef); - _surfaceRef = NULL; - } [_surfaceTexture release]; _surfaceTexture = nil; #endif // SYPHON_DEBUG_NO_DRAWING } -#pragma mark Notification Handling for Server Presence -/* - Broadcast and discovery is done via NSDistributedNotificationCenter. Servers notify announce, change (currently only affects name) and retirement. - Discovery is done by a discovery-request notification, to which servers respond with an announce. - - If this gets unweildy we could move it into a SyphonBroadcaster class - - */ - -/* - We track all instances and send a retirement broadcast for any which haven't been stopped when the code is unloaded. - */ - -static OSSpinLock mRetireListLock = OS_SPINLOCK_INIT; -static NSMutableSet *mRetireList = nil; - -+ (void)addServerToRetireList:(NSString *)serverUUID -{ - OSSpinLockLock(&mRetireListLock); - if (mRetireList == nil) - { - mRetireList = [[NSMutableSet alloc] initWithCapacity:1U]; - } - [mRetireList addObject:serverUUID]; - OSSpinLockUnlock(&mRetireListLock); -} - -+ (void)removeServerFromRetireList:(NSString *)serverUUID -{ - OSSpinLockLock(&mRetireListLock); - [mRetireList removeObject:serverUUID]; - if ([mRetireList count] == 0) - { - [mRetireList release]; - mRetireList = nil; - } - OSSpinLockUnlock(&mRetireListLock); -} - -+ (void)retireRemainingServers -{ - // take the set out of the global so we don't hold the spin-lock while we send the notifications - // even though there should never be contention for this - NSMutableSet *mySet = nil; - OSSpinLockLock(&mRetireListLock); - mySet = mRetireList; - mRetireList = nil; - OSSpinLockUnlock(&mRetireListLock); - for (NSString *uuid in mySet) { - SYPHONLOG(@"Retiring a server at code unload time because it was not properly stopped"); - NSDictionary *fakeServerDescription = [NSDictionary dictionaryWithObject:uuid forKey:SyphonServerDescriptionUUIDKey]; - [[NSDistributedNotificationCenter defaultCenter] postNotificationName:SyphonServerRetire - object:SyphonServerDescriptionUUIDKey - userInfo:fakeServerDescription - deliverImmediately:YES]; - } - [mySet release]; -} - -- (void)startBroadcasts -{ - // Register for any Announcement Requests. - [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(handleDiscoveryRequest:) name:SyphonServerAnnounceRequest object:nil]; - - [self broadcastServerAnnounce]; -} - -- (void) handleDiscoveryRequest:(NSNotification*) aNotification -{ - SYPHONLOG(@"Got Discovery Request"); - - [self broadcastServerAnnounce]; -} - -- (void)broadcastServerAnnounce -{ - if (_broadcasts) - { - NSDictionary *description = self.serverDescription; - [[NSDistributedNotificationCenter defaultCenter] postNotificationName:SyphonServerAnnounce - object:[description objectForKey:SyphonServerDescriptionUUIDKey] - userInfo:description - deliverImmediately:YES]; - } -} - -- (void)broadcastServerUpdate -{ - NSDictionary *description = self.serverDescription; - [[NSDistributedNotificationCenter defaultCenter] postNotificationName:SyphonServerUpdate - object:[description objectForKey:SyphonServerDescriptionUUIDKey] - userInfo:description - deliverImmediately:YES]; -} - -- (void)stopBroadcasts -{ - [[NSDistributedNotificationCenter defaultCenter] removeObserver:self]; - NSDictionary *description = self.serverDescription; - [[NSDistributedNotificationCenter defaultCenter] postNotificationName:SyphonServerRetire - object:[description objectForKey:SyphonServerDescriptionUUIDKey] - userInfo:description - deliverImmediately:YES]; -} - @end diff --git a/SyphonServerBase.h b/SyphonServerBase.h new file mode 100644 index 0000000..fec5251 --- /dev/null +++ b/SyphonServerBase.h @@ -0,0 +1,68 @@ +// +// SyphonIOSurfaceServer.h +// Syphon +// +// Created by Tom Butterworth on 26/04/2019. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +/*! + @relates SyphonServerBase + If this key is matched with a NSNumber with a BOOL value YES, then the server will be invisible to other Syphon users. You are then responsible for passing the NSDictionary returned by serverDescription to processes which require it to create a SyphonClient. Default is NO. + */ +extern NSString * const SyphonServerOptionIsPrivate; + +#define SYPHON_SERVER_BASE_UNIQUE_CLASS_NAME SYPHON_UNIQUE_CLASS_NAME(SyphonServerBase) + +@interface SYPHON_SERVER_BASE_UNIQUE_CLASS_NAME : NSObject + +/*! + If you implement your own subclass of SyphonServerBase, you must call this designated initializer from your own initializer. + + Creates a new server with the specified human-readable name (which need not be unique), CGLContext and options. The server will be started immediately. Init may fail and return nil if the server could not be started. + + @param serverName Non-unique human readable server name. This is not required and may be nil, but is usually used by clients in their UI to aid identification. + @param options A dictionary containing key-value pairs to specify options for the server. Currently supported options are SyphonServerOptionIsPrivate, plus any added by the subclass. See their descriptions for details. + @returns A newly intialized Syphon server. Nil on failure. +*/ +- (instancetype)initWithName:(nullable NSString*)serverName options:(nullable NSDictionary *)options NS_DESIGNATED_INITIALIZER; +/*! + A string representing the name of the SyphonServer. + */ +@property (strong) NSString* name; + +/*! + A dictionary describing the server. Normally you won't need to access this, however if you created the server as private (using SyphonServerOptionIsPrivate) then you must pass this dictionary to any process in which you wish to create a SyphonClient. You should not rely on the presence of any particular keys in this dictionary. The content will always conform to the \ protocol. + */ +@property (readonly) NSDictionary* serverDescription; + +/*! + YES if clients are currently attached, NO otherwise. If you generate frames frequently (for instance on a display-link timer), you may choose to test this and only call publishFrameTexture:textureTarget:imageRegion:textureDimensions:flipped: when clients are attached. + */ +@property (readonly) BOOL hasClients; + +/*! + Stops the server instance. Use of this method is optional and releasing all references to the server has the same effect. + */ +- (void)stop; + +@end + +#if defined(SYPHON_USE_CLASS_ALIAS) +@compatibility_alias SyphonServerBase SYPHON_SERVER_BASE_UNIQUE_CLASS_NAME; +#endif + +@interface SyphonServerBase (SyphonSubclassing) +// TODO: document +- (IOSurfaceRef)copySurface; +// TODO: document +// clients are not updated until a subsequent call to publish +- (void)setSurface:(nullable IOSurfaceRef)surface; +// TODO: document +- (void)publish; + +@end +NS_ASSUME_NONNULL_END diff --git a/SyphonServerBase.m b/SyphonServerBase.m new file mode 100644 index 0000000..9fd6c0b --- /dev/null +++ b/SyphonServerBase.m @@ -0,0 +1,370 @@ +// +// SyphonIOSurfaceServer.m +// Syphon +// +// Created by Tom Butterworth on 26/04/2019. +// + +#import "SyphonServerBase.h" +#import "SyphonServerConnectionManager.h" +#import "SyphonPrivate.h" + +@interface SyphonServerBase (Private) ++ (void)retireRemainingServers; +@end + +__attribute__((destructor)) +static void finalizer() +{ + [SyphonServerBase retireRemainingServers]; +} + +@implementation SyphonServerBase +{ + // Once our minimum version reaches 10.12, replace + // this with os_unfair_lock + OSSpinLock _mdLock; + + NSString *_name; + NSString *_uuid; + BOOL _broadcasts; + + SyphonServerConnectionManager *_connectionManager; + id _activityToken; + + IOSurfaceRef _surface; + BOOL _pushPending; +} + ++ (NSSet *)keyPathsForValuesAffectingValueForKey:(NSString *)key +{ + if ([key isEqualToString:@"serverDescription"]) + { + return [NSSet setWithObject:@"name"]; + } + else + { + return [super keyPathsForValuesAffectingValueForKey:key]; + } +} + ++ (BOOL)automaticallyNotifiesObserversForKey:(NSString *)theKey +{ + BOOL automatic; + if ([theKey isEqualToString:@"hasClients"]) + { + automatic=NO; + } + else + { + automatic=[super automaticallyNotifiesObserversForKey:theKey]; + } + return automatic; +} + +- (id)init +{ + return [self initWithName:@"" options:nil]; +} + +- (instancetype)initWithName:(NSString *)serverName options:(NSDictionary *)options +{ + self = [super init]; + if (self) + { + if (serverName == nil) + { + serverName = @""; + } + _name = [serverName copy]; + _uuid = SyphonCreateUUIDString(); + + NSNumber *isPrivate = [options objectForKey:SyphonServerOptionIsPrivate]; + if ([isPrivate respondsToSelector:@selector(boolValue)] + && [isPrivate boolValue] == YES) + { + _broadcasts = NO; + } + else + { + _broadcasts = YES; + } + + _mdLock = OS_SPINLOCK_INIT; + + _connectionManager = [[SyphonServerConnectionManager alloc] initWithUUID:_uuid options:options]; + + [_connectionManager addObserver:self forKeyPath:@"hasClients" options:NSKeyValueObservingOptionPrior context:nil]; + + if (![_connectionManager start]) + { + [self release]; + return nil; + } + + if (_broadcasts) + { + [[self class] addServerToRetireList:_uuid]; + [self startBroadcasts]; + } + + // Prevent this app from being suspended or terminated eg if it goes off-screen (MacOS 10.9+ only) + NSProcessInfo *processInfo = [NSProcessInfo processInfo]; + if ([processInfo respondsToSelector:@selector(beginActivityWithOptions:reason:)]) + { + NSActivityOptions options = NSActivityAutomaticTerminationDisabled | NSActivityBackground; + _activityToken = [[processInfo beginActivityWithOptions:options reason:_uuid] retain]; + } + } + return self; +} + +- (void)dealloc +{ + SYPHONLOG(@"Server deallocing, name: %@, UUID: %@", self.name, [self.serverDescription objectForKey:SyphonServerDescriptionUUIDKey]); + [self stop]; + [_name release]; + [_uuid release]; + [super dealloc]; +} + +- (NSString*)name +{ + OSSpinLockLock(&_mdLock); + NSString *result = [_name retain]; + OSSpinLockUnlock(&_mdLock); + return [result autorelease]; +} + +- (void)setName:(NSString *)newName +{ + if (newName == nil) + { + newName = @""; + } + [newName copy]; + [newName retain]; + OSSpinLockLock(&_mdLock); + [_name release]; + _name = newName; + OSSpinLockUnlock(&_mdLock); + [(SyphonServerConnectionManager *)_connectionManager setName:newName]; + if (_broadcasts) + { + [self broadcastServerUpdate]; + } +} + +- (NSDictionary *)serverDescription +{ + NSDictionary *surface = ((SyphonServerConnectionManager *)_connectionManager).surfaceDescription; + if (!surface) surface = [NSDictionary dictionary]; + /* + Getting the app name: helper tasks, command-line tools, etc, don't have a NSRunningApplication instance, + so fall back to NSProcessInfo in those cases, then use an empty string as a last resort. + + http://developer.apple.com/library/mac/qa/qa1544/_index.html + + */ + NSString *appName = [[NSRunningApplication currentApplication] localizedName]; + if (!appName) appName = [[NSProcessInfo processInfo] processName]; + if (!appName) appName = [NSString string]; + + return [NSDictionary dictionaryWithObjectsAndKeys: + [NSNumber numberWithUnsignedInt:kSyphonDictionaryVersion], SyphonServerDescriptionDictionaryVersionKey, + self.name, SyphonServerDescriptionNameKey, + _uuid, SyphonServerDescriptionUUIDKey, + appName, SyphonServerDescriptionAppNameKey, + [NSArray arrayWithObject:surface], SyphonServerDescriptionSurfacesKey, + nil]; +} + +- (BOOL)hasClients +{ + return ((SyphonServerConnectionManager *)_connectionManager).hasClients; +} + +- (void)stop +{ + if (_connectionManager) + { + [(SyphonServerConnectionManager *)_connectionManager removeObserver:self forKeyPath:@"hasClients"]; + [(SyphonServerConnectionManager *)_connectionManager stop]; + [(SyphonServerConnectionManager *)_connectionManager release]; + _connectionManager = nil; + } + if (_broadcasts) + { + [self stopBroadcasts]; + [[self class] removeServerFromRetireList:_uuid]; + } + if (_activityToken) + { + [[NSProcessInfo processInfo] endActivity:_activityToken]; + [_activityToken release]; + _activityToken = nil; + } + if (_surface != NULL) + { + CFRelease(_surface); + _surface = NULL; + } +} + +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context +{ + if ([keyPath isEqualToString:@"hasClients"]) + { + if ([[change objectForKey:NSKeyValueChangeNotificationIsPriorKey] boolValue] == YES) + { + [self willChangeValueForKey:keyPath]; + } + else + { + [self didChangeValueForKey:keyPath]; + } + } + else + { + [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; + } +} + +- (IOSurfaceRef)copySurface +{ + // TODO: we locking here? + CFRetain(_surface); + return _surface; +} + +- (void)setSurface:(IOSurfaceRef)surface +{ + // TODO: ditto re lock + if (surface) + { + CFRetain(surface); + } + if (_surface) + { + CFRelease(_surface); + } + _surface = surface; + _pushPending = YES; +} + +- (void)publish +{ + if (_pushPending) + { + // Push the new surface ID to clients + [(SyphonServerConnectionManager *)_connectionManager setSurfaceID:IOSurfaceGetID(_surface)]; + _pushPending = NO; + } + [(SyphonServerConnectionManager *)_connectionManager publishNewFrame]; +} +#pragma mark Notification Handling for Server Presence +/* + Broadcast and discovery is done via NSDistributedNotificationCenter. Servers notify announce, change (currently only affects name) and retirement. + Discovery is done by a discovery-request notification, to which servers respond with an announce. + + If this gets unweildy we could move it into a SyphonBroadcaster class + + */ + +/* + We track all instances and send a retirement broadcast for any which haven't been stopped when the code is unloaded. + */ + +static OSSpinLock mRetireListLock = OS_SPINLOCK_INIT; +static NSMutableSet *mRetireList = nil; + ++ (void)addServerToRetireList:(NSString *)serverUUID +{ + OSSpinLockLock(&mRetireListLock); + if (mRetireList == nil) + { + mRetireList = [[NSMutableSet alloc] initWithCapacity:1U]; + } + [mRetireList addObject:serverUUID]; + OSSpinLockUnlock(&mRetireListLock); +} + ++ (void)removeServerFromRetireList:(NSString *)serverUUID +{ + OSSpinLockLock(&mRetireListLock); + [mRetireList removeObject:serverUUID]; + if ([mRetireList count] == 0) + { + [mRetireList release]; + mRetireList = nil; + } + OSSpinLockUnlock(&mRetireListLock); +} + ++ (void)retireRemainingServers +{ + // take the set out of the global so we don't hold the spin-lock while we send the notifications + // even though there should never be contention for this + NSMutableSet *mySet = nil; + OSSpinLockLock(&mRetireListLock); + mySet = mRetireList; + mRetireList = nil; + OSSpinLockUnlock(&mRetireListLock); + for (NSString *uuid in mySet) { + SYPHONLOG(@"Retiring a server at code unload time because it was not properly stopped"); + NSDictionary *fakeServerDescription = [NSDictionary dictionaryWithObject:uuid forKey:SyphonServerDescriptionUUIDKey]; + [[NSDistributedNotificationCenter defaultCenter] postNotificationName:SyphonServerRetire + object:SyphonServerDescriptionUUIDKey + userInfo:fakeServerDescription + deliverImmediately:YES]; + } + [mySet release]; +} + +- (void)startBroadcasts +{ + // Register for any Announcement Requests. + [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(handleDiscoveryRequest:) name:SyphonServerAnnounceRequest object:nil]; + + [self broadcastServerAnnounce]; +} + +- (void) handleDiscoveryRequest:(NSNotification*) aNotification +{ + SYPHONLOG(@"Got Discovery Request"); + + [self broadcastServerAnnounce]; +} + +- (void)broadcastServerAnnounce +{ + if (_broadcasts) + { + NSDictionary *description = self.serverDescription; + [[NSDistributedNotificationCenter defaultCenter] postNotificationName:SyphonServerAnnounce + object:[description objectForKey:SyphonServerDescriptionUUIDKey] + userInfo:description + deliverImmediately:YES]; + } +} + +- (void)broadcastServerUpdate +{ + NSDictionary *description = self.serverDescription; + [[NSDistributedNotificationCenter defaultCenter] postNotificationName:SyphonServerUpdate + object:[description objectForKey:SyphonServerDescriptionUUIDKey] + userInfo:description + deliverImmediately:YES]; +} + +- (void)stopBroadcasts +{ + [[NSDistributedNotificationCenter defaultCenter] removeObserver:self]; + NSDictionary *description = self.serverDescription; + [[NSDistributedNotificationCenter defaultCenter] postNotificationName:SyphonServerRetire + object:[description objectForKey:SyphonServerDescriptionUUIDKey] + userInfo:description + deliverImmediately:YES]; +} + +@end