Making OpenGL and JVM friendly on macOS

Historically, macOS has been very different from other OSes when it comes to native windows and graphics. And we can’t say that this is definitely bad or good. In this regard, Apple decided to follow their favorite path: “we know better what you need, so we did everything for you.” How does this manifest itself?

Windows and threads

Although the title of the article says about OpenGL and JVM, it is important to explain what all the fuss is about.

The main problem is how the windowing system works in macOS. If you look at Windows and Linux, you can follow the following logic: we create a window, and in an “endless” loop we wait for messages with various events coming to the current thread. This gives us flexibility in managing these very events, as well as control over which thread our code is executed in.

In macOS, we take everything to a higher level. We don't listen to events in a loop, we simply create an object and add listeners for specific events to it. If this does not cause us any discomfort, then here is the real problem – the window must be created in the main thread of our process. If you try to go against the rules, the application will simply crash.

It turns out that macOS, when working with windows, can only operate with the main thread, and somewhere under the hood it receives events specifically into it. What's the problem?

JVM

If you have ever displayed a list of all current threads of a process, or read a crash log, you could see that in addition to the main thread, there are several others. For example, multiple threads can be used for garbage collection.

And okay, if there were just a few threads hanging in the background, it wouldn’t complicate our life. The problem starts to appear when we try to create a native window on macOS in the main method. It turns out that we are not actually in the main thread. It's called main, but it's not. But in order for the JVM to launch our program in the main thread, we need to add a parameter -XstartOnFirstThread. Perhaps someone came across this parameter when trying to run Minecraft, or any OpenGL game.

OpenGL

The most popular library for working with OpenGL in Java is LWJGL. And if you go to their official website, then in the example tab you can see a note for macOS with exactly the parameter that I described above. This is not surprising, since this example uses GLFW, which itself is a bit stupid and clumsy. Let's see if we really need this parameter?

OpenGL without window

OpenGL itself is not required by the specification to have an attached window. Roughly speaking, this is just an API through which we can interact with the video card. We set up the rendering pipeline, bind framebuffers, and draw the vertices. The window is just a way to display one of the resulting framebuffers.

If we again look towards Windows and Linux, we will see that we cannot do without windows – on Windows they are needed to create a full-fledged context, and on Linux we cannot even take a step without it.

But what about on macOS? GLFW offers us to create an invisible window… Okay, but we have already found out that we cannot use this option when we are not in the main thread. Here we can praise Apple – although OpenGL in their system lags behind, and is even marked as deprecated, it can still sometimes show itself better than its competitors. It turns out that we have a unique opportunity to create a context without a window. There is a whole layer of functions for this, called CGL (CoreGL).

Here is an example of its creation in C++:

GLint num;
CGLPixelFormatObj format;
CGLPixelFormatAttribute attributes[4] = {
        kCGLPFAAccelerated,
        kCGLPFAOpenGLProfile,
        kCGLOGLPVersion_GL3_Core,
        (CGLPixelFormatAttribute) 0
};
CGLChoosePixelFormat(attributes, &format, &num);

CGLContextObj context;
CGLCreateContext(format, nullptr, &context);

Conventional NS windows under the hood use this exact mechanism. What's great is that we are not tied to the main thread, and can create a context whenever it is convenient for us.

OpenGL with window

But here's the problem – we need a window. If you go to Google with such a question, you won’t find out much. Still, you have to stop at the parameter -XstartOnFirstThread?

But how, for example, do AWT and JavaFX act when they create their windows? After all, when using them, this parameter is not needed. Here we need the “Apple magic”. It turns out there is a way to run code on the main thread from anywhere in the program. And this is done using the function performSelectorOnMainThread. In the following example, I'll show how you can use it in a JNI-bound utility class to run from the JVM.

This is what our utility class will look like in Kotlin:

package com.huskerdev.test

class MacOSUtils {
    companion object {
        @JvmStatic external fun invokeOnMainThread(runnable: Runnable)
    }
}

And this is the JNI code in Objective-C in one file:

#import <Cocoa/Cocoa.h>

/* ======================
       ThreadUtilities
   ====================== */

@interface ThreadUtilities : NSObject { }
+ (void)performOnMainThread:(BOOL)wait block:(void (^)())block;
@end

@implementation ThreadUtilities
static NSArray<NSString*> *javaModes = [[NSArray alloc] initWithObjects:
        NSDefaultRunLoopMode, NSModalPanelRunLoopMode, NSEventTrackingRunLoopMode, @"grapl", nil];

+ (void)invokeBlock:(void (^)())block {
    block();
}

+ (void)invokeBlockCopy:(void (^)(void))blockCopy {
    blockCopy();
    Block_release(blockCopy);
}

+ (void)performOnMainThread:(BOOL)wait block:(void (^)())block {
    if (![NSThread isMainThread]){
        [self
            performSelectorOnMainThread:    wait == YES ? @selector(invokeBlock:) : @selector(invokeBlockCopy:)
            withObject:                     wait == YES ? block : Block_copy(block)
            waitUntilDone:                  wait
            modes:                          javaModes
        ];
    } else
        block();
}
@end

/* ======================
            JNI
   ====================== */

extern "C" JNIEXPORT void 
JNICALL Java_com_huskerdev_test_MacOSUtils_invokeOnMainThread(JNIEnv* env, jobject, jobject runnable) {
    JavaVM* jvm;
    env->GetJavaVM(&jvm);

    jobject runnableGlobal = env->NewGlobalRef(runnable);
    jclass runnableClass = env->GetObjectClass(runnableGlobal);
    jmethodID runMethod = env->GetMethodID(runnableClass, "run", "()V");

    [ThreadUtilities performOnMainThread:YES block:^() {
        JNIEnv* env;
        jvm->AttachCurrentThread((void**)&env, NULL);
        env->CallVoidMethod(runnableGlobal, runMethod);
        env->DeleteGlobalRef(runnableGlobal);
        jvm->DetachCurrentThread();
    }];
}

With this scary code, we will be able to request that actions be performed on the main thread by calling MacOSUtils.invokeOnMainThread { ... }. This code also has a sync/async switch, but I didn't make it a separate parameter to make things a little simpler.

I tested the code from the example from the official LWJGL website with a call to our function – it worked perfectly without any JVM parameters.

Bottom line

OpenGL on macOS does have some limitations related to threading, but all of them can be bypassed if you really want to.

I will be glad if anyone points out the shortcomings, or shares their experience with LWJGL and GLFW under macOS.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *