1// This file is part of BOINC.
2// http://boinc.berkeley.edu
3// Copyright (C) 2017 University of California
4//
5// BOINC is free software; you can redistribute it and/or modify it
6// under the terms of the GNU Lesser General Public License
7// as published by the Free Software Foundation,
8// either version 3 of the License, or (at your option) any later version.
9//
10// BOINC is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
13// See the GNU Lesser General Public License for more details.
14//
15// You should have received a copy of the GNU Lesser General Public License
16// along with BOINC.  If not, see <http://www.gnu.org/licenses/>.
17
18//
19//  Mac_Saver_ModuleView.m
20//  BOINC_Saver_Module
21//
22
23#define GL_DO_NOT_WARN_IF_MULTI_GL_VERSION_HEADERS_INCLUDED 1
24
25#import "Mac_Saver_ModuleView.h"
26#include <Carbon/Carbon.h>
27#include <AppKit/AppKit.h>
28#include <IOKit/hidsystem/IOHIDLib.h>
29#include <IOKit/hidsystem/IOHIDParameter.h>
30#include <IOKit/hidsystem/event_status_driver.h>
31#import <OpenGL/gl.h>
32#import <GLKit/GLKit.h>
33#include <servers/bootstrap.h>
34//#import <IOSurface/IOSurface.h>
35//#import <OpenGL/gl3.h>
36//#import <OpenGL/CGLIOSurface.h>
37
38#include "mac_util.h"
39#import "MultiGPUMig.h"
40#import "MultiGPUMigServer.h"
41
42#ifndef NSInteger
43#if __LP64__ || NS_BUILD_32_LIKE_64
44typedef long NSInteger;
45#else
46typedef int NSInteger;
47#endif
48#endif
49
50#ifndef CGFLOAT_DEFINED
51typedef float CGFloat;
52#endif
53
54// NSCompositeSourceOver is deprecated in OS 10.12 and is replaced by
55// NSCompositingOperationSourceOver, which is not defined before OS 10.12
56#ifndef NSCompositingOperationSourceOver
57#define NSCompositingOperationSourceOver NSCompositeSourceOver
58#endif
59
60// NSCompositeCopy is deprecated in OS 10.12 and is replaced by
61// NSCompositingOperationCopy, which is not defined before OS 10.12
62#ifndef NSCompositingOperationCopy
63#define NSCompositingOperationCopy NSCompositeCopy
64#endif
65
66// NSCriticalAlertStyle is deprecated in OS 10.12 and is replaced by
67// NSAlertStyleCritical, which is not defined before OS 10.12
68#ifndef NSAlertStyleCritical
69#define NSAlertStyleCritical NSCriticalAlertStyle
70#endif
71
72static double gSS_StartTime = 0.0;
73mach_port_t gEventHandle = 0;
74
75int gGoToBlank;      // True if we are to blank the screen
76int gBlankingTime;   // Delay in minutes before blanking the screen
77NSString *gPathToBundleResources = NULL;
78NSString *mBundleID = NULL; // our bundle ID
79NSImage *gBOINC_Logo = NULL;
80NSImage *gPreview_Image = NULL;
81
82int gTopWindowListIndex = -1;
83NSInteger myWindowNumber;
84
85NSRect gMovingRect;
86float gImageXIndent;
87float gTextBoxHeight;
88CGFloat gActualTextBoxHeight;
89NSPoint gCurrentPosition;
90NSPoint gCurrentDelta;
91
92CGContextRef myContext;
93bool isErased;
94
95static SharedGraphicsController *mySharedGraphicsController;
96static bool runningSharedGraphics;
97static bool useCGWindowList;
98static pid_t childPid;
99static int gfxAppWindowNum;
100static NSView *imageView;
101static char gfxAppPath[MAXPATHLEN];
102static int taskSlot;
103static NSRunningApplication *childApp;
104static double gfxAppStartTime;
105static bool UseSharedOffscreenBuffer(void);
106
107
108#define TEXTBOXMINWIDTH 400.0
109#define MINTEXTBOXHEIGHT 40.0
110#define MAXTEXTBOXHEIGHT 300.0
111#define TEXTBOXTOPBORDER 15
112#define SAFETYBORDER 20.0
113#define MINDELTA 8
114#define MAXDELTA 16
115
116// On OS 10.13+, assume graphics app is not compatible if no MachO connection after 5 seconds
117#define MAXWAITFORCONNECTION 5.0
118
119int signof(float x) {
120    return (x > 0.0 ? 1 : -1);
121}
122
123void launchedGfxApp(char * appPath, pid_t thePID, int slot) {
124    strlcpy(gfxAppPath, appPath, sizeof(gfxAppPath));
125    childPid = thePID;
126    taskSlot = slot;
127    gfxAppStartTime = getDTime();
128    if (thePID == 0) {
129        useCGWindowList = false;
130        gfxAppStartTime = 0.0;
131        if (imageView) {
132            [imageView removeFromSuperview];   // Releases imageView
133            imageView = nil;
134        }
135    }
136}
137
138@implementation BOINC_Saver_ModuleView
139
140- (id)initWithFrame:(NSRect)frame isPreview:(BOOL)isPreview {
141    self = [ super initWithFrame:frame isPreview:isPreview ];
142    return self;
143}
144
145// If there are multiple displays, this may get called
146// multiple times (once for each display), so we need to guard
147// against any problems that may cause.
148- (void)startAnimation {
149    NSBundle * myBundle;
150    int newFrequency;
151    int period;
152
153    gEventHandle = NXOpenEventStatus();
154
155    initBOINCSaver();
156
157    if (gBOINC_Logo == NULL) {
158        if (self) {
159            myBundle = [ NSBundle bundleForClass:[self class]];
160            // grab the screensaver defaults
161            if (mBundleID == NULL) {
162                mBundleID = [ myBundle bundleIdentifier ];
163            }
164
165            // Path to our copy of switcher utility application in this screensaver bundle
166            if (gPathToBundleResources == NULL) {
167                gPathToBundleResources = [ myBundle resourcePath ];
168            }
169
170            ScreenSaverDefaults *defaults = [ ScreenSaverDefaults defaultsForModuleWithName:mBundleID ];
171
172            // try to load the version key, used to see if we have any saved settings
173            mVersion = [defaults floatForKey:@"version"];
174            if (!mVersion) {
175                // no previous settings so define our defaults
176                gGoToBlank = NO;
177                gBlankingTime = 1;
178
179                // write out the defaults
180                [ defaults setInteger:gGoToBlank forKey:@"GoToBlank" ];
181                [ defaults setInteger:gBlankingTime forKey:@"BlankingTime" ];
182            }
183
184            if (mVersion < 2) {
185                mVersion = 2;
186
187                [ defaults setInteger:mVersion forKey:@"version" ];
188                period = getGFXDefaultPeriod() / 60;
189                [ defaults setInteger:period forKey:@"DefaultPeriod" ];
190                period = getGFXSciencePeriod() / 60;
191                [ defaults setInteger:period forKey:@"SciencePeriod" ];
192                period = getGGFXChangePeriod() / 60;
193                [ defaults setInteger:period forKey:@"ChangePeriod" ];
194
195                // synchronize
196                [defaults synchronize];
197            }
198
199            // get defaults...
200            gGoToBlank = [ defaults integerForKey:@"GoToBlank" ];
201            gBlankingTime = [ defaults integerForKey:@"BlankingTime" ];
202            period = [ defaults integerForKey:@"DefaultPeriod" ];
203            setGFXDefaultPeriod((double)(period * 60));
204            period = [ defaults integerForKey:@"SciencePeriod" ];
205            setGFXSciencePeriod((double)(period * 60));
206            period = [ defaults integerForKey:@"ChangePeriod" ];
207            setGGFXChangePeriod((double)(period * 60));
208
209           [ self setAutoresizesSubviews:YES ];	// make sure the subview resizes.
210
211            NSString *fileName = [[ NSBundle bundleForClass:[ self class ]] pathForImageResource:@"boinc_ss_logo" ];
212            if (! fileName) {
213                // What should we do in this case?
214                return;
215            }
216
217            gBOINC_Logo = [[ NSImage alloc ] initWithContentsOfFile:fileName ];
218            gMovingRect.origin.x = 0.0;
219            gMovingRect.origin.y = 0.0;
220            gMovingRect.size = [gBOINC_Logo size];
221
222            if (gMovingRect.size.width < TEXTBOXMINWIDTH) {
223                gImageXIndent = (TEXTBOXMINWIDTH - gMovingRect.size.width) / 2;
224                gMovingRect.size.width = TEXTBOXMINWIDTH;
225            } else {
226                gImageXIndent = 0.0;
227            }
228            gTextBoxHeight = MINTEXTBOXHEIGHT;
229            gMovingRect.size.height += gTextBoxHeight;
230            gCurrentPosition.x = SAFETYBORDER + 1;
231            gCurrentPosition.y = SAFETYBORDER + 1 + gTextBoxHeight;
232            gCurrentDelta.x = 1.0;
233            gCurrentDelta.y = 1.0;
234
235            gActualTextBoxHeight = MINTEXTBOXHEIGHT;
236
237            [ self setAnimationTimeInterval:1/8.0 ];
238        }
239    }
240
241    // Path to our copy of switcher utility application in this screensaver bundle
242    if (gPathToBundleResources == NULL) {
243        gPathToBundleResources = [ myBundle resourcePath ];
244    }
245
246    [ super startAnimation ];
247
248    if ( [ self isPreview ] ) {
249        [ self setAnimationTimeInterval:1.0/8.0 ];
250        return;
251    }
252
253    newFrequency = startBOINCSaver();
254    if (newFrequency)
255        [ self setAnimationTimeInterval:1.0/newFrequency ];
256
257    gSS_StartTime = getDTime();
258}
259
260// If there are multiple displays, this may get called
261// multiple times (once for each display), so we need to guard
262// against any problems that may cause.
263- (void)stopAnimation {
264    [ super stopAnimation ];
265
266    if ( ! [ self isPreview ] ) {
267        closeBOINCSaver();
268    }
269
270    gTopWindowListIndex = -1;
271
272//    if (gBOINC_Logo) {
273//        [ gBOINC_Logo release ];
274//    }
275    gBOINC_Logo = NULL;
276
277    // gPathToBundleResources has been released by autorelease
278    gPathToBundleResources = NULL;
279}
280
281// If there are multiple displays, this may get called
282// multiple times (once for each display), so we need to guard
283// against any problems that may cause.
284- (void)drawRect:(NSRect)rect {
285    [ super drawRect:rect ];
286
287//  optionally draw here
288}
289
290// If there are multiple displays, this may get called
291// multiple times (once for each display), so we need to guard
292// against any problems that may cause.
293- (void)animateOneFrame {
294    int newFrequency = 0;
295    int coveredFreq = 0;
296    NSRect theFrame = [ self frame ];
297    NSUInteger n;
298    NSRect currentDrawingRect, eraseRect;
299    NSPoint imagePosition;
300    char *msg;
301    CFStringRef cf_msg;
302    double timeToBlock, frameStartTime = getDTime();
303    double          idleTime = 0;
304    HIThemeTextInfo textInfo;
305
306   if ([ self isPreview ]) {
307#if 1   // Currently drawRect just draws our logo in the preview window
308        if (gPreview_Image == NULL) {
309            NSString *fileName = [[ NSBundle bundleForClass:[ self class ]] pathForImageResource:@"boinc" ];
310            if (fileName) {
311                gPreview_Image = [[ NSImage alloc ] initWithContentsOfFile:fileName ];
312            }
313        }
314        if (gPreview_Image) {
315            [ gPreview_Image setSize:theFrame.size ];
316            [ gPreview_Image drawAtPoint:NSZeroPoint fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0 ];
317        }
318        [ self setAnimationTimeInterval:1/1.0 ];
319#else   // Code for possible future use if we want to draw more in preview
320        myContext = [[NSGraphicsContext currentContext] graphicsPort];
321        drawPreview(myContext);
322        [ self setAnimationTimeInterval:1/30.0 ];
323#endif
324        return;
325    } else {
326        NSWindow *myWindow = [ self window ];
327        NSRect windowFrame = [ myWindow frame ];
328        if ( (windowFrame.origin.x == 0) && (windowFrame.origin.y == 0) ) { // Main screen
329            // On OS 10.13 or later, use MachO comunication and IOSurfaceBuffer to
330            // display the graphics output of our child graphics apps in our window.
331            if (UseSharedOffscreenBuffer() && !mySharedGraphicsController) {
332                mySharedGraphicsController = [[SharedGraphicsController alloc] init:self] ;
333            }
334        }
335    }
336
337    // For unkown reasons, OS 10.7 Lion screensaver and later delay several seconds
338    // after user activity before calling stopAnimation, so we check user activity here
339    if ((compareOSVersionTo(10, 7) >= 0) && ((getDTime() - gSS_StartTime) > 2.0)) {
340           idleTime =  CGEventSourceSecondsSinceLastEventType
341                    (kCGEventSourceStateCombinedSessionState, kCGAnyInputEventType);
342        if (idleTime < 1.5) {
343            [ NSApp terminate:nil ];
344        }
345    }
346
347   myContext = [[NSGraphicsContext currentContext] graphicsPort];
348//    [myContext retain];
349
350    NSWindow *myWindow = [ self window ];
351    NSRect windowFrame = [ myWindow frame ];
352    if ( (windowFrame.origin.x != 0) || (windowFrame.origin.y != 0) ) {
353        // Hide window on second display to aid in debugging
354#ifdef _DEBUG
355        // This technique no longer works on newer versions of OS X
356        [ myWindow setLevel:kCGMinimumWindowLevel ];
357        NSInteger alpha = 0;
358        [ myWindow setAlphaValue:alpha ];   // For OS 10.6
359        [ myWindow orderOut:self];
360#endif
361        return;         // We draw only to main screen
362    }
363
364	// On OS 10.13 or later, use MachO comunication and IOSurfaceBuffer to
365	// display the graphics output of our child graphics apps in our window.
366    // Graphics apps linked with our current libraries have support for
367    // MachO comunication and IOSurfaceBuffer.
368    //
369    // For graphics apps linked with older libraries, use the API
370    // CGWindowListCreateImage to copy the graphic app window's image,
371    // but this is far slower because it does not take advantage of GPU
372    // acceleration, so it uses more CPU and animation may not appear smooth.
373    //
374    if (runningSharedGraphics || useCGWindowList ) {
375        // Since ScreensaverEngine.app is running in the foreground, our child
376        // graphics app may not get enough CPU cycles for good animation.
377        // Calling [ NSApp activateIgnoringOtherApps:YES ] frequently from the
378        // child doesn't help. But activating our child frequently from the
379        // front process (this screensaver plugin) does appear to guarantee
380        // good animation.
381        //
382        // An alternate approach that also works is to have the child process
383        // tell the kernel it has real time constraints by calling
384        // thread_policy_set() with thread_policy_flavor_t set to
385        // THREAD_TIME_CONSTRAINT_POLICY as described in
386        // <https://developer.apple.com/library/content/technotes/tn2169>.
387        //
388        // But different graphics apps may have different time requirements,
389        // so it is difficult to know the best values to set in the
390        // thread_time_constraint_policy_data_t struct. If the graphics app asks
391        // for too much time, the worker apps will get less time, and if it asks
392        // for too little time the animation won't be smooth.
393        //
394        // So frequently activating the child app here seems to be best.
395        //
396        if (childApp) {
397             if (![ childApp activateWithOptions:NSApplicationActivateIgnoringOtherApps ]) {
398                launchedGfxApp("", 0, -1);  // Graphics app is no longer running
399             }
400             if (useCGWindowList) {
401                CGImageRef windowImage = CGWindowListCreateImage(CGRectNull,
402                                            kCGWindowListOptionIncludingWindow,
403                                            gfxAppWindowNum,
404                                            kCGWindowImageBoundsIgnoreFraming);
405                if (windowImage) {
406                    // Create a bitmap rep from the image...
407                    NSBitmapImageRep *bitmapRep = [[NSBitmapImageRep alloc] initWithCGImage:windowImage];
408                    // Create an NSImage and add the bitmap rep to it...
409                    NSImage *image = [[NSImage alloc] init];
410                    [image addRepresentation:bitmapRep];
411                    [image drawInRect:[self frame]];
412                    CGImageRelease(windowImage);
413                }
414            }
415        }
416        isErased = false;
417        return;
418    }
419
420    NSRect viewBounds = [self bounds];
421
422    newFrequency = getSSMessage(&msg, &coveredFreq);
423
424    if (UseSharedOffscreenBuffer()) {
425        // If runningSharedGraphics is still false after MAXWAITFORCONNECTION,
426        // assume graphics app is not compatible with OS 10.13+ and kill it.
427        if (gfxAppStartTime) {
428            if ((getDTime() - gfxAppStartTime)> MAXWAITFORCONNECTION) {
429                gfxAppStartTime = 0.0;
430                if ([self setUpToUseCGWindowList] == false) {
431                    incompatibleGfxApp(gfxAppPath, childPid, taskSlot);
432                }
433            }
434        }
435    // As of OS 10.13, app windows can no longer appear on top of screensaver
436    // window, but we still use this method on older versions of OS X for
437    // compatibility with older project graphics apps (those which have not
438    // yet been relinked with the updated libboinc_graphics2.a.)
439    } else {
440        // NOTE: My tests seem to confirm that the top window is always the first
441        // window returned by [NSWindow windowNumbersWithOptions:] However, Apple's
442        // documentation is unclear whether we can depend on this.  So I have
443        // added some safety by doing two things:
444        // [1] Only use the windowNumbersWithOptions test when we have started
445        //     project graphics.
446        // [2] Assume that our window is covered 45 seconds after starting project
447        //     graphics even if the windowNumbersWithOptions test did not indicate
448        //     that is so.
449        //
450        // getSSMessage() returns a non-zero value for coveredFreq only if we have started
451        // project graphics.
452        //
453        // If we should use a different frequency when our window is covered by another
454        // window, then check whether there is a window at a higher z-level than ours.
455
456        // Assuming our window(s) are initially the top window(s), determine our position
457        // in the window list when no graphics applications have covered us.
458        if (gTopWindowListIndex < 0) {
459            NSArray *theWindowList = [NSWindow windowNumbersWithOptions:NSWindowNumberListAllApplications];
460            myWindowNumber = [ myWindow windowNumber ];
461            gTopWindowListIndex = [theWindowList indexOfObjectIdenticalTo:[NSNumber numberWithInt:myWindowNumber]];
462        }
463
464        if (coveredFreq) {
465            if ( (msg != NULL) && (msg[0] != '\0') ) {
466                NSArray *theWindowList = [NSWindow windowNumbersWithOptions:NSWindowNumberListAllApplications];
467                n = [theWindowList count];
468                if (gTopWindowListIndex < n) {
469                    if ([(NSNumber*)[theWindowList objectAtIndex:gTopWindowListIndex] integerValue] != myWindowNumber) {
470                        // Project graphics application has a window open above ours
471                        // Don't waste CPU cycles since our window is obscured by application graphics
472                        newFrequency = coveredFreq;
473                        msg = NULL;
474                        windowIsCovered();
475                    }
476                }
477            } else {
478                newFrequency = coveredFreq;
479            }
480        }
481    }
482
483    // Draw our moving BOINC logo and screensaver status text
484
485    // Clear the previous drawing area
486    currentDrawingRect = gMovingRect;
487    currentDrawingRect.origin.x = (float) ((int)gCurrentPosition.x);
488    currentDrawingRect.origin.y += (float) ((int)gCurrentPosition.y - gTextBoxHeight);
489
490    if ( (msg != NULL) && (msg[0] != '\0') ) {
491
492        // Set direction of motion to "bounce" off edges of screen
493       if (currentDrawingRect.origin.x <= SAFETYBORDER) {
494            gCurrentDelta.x = (float)SSRandomIntBetween(MINDELTA, MAXDELTA) / 16.;
495            gCurrentDelta.y = (float)(SSRandomIntBetween(MINDELTA, MAXDELTA) * signof(gCurrentDelta.y)) / 16.;
496        }
497        if ( (currentDrawingRect.origin.x + currentDrawingRect.size.width) >=
498                    (viewBounds.origin.x + viewBounds.size.width - SAFETYBORDER) ) {
499            gCurrentDelta.x = -(float)SSRandomIntBetween(MINDELTA, MAXDELTA) / 16.;
500            gCurrentDelta.y = (float)(SSRandomIntBetween(MINDELTA, MAXDELTA) * signof(gCurrentDelta.y)) / 16.;
501        }
502        if (currentDrawingRect.origin.y + gTextBoxHeight - gActualTextBoxHeight <= SAFETYBORDER) {
503            gCurrentDelta.y = (float)SSRandomIntBetween(MINDELTA, MAXDELTA) / 16.;
504            gCurrentDelta.x = (float)(SSRandomIntBetween(MINDELTA, MAXDELTA) * signof(gCurrentDelta.x)) / 16.;
505        }
506        if ( (currentDrawingRect.origin.y + currentDrawingRect.size.height) >=
507                   (viewBounds.origin.y + viewBounds.size.height - SAFETYBORDER) ) {
508            gCurrentDelta.y = -(float)SSRandomIntBetween(MINDELTA, MAXDELTA) / 16.;
509            gCurrentDelta.x = (float)(SSRandomIntBetween(MINDELTA, MAXDELTA) * signof(gCurrentDelta.x)) / 16.;
510        }
511#if 0
512        // For testing
513        gCurrentDelta.x = 0;
514        gCurrentDelta.y = 0;
515#endif
516
517        if (!isErased) {
518            [[NSColor blackColor] set];
519
520            // Erasing only 2 small rectangles reduces screensaver's CPU usage by about 25%
521            imagePosition.x = (float) ((int)gCurrentPosition.x + gImageXIndent);
522            imagePosition.y = (float) (int)gCurrentPosition.y;
523            eraseRect.origin.y = imagePosition.y;
524            eraseRect.size.height = currentDrawingRect.size.height - gTextBoxHeight;
525
526            if (gCurrentDelta.x > 0) {
527                eraseRect.origin.x = imagePosition.x - 1;
528                eraseRect.size.width = gCurrentDelta.x + 1;
529            } else {
530                eraseRect.origin.x = currentDrawingRect.origin.x + currentDrawingRect.size.width - gImageXIndent + gCurrentDelta.x - 1;
531                eraseRect.size.width = -gCurrentDelta.x + 1;
532            }
533
534            eraseRect = NSInsetRect(eraseRect, -1, -1);
535            NSRectFill(eraseRect);
536
537            eraseRect.origin.x = imagePosition.x;
538            eraseRect.size.width = currentDrawingRect.size.width - gImageXIndent - gImageXIndent;
539
540            if (gCurrentDelta.y > 0) {
541                eraseRect.origin.y = imagePosition.y;
542                eraseRect.size.height = gCurrentDelta.y + 1;
543            } else {
544                eraseRect.origin.y = imagePosition.y + currentDrawingRect.size.height - gTextBoxHeight - 1;
545                eraseRect.size.height = -gCurrentDelta.y + 1;
546            }
547            eraseRect = NSInsetRect(eraseRect, -1, -1);
548            NSRectFill(eraseRect);
549
550            eraseRect = currentDrawingRect;
551            eraseRect.size.height = gTextBoxHeight;
552            eraseRect = NSInsetRect(eraseRect, -1, -1);
553            NSRectFill(eraseRect);
554
555            isErased  = true;
556        }
557
558        // Get the new drawing area
559        gCurrentPosition.x += gCurrentDelta.x;
560        gCurrentPosition.y += gCurrentDelta.y;
561
562        imagePosition.x = (float) ((int)gCurrentPosition.x + gImageXIndent);
563        imagePosition.y = (float) (int)gCurrentPosition.y;
564
565        [ gBOINC_Logo drawAtPoint:imagePosition fromRect:NSZeroRect operation:NSCompositeCopy fraction:1.0 ];
566
567        if ( (msg != NULL) && (msg[0] != '\0') ) {
568            cf_msg = CFStringCreateWithCString(NULL, msg, kCFStringEncodingMacRoman);
569
570            CGRect bounds = CGRectMake((float) ((int)gCurrentPosition.x),
571                                 viewBounds.size.height - imagePosition.y + TEXTBOXTOPBORDER,
572                                 gMovingRect.size.width,
573                                 MAXTEXTBOXHEIGHT
574                            );
575
576            CGContextSaveGState (myContext);
577            CGContextTranslateCTM (myContext, 0, viewBounds.origin.y + viewBounds.size.height);
578            CGContextScaleCTM (myContext, 1.0f, -1.0f);
579
580            CTFontRef myFont = CTFontCreateWithName(CFSTR("Helvetica"), 20, NULL);
581
582            HIThemeTextInfo theTextInfo = {kHIThemeTextInfoVersionOne, kThemeStateActive, kThemeSpecifiedFont,
583                        kHIThemeTextHorizontalFlushLeft, kHIThemeTextVerticalFlushTop,
584                        kHIThemeTextBoxOptionNone, kHIThemeTextTruncationNone, 0, false,
585                        0, myFont
586                        };
587            textInfo = theTextInfo;
588
589            HIThemeGetTextDimensions(cf_msg, (float)gMovingRect.size.width, &textInfo, NULL, &gActualTextBoxHeight, NULL);
590            gActualTextBoxHeight += TEXTBOXTOPBORDER;
591
592            CGFloat myWhiteComponents[] = {1.0, 1.0, 1.0, 1.0};
593            CGColorSpaceRef myColorSpace = CGColorSpaceCreateDeviceRGB ();
594            CGColorRef myTextColor = CGColorCreate(myColorSpace, myWhiteComponents);
595
596            CGContextSetFillColorWithColor(myContext, myTextColor);
597
598            HIThemeDrawTextBox(cf_msg, &bounds, &textInfo, myContext, kHIThemeOrientationNormal);
599
600            CGColorRelease(myTextColor);
601            CGColorSpaceRelease(myColorSpace);
602            CGContextRestoreGState (myContext);
603            CFRelease(cf_msg);
604        }
605
606        gTextBoxHeight = MAXTEXTBOXHEIGHT + TEXTBOXTOPBORDER;
607        gMovingRect.size.height = [gBOINC_Logo size].height + gTextBoxHeight;
608
609        isErased  = false;
610
611    } else {        // Empty or NULL message
612        if (!isErased) {
613            eraseRect = NSInsetRect(currentDrawingRect, -1, -1);
614            [[NSColor blackColor] set];
615            isErased  = true;
616            NSRectFill(eraseRect);
617            gTextBoxHeight = MAXTEXTBOXHEIGHT;
618            gMovingRect.size.height = [gBOINC_Logo size].height + gTextBoxHeight;
619        }
620    }
621
622    if (newFrequency) {
623        [ self setAnimationTimeInterval:(1.0/newFrequency) ];
624        // setAnimationTimeInterval does not seem to be working, so we
625        // throttle the screensaver directly here.
626        timeToBlock = (1.0/newFrequency) - (getDTime() - frameStartTime);
627        if (timeToBlock > 0.0) {
628            doBoinc_Sleep(timeToBlock);
629        }
630    }
631
632    // Check for a new graphics app sending us data
633    if (UseSharedOffscreenBuffer() && gfxAppStartTime) {
634        [mySharedGraphicsController testConnection];
635    }
636}
637
638- (BOOL)hasConfigureSheet {
639    return YES;
640}
641
642// Display the configuration sheet for the user to choose their settings
643- (NSWindow*)configureSheet
644{
645    int period;
646
647	// if we haven't loaded our configure sheet, load the nib named MyScreenSaver.nib
648	if (!mConfigureSheet) {
649        if ([[ NSBundle bundleForClass:[ self class ]] respondsToSelector: @selector(loadNibNamed: owner: topLevelObjects:)]) {
650
651#pragma clang diagnostic push
652#pragma clang diagnostic ignored "-Wobjc-method-access"
653            // [NSBundle loadNibNamed: owner: topLevelObjects:] is not available before OS 10.8
654            [ [ NSBundle bundleForClass:[ self class ]] loadNibNamed:@"BOINCSaver" owner:self topLevelObjects:NULL ];
655#pragma clang diagnostic pop
656        }
657#if __MAC_OS_X_VERSION_MIN_REQUIRED < 1080
658         else {
659#pragma clang diagnostic push
660#pragma clang diagnostic ignored "-Wdeprecated-declarations"
661            // [NSBundle loadNibNamed: owner:] is deprecated in OS 10.8
662            [ NSBundle loadNibNamed:@"BOINCSaver" owner:self ];
663#pragma clang diagnostic pop
664        }
665#endif
666    }
667	// set the UI state
668	[ mGoToBlankCheckbox setState:gGoToBlank ];
669
670    mBlankingTimeString = [[ NSString alloc ] initWithFormat:@"%d", gBlankingTime ];
671	[ mBlankingTimeTextField setStringValue:mBlankingTimeString ];
672
673    period = getGFXDefaultPeriod() / 60;
674    mDefaultPeriodString = [[ NSString alloc ] initWithFormat:@"%d", period ];
675	[ mDefaultPeriodTextField setStringValue:mDefaultPeriodString ];
676
677    period = getGFXSciencePeriod() / 60;
678    mSciencePeriodString = [[ NSString alloc ] initWithFormat:@"%d", period ];
679	[ mSciencePeriodTextField setStringValue:mSciencePeriodString ];
680
681    period = getGGFXChangePeriod() / 60;
682    mChangePeriodString = [[ NSString alloc ] initWithFormat:@"%d", period ];
683	[ mChangePeriodTextField setStringValue:mChangePeriodString ];
684
685	return mConfigureSheet;
686}
687
688// Called when the user clicked the SAVE button
689- (IBAction) closeSheetSave:(id) sender
690{
691    int period = 0;
692
693    NSScanner *scanner, *scanner2;
694
695    // get the defaults
696	ScreenSaverDefaults *defaults = [ ScreenSaverDefaults defaultsForModuleWithName:mBundleID ];
697
698	// save the UI state
699	gGoToBlank = [ mGoToBlankCheckbox state ];
700	mBlankingTimeString = [ mBlankingTimeTextField stringValue ];
701    gBlankingTime = [ mBlankingTimeString intValue ];
702    scanner = [ NSScanner scannerWithString:mBlankingTimeString];
703    if (![ scanner scanInt:&period ]) goto Bad;
704    if (![ scanner isAtEnd ]) goto Bad;
705    if ((period < 0) || (period > 999)) goto Bad;
706    gBlankingTime = period;
707
708	mDefaultPeriodString = [ mDefaultPeriodTextField stringValue ];
709    scanner2 = [ scanner initWithString:mDefaultPeriodString];
710    if (![ scanner2 scanInt:&period ]) goto Bad;
711    if (![ scanner2 isAtEnd ]) goto Bad;
712    if ((period < 0) || (period > 999)) goto Bad;
713    setGFXDefaultPeriod((double)(period * 60));
714
715	mSciencePeriodString = [ mSciencePeriodTextField stringValue ];
716    scanner2 = [ scanner initWithString:mSciencePeriodString];
717    if (![ scanner2 scanInt:&period ]) goto Bad;
718    if (![ scanner2 isAtEnd ]) goto Bad;
719    if ((period < 0) || (period > 999)) goto Bad;
720    setGFXSciencePeriod((double)(period * 60));
721
722	mChangePeriodString = [ mChangePeriodTextField stringValue ];
723    scanner2 = [ scanner initWithString:mChangePeriodString];
724    if (![ scanner2 scanInt:&period ]) goto Bad;
725    if (![ scanner2 isAtEnd ]) goto Bad;
726    if ((period < 0) || (period > 999)) goto Bad;
727    setGGFXChangePeriod((double)(period * 60));
728
729	// write the defaults
730	[ defaults setInteger:gGoToBlank forKey:@"GoToBlank" ];
731	[ defaults setInteger:gBlankingTime forKey:@"BlankingTime" ];
732    period = getGFXDefaultPeriod() / 60;
733    [ defaults setInteger:period forKey:@"DefaultPeriod" ];
734    period = getGFXSciencePeriod() / 60;
735    [ defaults setInteger:period forKey:@"SciencePeriod" ];
736    period = getGGFXChangePeriod() / 60;
737    [ defaults setInteger:period forKey:@"ChangePeriod" ];
738
739	// synchronize
740    [ defaults synchronize ];
741
742	// end the sheet
743    [ NSApp endSheet:mConfigureSheet ];
744    return;
745Bad:
746;   // Empty statement is needed to prevent compiler error
747    NSAlert *alert = [[NSAlert alloc] init];
748    [alert addButtonWithTitle:@"OK"];
749    [alert setMessageText:@"Please enter a number between 0 and 999."];
750    [alert setAlertStyle:NSCriticalAlertStyle];
751
752    if ([alert respondsToSelector: @selector(beginSheetModalForWindow: completionHandler:)]){
753#pragma clang diagnostic push
754#pragma clang diagnostic ignored "-Wobjc-method-access"
755        // [NSAlert beginSheetModalForWindow: completionHandler:] is not available before OS 10.9
756        [alert beginSheetModalForWindow:mConfigureSheet completionHandler:^(NSModalResponse returnCode){}];
757#pragma clang diagnostic pop
758    }
759#if __MAC_OS_X_VERSION_MIN_REQUIRED < 1090
760        else {
761#pragma clang diagnostic push
762#pragma clang diagnostic ignored "-Wdeprecated-declarations"
763            // [NSAlert beginSheetModalForWindow: modalDelegate: didEndSelector: contextInfo:] is deprecated in OS 10.9
764            [alert beginSheetModalForWindow:mConfigureSheet modalDelegate:self didEndSelector:nil contextInfo:nil];
765#pragma clang diagnostic pop
766        }
767#endif
768}
769
770// Called when the user clicked the CANCEL button
771- (IBAction) closeSheetCancel:(id) sender
772{
773	// nothing to configure
774    [ NSApp endSheet:mConfigureSheet ];
775}
776
777// Find the gtaphics app's window number (window ID)
778- (bool) setUpToUseCGWindowList
779{
780    NSArray *windowList = (__bridge NSArray*)CGWindowListCopyWindowInfo(
781                            kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements,
782                            kCGNullWindowID);
783    for (int i=[windowList count]-1; i>=0; i--) {
784        NSDictionary *dict = (NSDictionary*)(windowList[i]);
785        NSString * pidString = dict[(id)kCGWindowOwnerPID];
786        if ((pid_t)[pidString intValue] == childPid) {
787            NSString * windowNumString = dict[(id)kCGWindowNumber];
788            gfxAppWindowNum = (int)[windowNumString intValue];
789            useCGWindowList = true;
790            childApp = [NSRunningApplication runningApplicationWithProcessIdentifier:childPid];
791            if (imageView == nil) {
792                imageView = [[NSView alloc] initWithFrame:[self frame]];
793                [self addSubview:imageView];
794            }
795            return true;    // Success
796        }
797    }
798    return false;   // Not found
799}
800
801@end
802
803// On OS 10.13 or later, use MachO comunication and IOSurfaceBuffer to
804// display the graphics output of our child graphics apps in our window.
805// All code past this point is for that implementation.
806
807// Adapted from Apple Developer Tech Support Sample Code MutiGPUIOSurface:
808// <https://developer.apple.com/library/content/samplecode/MultiGPUIOSurface>
809
810#define NUM_IOSURFACE_BUFFERS 2
811
812@interface SharedGraphicsController()
813{
814	NSMachPort *serverPort;
815	NSMachPort *localPort;
816
817	uint32_t serverPortName;
818	uint32_t localPortName;
819
820	int32_t clientIndex;
821	uint32_t nextFrameIndex;
822
823    NSView *screenSaverView;
824    saverOpenGLView *openGLView;
825
826	IOSurfaceRef _ioSurfaceBuffers[NUM_IOSURFACE_BUFFERS];
827    mach_port_t _ioSurfaceMachPorts[NUM_IOSURFACE_BUFFERS];
828	GLuint _textureNames[NUM_IOSURFACE_BUFFERS];
829}
830@end
831
832static bool okToDraw;
833
834@implementation SharedGraphicsController
835
836- (instancetype)init:(NSView*)saverView {
837    screenSaverView = saverView;
838
839    [[NSNotificationCenter defaultCenter] addObserver:self
840	    selector:@selector(portDied:) name:NSPortDidBecomeInvalidNotification object:nil];
841
842    [self testConnection];
843
844    return self;
845}
846
847
848- (void) testConnection
849{
850    mach_port_t servicePortNum = MACH_PORT_NULL;
851    kern_return_t machErr;
852    char *portName = "edu.berkeley.boincsaver";
853
854	// Try to check in with master.
855// NSMachBootstrapServer is deprecated in OS 10.13, so use bootstrap_look_up
856//	serverPort = [(NSMachPort *)([[NSMachBootstrapServer sharedInstance] portForName:@"edu.berkeley.boincsaver"]) retain];
857	machErr = bootstrap_look_up(bootstrap_port, portName, &servicePortNum);
858    if (machErr == KERN_SUCCESS) {
859        serverPort = (NSMachPort*)[NSMachPort portWithMachPort:servicePortNum];
860    } else {
861        serverPort = MACH_PORT_NULL;
862    }
863
864	if(serverPort != MACH_PORT_NULL)
865	{
866		// Create our own local port.
867		localPort = [[NSMachPort alloc] init];
868
869		// Retrieve raw mach port names.
870		serverPortName = [serverPort machPort];
871		localPortName  = [localPort machPort];
872
873		// Register our local port with the current runloop.
874		[localPort setDelegate:self];
875		[localPort scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
876
877		// Check in with server.
878		int kr;
879		kr = _MGCCheckinClient(serverPortName, localPortName, &clientIndex);
880		if(kr != 0)
881			[NSApp terminate:nil];
882
883        openGLView = [[saverOpenGLView alloc] initWithFrame:[screenSaverView frame]];
884
885        [screenSaverView addSubview:openGLView];
886
887        runningSharedGraphics = true;
888
889        if (childPid) {
890            gfxAppStartTime = 0.0;
891            childApp = [NSRunningApplication runningApplicationWithProcessIdentifier:childPid];
892        }
893    }
894}
895
896- (void)portDied:(NSNotification *)notification
897{
898	NSPort *port = [notification object];
899	if(port == serverPort) {
900        childApp = nil;
901        gfxAppStartTime = 0.0;
902        gfxAppPath[0] = '\0';
903
904        if ([serverPort isValid]) {
905            [serverPort invalidate];
906//            [serverPort release];
907        }
908        serverPort = nil;
909		[localPort removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
910
911        if ([localPort isValid]) {
912            [localPort invalidate];
913        }
914//        [localPort release];
915        localPort = nil;
916
917        int i;
918        for(i = 0; i < NUM_IOSURFACE_BUFFERS; i++) {
919            if (_ioSurfaceBuffers[i]) {
920                CFRelease(_ioSurfaceBuffers[i]);
921                _ioSurfaceBuffers[i] = nil;
922            }
923
924            // if (glIsTexture(_textureNames[i])) {
925                // glDeleteTextures(1, _textureNames[i]);
926            // }
927            _textureNames[i] = 0;
928
929            if (_ioSurfaceMachPorts[i] != MACH_PORT_NULL) {
930                mach_port_deallocate(mach_task_self(), _ioSurfaceMachPorts[i]);
931                _ioSurfaceMachPorts[i] = MACH_PORT_NULL;
932            }
933        }
934
935        if ((serverPort == nil) && (localPort == nil)) {
936            runningSharedGraphics = false;
937            [openGLView removeFromSuperview];   // Releases openGLView
938        }
939	}
940}
941- (void)handleMachMessage:(void *)msg
942{
943	union __ReplyUnion___MGCMGSServer_subsystem reply;
944
945	mach_msg_header_t *reply_header = (void *)&reply;
946	kern_return_t kr;
947
948	if(MGSServer_server(msg, reply_header) && reply_header->msgh_remote_port != MACH_PORT_NULL)
949	{
950		kr = mach_msg(reply_header, MACH_SEND_MSG, reply_header->msgh_size, 0, MACH_PORT_NULL,
951			     0, MACH_PORT_NULL);
952        if(kr != 0)
953			[NSApp terminate:nil];
954	}
955}
956
957- (kern_return_t)displayFrame:(int32_t)frameIndex surfacemachport:(mach_port_t)iosurface_port
958{
959	nextFrameIndex = frameIndex;
960
961	if(!_ioSurfaceBuffers[frameIndex])
962	{
963		_ioSurfaceBuffers[frameIndex] = IOSurfaceLookupFromMachPort(iosurface_port);
964        _ioSurfaceMachPorts[frameIndex] = iosurface_port;
965	}
966	if(!_textureNames[frameIndex])
967    {
968		_textureNames[frameIndex] = [openGLView setupIOSurfaceTexture:_ioSurfaceBuffers[frameIndex]];
969    }
970
971    okToDraw = true;    // Tell drawRect that we have real data to display
972
973	[openGLView setNeedsDisplay:YES];
974	[openGLView display];
975
976	return 0;
977}
978
979// For the MachO client, this is a no-op.
980kern_return_t _MGSCheckinClient(mach_port_t server_port, mach_port_t client_port,
981			       int32_t *client_index)
982{
983	return 0;
984}
985
986kern_return_t _MGSDisplayFrame(mach_port_t server_port, int32_t frame_index, mach_port_t iosurface_port)
987{
988	return [mySharedGraphicsController displayFrame:frame_index surfacemachport:iosurface_port];
989}
990
991- (GLuint)currentTextureName
992{
993	return _textureNames[nextFrameIndex];
994}
995
996@end
997
998@implementation saverOpenGLView
999
1000- (instancetype)initWithFrame:(NSRect)frame {
1001    NSOpenGLPixelFormatAttribute	attribs []	=
1002    {
1003//		NSOpenGLPFAWindow,
1004		NSOpenGLPFADoubleBuffer,
1005		NSOpenGLPFAAccelerated,
1006		NSOpenGLPFANoRecovery,
1007		NSOpenGLPFAColorSize,		(NSOpenGLPixelFormatAttribute)32,
1008		NSOpenGLPFAAlphaSize,		(NSOpenGLPixelFormatAttribute)8,
1009		NSOpenGLPFADepthSize,		(NSOpenGLPixelFormatAttribute)24,
1010		(NSOpenGLPixelFormatAttribute) 0
1011	};
1012
1013    NSOpenGLPixelFormat *pix_fmt = [[NSOpenGLPixelFormat alloc] initWithAttributes:attribs];
1014
1015    if(!pix_fmt)
1016       [ NSApp terminate:nil];
1017
1018	self = [super initWithFrame:frame pixelFormat:pix_fmt];
1019
1020
1021	[[self openGLContext] makeCurrentContext];
1022
1023    // drawRect is apparently called due to the above code, causing the
1024    // screen to flash unless we prevent any actual drawing, so tell
1025    // drawRect that we do not yet have real data to display
1026    okToDraw = false;
1027
1028	return self;
1029}
1030
1031- (void)prepareOpenGL
1032{
1033    [super prepareOpenGL];
1034}
1035
1036- (void)update
1037{
1038	// Override to do nothing.
1039}
1040
1041// Create an IOSurface backed texture
1042- (GLuint)setupIOSurfaceTexture:(IOSurfaceRef)ioSurfaceBuffer
1043{
1044	GLuint name;
1045	CGLContextObj cgl_ctx = (CGLContextObj)[[self openGLContext] CGLContextObj];
1046
1047	glGenTextures(1, &name);
1048
1049	glBindTexture(GL_TEXTURE_RECTANGLE, name);
1050    // At the moment, CGLTexImageIOSurface2D requires the GL_TEXTURE_RECTANGLE target
1051	CGLTexImageIOSurface2D(cgl_ctx, GL_TEXTURE_RECTANGLE, GL_RGBA, (GLsizei)self.bounds.size.width, (GLsizei)self.bounds.size.height, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV,
1052					ioSurfaceBuffer, 0);
1053
1054	glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
1055	glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
1056	glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
1057	glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
1058
1059	return name;
1060}
1061
1062- (BOOL)isOpaque
1063{
1064	return YES;
1065}
1066
1067// Render a quad with the the IOSurface backed texture
1068- (void)renderTextureFromIOSurfaceWithWidth:(GLsizei)logoWidth height:(GLsizei)logoHeight
1069{
1070    GLfloat quad[] = {
1071        //x, y            s, t
1072        (GLfloat)logoWidth, 0.0f,    0.0f, 0.0f,
1073        0.0f, (GLfloat)logoHeight,   0.0f, 0.0f,
1074        0.0f,  0.0f,     1.0f, 0.0f,
1075        0.0f,  0.0f,     0.0f, 1.0f
1076    };
1077
1078    GLint		saveMatrixMode;
1079
1080    glGetIntegerv(GL_MATRIX_MODE, &saveMatrixMode);
1081    glMatrixMode(GL_TEXTURE);
1082    glPushMatrix();
1083    glLoadMatrixf(quad);
1084    glMatrixMode(saveMatrixMode);
1085
1086    glBindTexture(GL_TEXTURE_RECTANGLE, [mySharedGraphicsController currentTextureName]);
1087    glEnable(GL_TEXTURE_RECTANGLE);
1088
1089    glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE);
1090
1091	//Draw textured quad
1092	glBegin(GL_QUADS);
1093		glTexCoord2f(0.0, 0.0);
1094		glVertex3f(-1.0, -1.0, 0.0);
1095		glTexCoord2f(1.0, 0.0);
1096		glVertex3f(1.0, -1.0, 0.0);
1097		glTexCoord2f(1.0, 1.0);
1098		glVertex3f(1.0, 1.0, 0.0);
1099		glTexCoord2f(0.0, 1.0);
1100		glVertex3f(-1.0, 1.0, 0.0);
1101	glEnd();
1102
1103		glDisable(GL_TEXTURE_RECTANGLE);
1104
1105		glGetIntegerv(GL_MATRIX_MODE, &saveMatrixMode);
1106		glMatrixMode(GL_TEXTURE);
1107		glPopMatrix();
1108		glMatrixMode(saveMatrixMode);
1109
1110}
1111
1112- (void)drawRect:(NSRect)theRect
1113{
1114    glViewport(0, 0, (GLint)theRect.size.width, (GLint)theRect.size.height);
1115
1116    glClearColor(0.0, 0.0, 0.0, 0.0);
1117
1118    glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);
1119
1120    // drawRect is apparently called before we have real data to display,
1121    // causing the screen to flash unless we prevent any actual drawing.
1122    if (!okToDraw) {
1123        [[self openGLContext] flushBuffer];
1124    return;
1125}
1126
1127    // MachO client draws with current IO surface contents as texture
1128    [self renderTextureFromIOSurfaceWithWidth:(GLsizei)self.bounds.size.width height:(GLsizei)self.bounds.size.height];
1129
1130    [[self openGLContext] flushBuffer];
1131}
1132
1133@end
1134
1135
1136// On OS 10.13 or later, use MachO comunication and IOSurfaceBuffer to
1137// display the graphics output of our child graphics apps in our window.
1138static bool UseSharedOffscreenBuffer() {
1139    static bool alreadyTested = false;
1140    static bool needSharedGfxBuffer = false;
1141
1142//return true;    // FOR TESTING ONLY
1143    if (alreadyTested) {
1144        return needSharedGfxBuffer;
1145    }
1146    alreadyTested = true;
1147    if (compareOSVersionTo(10, 13) >= 0) {
1148        needSharedGfxBuffer = true;
1149        return true;
1150    }
1151    return false;
1152}
1153
1154
1155