Skip to content

Instantly share code, notes, and snippets.

@lucamignatti
Last active January 10, 2026 01:39
Show Gist options
  • Select an option

  • Save lucamignatti/5312f5e937de2ba44256ecba6de54cc2 to your computer and use it in GitHub Desktop.

Select an option

Save lucamignatti/5312f5e937de2ba44256ecba6de54cc2 to your computer and use it in GitHub Desktop.
Running Minecraft on macOS with Zink + KosmicKrisp

Running Minecraft on macOS with Zink + KosmicKrisp

Screenshot 2025-12-31 at 11 15 00 AM

TL;DR: I got OpenGL 4.6 apps (Minecraft) running on macOS by translating OpenGL → Vulkan → Metal using Mesa's Zink driver and the KosmicKrisp Vulkan implementation.

Minecraft (OpenGL 4.6) → Zink → Vulkan → KosmicKrisp → Metal → GPU

Build Instructions

Prerequisites

# Core build tools
brew install meson ninja python3

# Mesa dependencies
brew install bison flex llvm glslang spirv-tools molten-vk

# Add LLVM and bison to PATH (Apple's versions are outdated)
export PATH="/opt/homebrew/opt/llvm/bin:/opt/homebrew/opt/bison/bin:$PATH"

Mesa

git clone https://github.com/lucamignatti/mesa.git
cd mesa

# Create native file for Homebrew bison
cat > native.ini << 'EOF'
[binaries]
bison = '/opt/homebrew/opt/bison/bin/bison'
EOF

# Configure
meson setup build --native-file native.ini \
  -Dprefix=$HOME/mesa-native \
  -Dbuildtype=release \
  -Dplatforms=macos \
  -Degl-native-platform=surfaceless \
  -Degl=enabled \
  -Dgallium-drivers=zink \
  -Dvulkan-drivers=kosmickrisp \
  -Dgles1=enabled \
  -Dgles2=enabled \
  -Dglx=disabled \
  -Dgbm=disabled \
  -Dmoltenvk-dir=/opt/homebrew/opt/molten-vk

# Build and install
ninja -C build
ninja -C build install

# Configure DRI
mkdir -p $HOME/mesa-native/lib/dri
cd $HOME/mesa-native/lib/dri  
ln -sf ../libgallium-26.0.0-devel.dylib zink_dri.so
ln -sf ../libgallium-26.0.0-devel.dylib swrast_dri.so

# Copy vulkan loader
cp /opt/homebrew/lib/libvulkan.1.dylib $HOME/mesa-native/lib/

GLFW

git clone https://github.com/lucamignatti/glfw.git
cd glfw
mkdir build && cd build
cmake .. -DGLFW_BUILD_EXAMPLES=OFF -DGLFW_BUILD_TESTS=OFF -DBUILD_SHARED_LIBS=ON
make -j8

The library is at build/src/libglfw.3.dylib.

Running Minecraft

Environment Variables (set in your launcher):

DYLD_INSERT_LIBRARIES=$HOME/mesa-native/lib/libgl_interpose.dylib
DYLD_LIBRARY_PATH=$HOME/mesa-native/lib
LIBGL_DRIVERS_PATH=$HOME/mesa-native/lib/dri
VK_DRIVER_FILES=$HOME/mesa-native/share/vulkan/icd.d/kosmickrisp_mesa_icd.aarch64.json
EGL_PLATFORM=surfaceless
MESA_LOADER_DRIVER_OVERRIDE=zink
MESA_GL_VERSION_OVERRIDE=4.6
MESA_GLSL_VERSION_OVERRIDE=460

JVM Arguments:

-Dorg.lwjgl.egl.libname=$HOME/mesa-native/lib/libEGL.dylib
-Dorg.lwjgl.opengl.libname=$HOME/mesa-native/lib/libGL.dylib
-Dorg.lwjgl.glfw.libname=/path/to/glfw/build/src/libglfw.3.dylib

Known Issues

  • The game initially only renders in the bottom left quarter of the window. Resize the window to fix.

Getting it working

Apple deprecated OpenGL in 2018 and capped macOS at OpenGL 4.1. But shader packs, mods, and modern Minecraft features increasingly expect OpenGL 4.6. Apple's answer is Metal, but that doesnt help us.

Recently, the KosmicKrisp driver was created as a part of mesa. This is the first compliant vulkan driver on macOS, and according to various MRs it is capable of running zink. So, we translate OpenGL to Vulkan using Mesa's Zink driver, then translate Vulkan to Metal using KosmicKrisp. Sounds simple right? It wasn't.

Making Zink support KosmicKrisp

Zink requires many different vulkan features to be supported to impliment correct and performant openGL, and until recently, kosmickrisp didnt support all of them. currently, it's lacking one non-critical feature: NullDescriptor. Fortunately a current MR enables zink to work around this. So, we apply this MR and it's off to the races. Except that it's not.

First Problem: Where Do We Even Render?

Mesa's EGL implementation needs a "platform" to talk to the display server. On Linux, this is X11, Wayland, or DRM/KMS. macOS has none of these. I tried creating a platform_macos.c from scratch. Dead end. it would require weeks of work to implement the full EGL platform interface. Then I noticed something: the surfaceless platform. It's designed for headless rendering such as GPU compute, offscreen rendering, or CI testing. It explicitly has no window support. But it does talk to Vulkan via Kopper, Zink's WSI layer. What if we just... added window support to surfaceless? On macOS only?

This is cursed. A platform called "surfaceless" that creates window surfaces. But it works. I added a macOS-specific code path in platform_surfaceless.c that activates when Kopper detects Metal support. The "Metal display vtbl" includes create_window_surface, swap_buffers, and swap_interval. Functions that surfaceless normally doesn't have. We also force the software driver probe path, because macOS has no DRM devices to enumerate.

Second Problem: GLFW Uses the Wrong OpenGL

GLFW on macOS defaults to NSGL, which is Apple's native OpenGL implementation. That's the capped 4.1 version we're trying to escape. I patched cocoa_window.m to route all context requests to EGL instead. This way Mesa handles the context, not Apple's deprecated driver.

Third Problem: LWJGL Looks for GL in the Wrong Place

GLFW was now using EGL, but Minecraft still crashed. LWJGL (Minecraft's native library layer) loads OpenGL function pointers by calling dlsym on libGL.dylib. But our OpenGL implementation lives in EGL. there is no system libGL.dylib that knows about our context.

I created a small interposition library (libgl_interpose.dylib) that uses macOS's DYLD_INTERPOSE mechanism to intercept all dlsym calls. When code asks for a function starting with gl, we redirect to eglGetProcAddress instead. Then, we inject this via DYLD_INSERT_LIBRARIES and a launch argument. Now when LWJGL asks for glGenBuffers, it gets Mesa's implementation instead of nothing.

Fourth Problem: Wrong Layer Type

First launch. Immediate crash: vkCreateMetalSurfaceEXT failed. Debugging revealed the issue: GLFW passes us an NSView's backing layer. By default that's NSViewBackingLayer, which is a generic 2D compositing layer. Metal needs a CAMetalLayer.

We can't ask GLFW to change this without major patches. So we swap the layer ourselves. When our surface creation callback is invoked, we check the layer class using Objective-C runtime introspection. If it's NSViewBackingLayer, we create a fresh CAMetalLayer, configure it (frame, scale factor, opaque), and attach it to the view. All of this must happen on the main thread, CoreAnimation's requirement. We use dispatch_sync_f() to safely dispatch from render threads.

Fifth Problem: 1×1 Swapchain

Minecraft launched! Into a 1×1 pixel window.

The Kopper interface has a GetDrawableInfo() callback to query window dimensions. Our implementation queried [layer drawableSize]. But CAMetalLayer.drawableSize isn't populated until the next CoreAnimation layout pass, which hasn't happened yet because we just created the layer. So, we explicitly set drawableSize when creating the layer, matching the view bounds times the scale factor. Now the first query returns sensible values.

Sixth Problem: Random SIGBUS Crashes

Minecraft would sometimes crash with SIGBUS in CoreAnimation internals when querying drawable size. Completely non-deterministic.

After adding backtraces, I identified the pattern: crashes happened when GetDrawableInfo() was called from a render thread while CoreAnimation was doing layout on the main thread. CoreAnimation is not thread-safe. We wrapped all size queries in dispatch_sync_f() to the main thread.

Seventh Problem: Next-Launch Crashes

The first run works! the second run has crash in Metal shader compilation with invalid pointers though.

I discovered that KosmicKrisp caches compiled shaders to disk. The cache included Metal Pipeline State Object pointers, but those are process-specific. On the next launch, they're garbage pointing to deallocated GPU resources. So, I disable shader disk caching in KosmicKrisp. Performance loss is negligible for Minecraft anyway.

Eighth Problem: The Gray Screen

Minecraft loaded. The main menu background though, was gray. The world was also gray. Only UI elements where visible. oddly though, the 3d world was visible from behind f3 text and the chat box.

I added logging throughout the blit path. The RGB channels had correct values, but the gray areas all had alpha = 0. Minecraft renders the 3D world with alpha=0, it doesn't need transparency for terrain. On typical OpenGL, this is fine. But Metal's compositor respects alpha. Alpha=0 means "fully transparent". To fix this, I force alpha = 1.0 in the blit shaders.

Ninth Problem: Black Flashing

Main menu rendered correctly! Loading a world... black frames. Every few frames where pure black.

Zink has two blit paths:

  1. vk_meta_blit (native): Uses Vulkan blit commands. Fast.
  2. util_blitter (Gallium fallback): Software-assisted, and more compatible.

For format conversions (RGBA↔BGRA when blitting to the swapchain), Zink was using vk_meta_blit. On KosmicKrisp, this exhibited intermittent black frames. The root cause is still unknown. possibly a synchronization issue in KosmicKrisp's blit. Regardless, I force format-conversion blits to use util_blitter instead. The performance penalty is about ~2-3%, which is acceptable for stable frames. Both paths now force alpha=1.

Tenth Problem: Dead-end Window Resizing

The game finally launched, looked right, and ran smoothly. But there was one final annoyance. Resizing the window didnt resize the view, and broke everything. The internal rendering resolution remained stuck at whatever size the window was when the game first appeared, leading to either black borders or extreme stretching.

Zink's Kopper layer has an update mechanism that is supposed to handle window resizing. However, this path was hardcoded to only query surface capabilities for X11 platform surfaces. For every other surface type, including our new Metal surfaces, it just returned the current resource dimensions, effectively telling the driver that the size had never changed.

I extended zink_kopper_update and the DRI frontend to handle KOPPER_METAL surfaces. Now, every frame, Zink queries the current actual size of the CAMetalLayer via Vulkan surface capabilities. If there's a mismatch, it triggers a swapchain recreation. After this change Minecraft responds correctly to window resizes, maintaining the correct aspect ratio and resolution as you resize.

It Works

70fps, shader packs load, modern mods work, and f3 reports OpenGL 4.6 on macOS, running entirely through Mesa, Vulkan, and Metal.

The surfaceless platform has surfaces, the blit is emulated and lies, we swap layer types at runtime, and theres no shader cache. It's held together with the most hackiest of hacks.

But it works. mostly. There are still bugs in rendering deep in kosmickrisp somewhere (youll notice the hotbar, for example, looks strange). not all shaders work as kosmickrisp doesnt expose all needed features for zink to run them, and voxy doesnt have the indirect GL features it needs as kosmickrisp doesnt support them yet, and performance is lacking. But maybe one day not to far away, everything will work. This at least proves that it can. On another note, it's not impossible this is the first time ever openGL 4.6 has run on macOS, which is cool.

@Thinkseal
Copy link

from my testing it seems to break them for a bit then stop and idk why

@sassa7777
Copy link

dyld[87564]: terminating because inserted dylib '/Users/xrt3zy/mesa-native/lib/libgl_interpose.dylib' could not be loaded: tried: '/Users/xrt3zy/mesa-native/lib/libgl_interpose.dylib' (mach-o file, but is an incompatible architecture (have 'arm64', need 'arm64e')), '/Users/xrt3zy/mesa-native/lib/libgl_interpose.dylib' (mach-o file, but is an incompatible architecture (have 'arm64', need 'arm64e')), '/System/Volumes/Preboot/Cryptexes/OS/Users/xrt3zy/mesa-native/lib/libgl_interpose.dylib' (no such file), '/Users/xrt3zy/mesa-native/lib/libgl_interpose.dylib' (mach-o file, but is an incompatible architecture (have 'arm64', need 'arm64e')) dyld[87564]: tried: '/Users/xrt3zy/mesa-native/lib/libgl_interpose.dylib' (mach-o file, but is an incompatible architecture (have 'arm64', need 'arm64e')), '/Users/xrt3zy/mesa-native/lib/libgl_interpose.dylib' (mach-o file, but is an incompatible architecture (have 'arm64', need 'arm64e')), '/System/Volumes/Preboot/Cryptexes/OS/Users/xrt3zy/mesa-native/lib/libgl_interpose.dylib' (no such file), '/Users/xrt3zy/mesa-native/lib/libgl_interpose.dylib' (mach-o file, but is an incompatible architecture (have 'arm64', need 'arm64e'))

Any ideas? Did I build something incorrectly?

Do you have SIP disabled? If so, try enabling it.

@Thinkseal
Copy link

you need to compile the libraries as arm64 not arm64e

@SeseMueller
Copy link

Great Work! I did try to get it running in GTNH since the development team was crazy enough to get it to use Java 17-21 despite being on Mineraft 1.7.10.
Sadly, the workaround they use conflicts with this setup in some way; I get the error java.lang.IllegalStateException: There is no OpenGL context current in the current thread. no matter what I try. And the Java 8 instance just doesn't use it. I will keep experimenting, but I doubt I'll be successfull.

@LostShadowGD
Copy link

Run-time dependency libudev found: NO (tried pkgconfig, framework and cmake)
llvm-config found: NO need ['>= 15.0.0']
Run-time dependency LLVM found: NO (tried config-tool)
Looking for a fallback subproject for the dependency llvm (modules: bitwriter, engine, mcdisassembler, mcjit, core, executionengine, scalaropts, transformutils, instcombine, coverage, target, linker, irreader, option, libdriver, lto, native)
Building fallback subproject with default_library=shared
ERROR: Subproject llvm is buildable: NO

meson.build:1825:11: ERROR: Neither a subproject directory nor a llvm.wrap file was found.

A full log can be found at /Users/******/mesa/build/meson-logs/meson-log.txt

@LostShadowGD
Copy link

i won't post the file, as it's 3737 lines long and GitHub doesn't support .txt attachments.

@lucamignatti
Copy link
Author

@LostShadowGD you need to install llvm with brew, then run:
export PATH="/opt/homebrew/opt/llvm/bin:/opt/homebrew/opt/bison/bin:$PATH"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment