api/macglutfix.m
// Berkeley Open Infrastructure for Network Computing
// http://boinc.berkeley.edu
// Copyright (C) 2024 University of California
//
// This is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation;
// either version 2.1 of the License, or (at your option) any later version.
//
// This software is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU Lesser General Public License for more details.
//
// To view the GNU Lesser General Public License visit
// http://www.gnu.org/copyleft/lesser.html
// or write to the Free Software Foundation, Inc.,
// 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA

//
//  macglutfix.m
//

#define CREATE_LOG 0    // Set to 1 for debugging

#define GL_DO_NOT_WARN_IF_MULTI_GL_VERSION_HEADERS_INCLUDED

#include <Cocoa/Cocoa.h>
#include <mach/mach_time.h>
#include <pthread.h>
#import <OpenGL/CGLIOSurface.h>
#import <GLKit/GLKit.h>
#include <servers/bootstrap.h>
#import "MultiGPUMig.h"
#import "MultiGPUMigServer.h"
#include "x_opengl.h"
#include "boinc_gl.h"
#include "boinc_glut.h"

extern bool fullscreen; // set in graphics2_unix.cpp

// For unknown reason, "boinc_api.h" gets a compile
// error here so just declare boinc_is_standalone()
//#include "boinc_api.h"
extern int boinc_is_standalone(void);

// int set_realtime(int period, int computation, int constraint);
void MacGLUTFix(bool isScreenSaver);
void BringAppToFront(void);
int compareOSVersionTo(int toMajor, int toMinor);

// The standard ScreenSaverView class actually sets the window
// level to 2002, not the 1000 defined by NSScreenSaverWindowLevel
// and kCGScreenSaverWindowLevel
#define RealSaverLevel 2002
// Glut sets the window level to 100 when it sets full screen mode
#define GlutFullScreenWindowLevel 100

// Delay when switching to screensaver mode to reduce annoying flashes
#define SAVERDELAY 30

// NSClosableWindowMask is deprecated in OS 10.12 and is replaced by
// NSWindowStyleMaskClosable, which is not defined before OS 10.12
#ifndef NSWindowStyleMaskClosable
#define NSWindowStyleMaskClosable NSClosableWindowMask
#endif

static NSWindow* myWindow = nil;

void MacGLUTFix(bool isScreenSaver) {
    static int count = 0;
    static NSMenu * emptyMenu;
    NSOpenGLContext * myContext = nil;
    NSView *myView = nil;
    static int requestedWidth, requestedHeight;

    if (count == 2) ClearDocumentEditedDot();
    if (count++ > 2) return;   // Do the code below only twice

    if (! boinc_is_standalone()) {
        if (emptyMenu == nil) {
            emptyMenu = [ NSMenu alloc ];
            [ NSApp setMainMenu:emptyMenu ];
        }
    }

    myContext = [ NSOpenGLContext currentContext ];
    if (myContext)
        myView = [ myContext view ];
    if (myView)
        myWindow = [ myView window ];
    if (myWindow == nil)
        return;

    // Retina displays have 2X2 pixels per point. When OpenGL / GLUT apps built
    // using Xcode 11 are run on a Retina display under OS 10.15, they fail to
    // adjust their pixel dimesions to double the window size, and so fill only
    // 1/4 of the window (display at half width and height) until they are
    // resized by calls to glutReshapeWindow(), glutFullScreen(). etc.
    // However, they work correctly when run on earlier  versions of OS X.
    //
    // OpenGL / GLUT apps built using earlier versions of Xcode do not have this
    // problem on OS 10.15, but glutReshapeWindow().
    //
    // We work around this by calling glutReshapeWindow() twice, restoring the
    // window's original size. This transparently fixes the problem when necessary
    // without actually changing the window's size, and does no harm when not
    // necessary.

    if (!isScreenSaver) {
        if (count == 1) {
            requestedWidth = glutGet(GLUT_WINDOW_WIDTH);
            requestedHeight = glutGet(GLUT_WINDOW_HEIGHT);
            glutReshapeWindow(requestedWidth+1, requestedHeight);
        } else {
            glutReshapeWindow(requestedWidth, requestedHeight);
        }

        [myWindow setStyleMask:[myWindow styleMask] | NSWindowStyleMaskClosable];

        return;
    }

    // As of OS 10.13, app windows can no longer appear on top of screensaver
    // window, but we still use this method on older versions of OS X for
    // compatibility with older project graphics apps.
    if (!UseSharedOffscreenBuffer()) {
        // In screensaver mode, set our window's level just above
        // our BOINC screensaver's window level so it can appear
        // over it.  This doesn't interfere with the screensaver
        // password dialog because the dialog appears only after
        // our screensaver is closed.
        if ([ myWindow level ] == GlutFullScreenWindowLevel) {
            [ myWindow setLevel:RealSaverLevel+20 ];
        }
    }
}

void ClearDocumentEditedDot(void) {
    if (myWindow) {
        [myWindow setDocumentEdited: NO];
    }
}

#if 0
// NOT USED: See comments in animateOneFrame in Mac_Saver_ModuleView.m
// <https://developer.apple.com/library/content/technotes/tn2169>
int set_realtime(int period, int computation, int constraint) {
    mach_timebase_info_data_t timebase_info;
    mach_timebase_info(&timebase_info);

    const uint64_t NANOS_PER_MSEC = 1000000ULL;
    double clock2abs = ((double)timebase_info.denom / (double)timebase_info.numer) * NANOS_PER_MSEC;

    thread_time_constraint_policy_data_t policy;
    policy.period      = period;
    policy.computation = (uint32_t)(computation * clock2abs); // computation ms of work
    policy.constraint  = (uint32_t)(constraint * clock2abs);
//    policy.preemptible = FALSE;
    policy.preemptible = TRUE;

    int kr = thread_policy_set(pthread_mach_thread_np(pthread_self()),
                   THREAD_TIME_CONSTRAINT_POLICY,
                   (thread_policy_t)&policy,
                   THREAD_TIME_CONSTRAINT_POLICY_COUNT);
    if (kr != KERN_SUCCESS) {
        fprintf(stderr, "set_realtime() failed.\n");
        return 0;
    }
    return 1;
}
#endif

void BringAppToFront() {
    [ NSApp activateIgnoringOtherApps:YES ];
}

void HideThisApp() {
    [ NSApp hide:NSApp ];
}

// On OS 10.13 or later, use MachO comunication and IOSurfaceBuffer to
// display the graphics output of our child graphics apps in our window.

// Code adapted from Apple Developer Tech Support Sample Code MutiGPUIOSurface:
// <https://developer.apple.com/library/content/samplecode/MultiGPUIOSurface>

#define NUM_IOSURFACE_BUFFERS 2

@interface ServerController : NSObject <NSMachPortDelegate>
{
    NSMachPort *serverPort;

	NSMachPort *clientPort[16];
	uint32_t clientPortNames[16];
	uint32_t clientPortCount;

    bool mach_bootstrap_unavailable_to_screensavers;
}
- (ServerController *)init;
- (kern_return_t)checkInClient:(mach_port_t)client_port index:(int32_t *)client_index;
- (void)portDied:(NSNotification *)notification;
- (void)sendIOSurfaceMachPortToClients: (uint32_t)index withMachPort:(mach_port_t) iosurface_port;

@end

static ServerController *myserverController;

static uint32_t currentFrameIndex;

static IOSurfaceRef ioSurfaceBuffers[NUM_IOSURFACE_BUFFERS];
static mach_port_t ioSurfaceMachPorts[NUM_IOSURFACE_BUFFERS];
static GLuint textureNames[NUM_IOSURFACE_BUFFERS];
static GLuint fboNames[NUM_IOSURFACE_BUFFERS];
static GLuint depthBufferName;

@implementation ServerController

- (ServerController *)init
{
    mach_port_t servicePortNum = MACH_PORT_NULL;
    kern_return_t machErr;
    char *portNameV1 = "edu.berkeley.boincsaver";
    char *portNameV2 = "edu.berkeley.boincsaver-v2";

    mach_bootstrap_unavailable_to_screensavers = false;

// NSMachBootstrapServer is deprecated in OS 10.13, so use bootstrap_look_up
//	serverPort = [(NSMachPort *)([[NSMachBootstrapServer sharedInstance] portForName:@"edu.berkeley.boincsaver"]) retain];
	machErr = bootstrap_look_up(bootstrap_port, portNameV2, &servicePortNum);
    if (machErr == KERN_SUCCESS) {
        // As of MacOS 14.0, the legacyScreenSave sandbox prevents using
        // bootstrap_look_up. I have filed bug report FB13300491 with
        // Apple and hope they will change this in a future MacOS.
        mach_bootstrap_unavailable_to_screensavers = true;

        int32_t dummy_index;
        [self checkInClient:servicePortNum index:&dummy_index];
    } else {
// NSMachBootstrapServer is deprecated in OS 10.13, so use bootstrap_check_in
//	    serverPort = [(NSMachPort *)([[NSMachBootstrapServer sharedInstance] servicePortWithName:@"edu.berkeley.boincsaver"]) retain];
        machErr = bootstrap_check_in(bootstrap_port, portNameV1, &servicePortNum);
        if (machErr != KERN_SUCCESS) {  // maybe BOOTSTRAP_UNKNOWN_SERVICE
            [NSApp terminate:self];
        }
    }

    serverPort = (NSMachPort*)[NSMachPort portWithMachPort:servicePortNum];
	[[NSNotificationCenter defaultCenter] addObserver:self
	    selector:@selector(portDied:) name:NSPortDidBecomeInvalidNotification object:nil];

    if (!mach_bootstrap_unavailable_to_screensavers) {
        // Register server port with the current runloop.
        [serverPort setDelegate:self];  // CAF STD
        [serverPort scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; // CAF STD
    }

    // NOT USED: See comments in animateOneFrame in Mac_Saver_ModuleView.m
#if 0
    // This is an alternate method to get enough CPU cycles when we
    // are running in the background behind ScreensaverEngine.app
    set_realtime(0, 5, 33);
    //set_realtime(0, 5, 10);
    //set_realtime(33, 5, 33);
    //set_realtime(30, 3, 6);
    //set_realtime(30, 10, 20);
#endif

    return self;
}

- (void)portDied:(NSNotification *)notification
{
	NSPort *port = [notification object];
	if(port == serverPort)
	{
		[NSApp terminate:self];
	}
	else
	{
		int i;
		for(i = 0; i < clientPortCount+1; i++)
		{
			if([clientPort[i] isEqual:port])
			{
				[clientPort[i] release];
				clientPort[i] = nil;
				clientPortNames[i] = 0;
			}
		}
	}
}

- (void)handleMachMessage:(void *)msg
{
	union __ReplyUnion___MGCMGSServer_subsystem reply;

	mach_msg_header_t *reply_header = (void *)&reply;
	kern_return_t kr;

	if(MGSServer_server(msg, reply_header) && reply_header->msgh_remote_port != MACH_PORT_NULL)
	{
		kr = mach_msg(reply_header, MACH_SEND_MSG, reply_header->msgh_size, 0, MACH_PORT_NULL,
			     0, MACH_PORT_NULL);
        if(kr != 0)
			[NSApp terminate:nil];
	}
}

- (kern_return_t)checkInClient:(mach_port_t)client_port index:(int32_t *)client_index
{
	clientPortCount++;			// clients always start at index 1
	clientPortNames[clientPortCount] = client_port;
	clientPort[clientPortCount] = [[NSMachPort alloc] initWithMachPort:client_port];

	*client_index = clientPortCount;
	return 0;
}

kern_return_t _MGSCheckinClient(mach_port_t server_port, mach_port_t client_port,
			       int32_t *client_index)
{
	return [myserverController checkInClient:client_port index:client_index];
}

// For the MachO server, this is a no-op
kern_return_t _MGSDisplayFrame(mach_port_t server_port, int32_t frame_index, uint32_t iosurface_port)
{
	return 0;
}

- (void)sendIOSurfaceMachPortToClients:(uint32_t)index withMachPort:(mach_port_t)iosurface_port
{
	int i;
	for(i = 0; i < clientPortCount+1; i++)
	{
		if(clientPortNames[i])
		{
			_MGCDisplayFrame(clientPortNames[i], index, iosurface_port);
		}
	}
}
@end


// OpenGL / GLUT apps which call glutFullScreen() and are built using
// Xcode 11 apparently use window dimensions based on the number of
// backing store pixels. That is, they double the window dimensions
// for Retina displays (which have 2X2 pixels per point.) But OpenGL
// apps built under earlier versions of Xcode don't.
//
// OS 10.15 Catalina assumes OpenGL / GLUT apps work as built under
// Xcode 11, so it displays older builds at half width and height,
// unless we compensate in our code. To ensure that BOINC graphics apps
// built on all versions of Xcode work properly on all versions of OS X,
// we set the IOSurface dimensions in this module to the viewportRect
// dimensions.
//
// See also:
// [BOINC_Saver_ModuleView initWithFrame:] in clientscr/Mac_Saver_ModuleView.m
// clientscr/Mac_Saver_ModuleCiew.m and MacGLUTFix(bool isScreenSaver) above.
//
// NOTE: Graphics apps must now be linked with the IOSurface framework.
//
void MacPassOffscreenBufferToScreenSaver() {
    NSOpenGLContext * myContext = [ NSOpenGLContext currentContext ];
    int viewportRect[4];
    GLsizei w, h;
    GLuint name, namef;

    glGetIntegerv(GL_VIEWPORT, (GLint*)viewportRect);
    w = viewportRect[2];
    h = viewportRect[3];

    if (!myserverController) {
        myserverController = [[[ServerController alloc] init] retain];
    }

    if (!ioSurfaceBuffers[0]) {
        // Set up all of our iosurface buffers
        for(int i = 0; i < NUM_IOSURFACE_BUFFERS; i++) {
            ioSurfaceBuffers[i] = IOSurfaceCreate((CFDictionaryRef)@{
                (id)kIOSurfaceWidth: [NSNumber numberWithInt: w],
                (id)kIOSurfaceHeight: [NSNumber numberWithInt: h],
                (id)kIOSurfaceBytesPerElement: @4
                });
            ioSurfaceMachPorts[i] = IOSurfaceCreateMachPort(ioSurfaceBuffers[i]);
        }
    }

    if(!textureNames[currentFrameIndex])
    {
        CGLContextObj cgl_ctx = (CGLContextObj)[myContext CGLContextObj];

        glGenTextures(1, &name);

        glBindTexture(GL_TEXTURE_RECTANGLE, name);
        // At the moment, CGLTexImageIOSurface2D requires the GL_TEXTURE_RECTANGLE target
        CGLTexImageIOSurface2D(cgl_ctx, GL_TEXTURE_RECTANGLE, GL_RGBA, w, h, GL_BGRA, GL_UNSIGNED_INT_8_8_8_8_REV,
                        ioSurfaceBuffers[currentFrameIndex], 0);
        glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_RECTANGLE, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

        // Generate an FBO and bind the texture to it as a render target.
        glBindTexture(GL_TEXTURE_RECTANGLE, 0);

        glGenFramebuffers(1, &namef);
        glBindFramebuffer(GL_FRAMEBUFFER, namef);
        glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_RECTANGLE, name, 0);

        if(!depthBufferName)
        {
            glGenRenderbuffers(1, &depthBufferName);
            glRenderbufferStorage(GL_TEXTURE_RECTANGLE, GL_DEPTH, w, h);
        }
        glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_RECTANGLE, depthBufferName);

        fboNames[currentFrameIndex] = namef;
        textureNames[currentFrameIndex] = name;
    }

    glBindFramebuffer(GL_FRAMEBUFFER, 0);   // First, draw to default FBO (screen FBO)

    // To see the original rendering in the graphics app's full-screen window
    // for debugging, temporarily enable this "glutSwapBuffers" statement
//    glutSwapBuffers();  // FOR DEBUGGING ONLY

    // Copy the default FBO to the IOSurface texture's FBO
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fboNames[currentFrameIndex]);
    glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
    glBlitFramebuffer(0,0,w,h, 0,0,w,h, GL_COLOR_BUFFER_BIT, GL_NEAREST);

    // To see the contents of the IOSurface in the graphics app's full-screen window
    // for debugging, temporarily change "#if 0" to #if 1" bin the next line:
 #if 0  // FOR DEBUGGING ONLY
    glBindFramebuffer(GL_READ_FRAMEBUFFER, fboNames[currentFrameIndex]);
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
    glBlitFramebuffer(0,0,w,h, 0,0,w,h, GL_COLOR_BUFFER_BIT, GL_NEAREST);
#endif

   glutSwapBuffers();
    [myserverController sendIOSurfaceMachPortToClients: currentFrameIndex
                        withMachPort:ioSurfaceMachPorts[currentFrameIndex]];
    glFlush();
	glBindFramebuffer(GL_FRAMEBUFFER, 0);

    currentFrameIndex = (currentFrameIndex + 1) % NUM_IOSURFACE_BUFFERS;
}

// Test OS version number on all versions of OS X without using deprecated Gestalt
// compareOSVersionTo(x, y) returns:
// -1 if the OS version we are running on is less than x.y
//  0 if the OS version we are running on is equal to x.y
// +1 if the OS version we are running on is lgreater than x.y
int compareOSVersionTo(int toMajor, int toMinor) {
    static SInt32 major = -1;
    static SInt32 minor = -1;

    if (major < 0) {
        char vers[100], *p1 = NULL;
        FILE *f;
        vers[0] = '\0';
        f = popen("sw_vers -productVersion", "r");
        if (f) {
            fscanf(f, "%s", vers);
            pclose(f);
        }
        if (vers[0] == '\0') {
            fprintf(stderr, "popen(\"sw_vers -productVersion\" failed\n");
            fflush(stderr);
            return 0;
        }
        // Extract the major system version number
        major = atoi(vers);
        // Extract the minor system version number
        p1 = strchr(vers, '.');
        minor = atoi(p1+1);
    }

    if (major < toMajor) return -1;
    if (major > toMajor) return 1;
    // if (major == toMajor) compare minor version numbers
    if (minor < toMinor) return -1;
    if (minor > toMinor) return 1;
    return 0;
}

// Code for debugging:

#if CREATE_LOG
void strip_cr(char *buf)
{
    char *theCR;

    theCR = strrchr(buf, '\n');
    if (theCR)
        *theCR = '\0';
    theCR = strrchr(buf, '\r');
    if (theCR)
        *theCR = '\0';
}
#endif

void print_to_log_file(const char *format, ...) {
#if CREATE_LOG
    va_list args;
    char buf[256];
    time_t t;
    FILE *f;
    if (fullscreen) {
        // We can't write to our home directory if running as user / group boinc_project
        f = fopen("/Users/Shared/test_log.txt", "a");
    } else {
        strlcpy(buf, getenv("HOME"), sizeof(buf));
        strlcat(buf, "/Documents/test_log.txt", sizeof(buf));
        f = fopen(buf, "a");
        // freopen(buf, "a", stdout);
        //freopen(buf, "a", stderr);
    }
    if (!f) return;
    time(&t);
    strcpy(buf, asctime(localtime(&t)));
    strip_cr(buf);

    fputs(buf, f);
    fputs("   ", f);

    va_start(args, format);
    vfprintf(f, format, args);
    va_end(args);

    fputs("\n", f);
    fflush(f);
    fclose(f);
#endif
}

