From 3b5cbe5d42c26915c6d34d1df6613db74dd8c48b Mon Sep 17 00:00:00 2001 From: Dennis de Vulder Date: Tue, 23 Jun 2026 23:24:13 +0200 Subject: [PATCH] preserve macos native bridge --- build.gradle | 39 ++ .../java/com/gpuvulkan/GpuVulkanPlugin.java | 10 - .../gpuvulkan/platform/MacOSMetalHelper.java | 187 ++++++- .../platform/MacOSPlatformSurface.java | 88 +++- .../gpuvulkan/platform/PlatformSurface.java | 7 +- src/main/native/rlmtl.m | 461 ++++++++++++++++++ .../resources/com/gpuvulkan/librlmtl.dylib | Bin 0 -> 139584 bytes 7 files changed, 768 insertions(+), 24 deletions(-) create mode 100644 src/main/native/rlmtl.m create mode 100755 src/main/resources/com/gpuvulkan/librlmtl.dylib diff --git a/build.gradle b/build.gradle index a6cb65f..74bb5db 100644 --- a/build.gradle +++ b/build.gradle @@ -94,6 +94,8 @@ dependencies { // and the committed binaries ship as-is. def gpuVulkanShaderSrc = file('src/main/shaders/gpuvulkan') def gpuVulkanResourceOut = file('src/main/resources/com/gpuvulkan') +def gpuVulkanNativeSrc = file('src/main/native/rlmtl.m') +def gpuVulkanNativeOut = new File(gpuVulkanResourceOut, 'librlmtl.dylib') def glslangValidator = providers.environmentVariable('GLSLANG').getOrElse('glslangValidator') def toolOnPath = { tool -> @@ -167,8 +169,45 @@ def checkGpuVulkanShadersFresh = tasks.register('checkGpuVulkanShadersFresh') { } } +def compileMacOSMetalHelper = tasks.register('compileMacOSMetalHelper') { + group = 'build' + description = 'Regenerate the committed macOS CAMetalLayer/JAWT helper (needs clang, macOS)' + + inputs.file(gpuVulkanNativeSrc) + outputs.file(gpuVulkanNativeOut) + onlyIf { + System.getProperty('os.name', '').toLowerCase().contains('mac') && toolOnPath('clang') + } + + doLast { + gpuVulkanResourceOut.mkdirs() + def javaHome = System.getenv('JAVA_HOME') ?: System.getProperty('java.home') + exec { + commandLine 'clang', + '-fno-objc-arc', + '-shared', + '-dynamiclib', + '-mmacosx-version-min=11.0', + '-arch', 'arm64', + '-arch', 'x86_64', + '-framework', 'Cocoa', + '-framework', 'QuartzCore', + '-framework', 'Metal', + '-I', "${javaHome}/include", + '-I', "${javaHome}/include/darwin", + '-Wl,-undefined,dynamic_lookup', + gpuVulkanNativeSrc.absolutePath, + '-o', gpuVulkanNativeOut.absolutePath + } + exec { + commandLine 'codesign', '--force', '--sign', '-', gpuVulkanNativeOut.absolutePath + } + } +} + processResources { dependsOn compileGpuVulkanShaders + dependsOn compileMacOSMetalHelper dependsOn checkGpuVulkanShadersFresh from(projectDir) { include 'runelite-plugin.properties' diff --git a/src/main/java/com/gpuvulkan/GpuVulkanPlugin.java b/src/main/java/com/gpuvulkan/GpuVulkanPlugin.java index 45b28c3..5c24c4e 100644 --- a/src/main/java/com/gpuvulkan/GpuVulkanPlugin.java +++ b/src/main/java/com/gpuvulkan/GpuVulkanPlugin.java @@ -169,10 +169,6 @@ public void hotkeyPressed() @Override protected void startUp() { - if (isMacOS()) - { - return; - } log.info("Starting GPU (Vulkan)"); shuttingDown = false; keyManager.registerKeyListener(inFlightClipHotkeyListener); @@ -220,12 +216,6 @@ protected void startUp() }); } - private static boolean isMacOS() - { - String os = System.getProperty("os.name", "").toLowerCase(); - return os.contains("mac") || os.contains("darwin"); - } - private static boolean isVulkanLoaderAvailable() { try diff --git a/src/main/java/com/gpuvulkan/platform/MacOSMetalHelper.java b/src/main/java/com/gpuvulkan/platform/MacOSMetalHelper.java index e991064..64d14f6 100644 --- a/src/main/java/com/gpuvulkan/platform/MacOSMetalHelper.java +++ b/src/main/java/com/gpuvulkan/platform/MacOSMetalHelper.java @@ -1,32 +1,203 @@ /* * Copyright (c) 2026, Dennis de Vulder * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.gpuvulkan; import java.awt.Canvas; +import java.awt.GraphicsConfiguration; +import java.awt.geom.AffineTransform; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import lombok.extern.slf4j.Slf4j; /** - * Plugin Hub build does not ship macOS native presentation support. + * macOS-only native bridge (rlmtl.m / librlmtl.dylib) that attaches a + * CAMetalLayer to the AWT Canvas via JAWT. Pure-Java JAWT bindings don't + * work on macOS. */ +@Slf4j final class MacOSMetalHelper { + private static volatile boolean loaded; + private static volatile double layerScale = 1.0; + private MacOSMetalHelper() {} - static void detachMetalLayer() {} + private static synchronized void ensureLoaded() + { + if (loaded) + { + return; + } + // Force-load libjawt first: the helper links JAWT symbols with + // dynamic_lookup, and without it the first call SIGSEGVs at PC=0. + try + { + System.loadLibrary("jawt"); + } + catch (UnsatisfiedLinkError e) + { + log.debug("System.loadLibrary(\"jawt\") said: {}", e.toString()); + } + + String resourcePath = "/com/gpuvulkan/librlmtl.dylib"; + try (InputStream in = MacOSMetalHelper.class.getResourceAsStream(resourcePath)) + { + if (in == null) + { + throw new RuntimeException("librlmtl.dylib not on classpath at " + + resourcePath + " — rebuild the jar on macOS with ./gradlew shadowJar " + + "so compileMacOSMetalHelper can package the native helper."); + } + File temp = File.createTempFile("librlmtl", ".dylib"); + temp.deleteOnExit(); + Files.copy(in, temp.toPath(), StandardCopyOption.REPLACE_EXISTING); + System.load(temp.getAbsolutePath()); + loaded = true; + log.info("Loaded librlmtl.dylib from {}", temp.getAbsolutePath()); + } + catch (IOException e) + { + throw new RuntimeException("Failed to extract/load librlmtl.dylib", e); + } + } + + private static native long nAttachMetalLayer(Canvas canvas, boolean vsync, + int initialWidthPoints, int initialHeightPoints, double scale); + private static native void nDetachMetalLayer(); + private static native void nResizeMetalLayer(int widthPoints, int heightPoints, double scale); + private static native long[] nNextDrawable(); + private static native void nPresentDrawable(long drawable, long mtlQueue); + private static native void nRetainObject(long ptr); + private static native void nReleaseObject(long ptr); + + static long attachMetalLayer(Canvas canvas, boolean vsync) + { + ensureLoaded(); + // Pass the Canvas size: LWAWT defers layout, and an unsized layer + // doesn't render until the first resize. + int w = Math.max(canvas.getWidth(), 1); + int h = Math.max(canvas.getHeight(), 1); + layerScale = canvasScale(canvas); + long ptr = nAttachMetalLayer(canvas, vsync, w, h, layerScale); + if (ptr == 0L) + { + throw new RuntimeException("nAttachMetalLayer returned NULL — " + + "JAWT_GetAWT rejected every version, or the canvas was " + + "not in a JAWT-lockable state"); + } + return ptr; + } + + static void detachMetalLayer() + { + if (loaded) + { + nDetachMetalLayer(); + } + } + + static void resizeMetalLayer(Canvas canvas) + { + long token = ResizeTrace.start("metal.resizeCanvas", + canvas == null ? "canvas=null" : canvas.getWidth() + "x" + canvas.getHeight()); + layerScale = canvasScale(canvas); + try + { + resizeMetalLayerSize(canvas.getWidth(), canvas.getHeight()); + } + finally + { + ResizeTrace.end(token, "metal.resizeCanvas"); + } + } - static void resizeMetalLayer(Canvas canvas) {} + static void resizeMetalLayerSize(int widthPoints, int heightPoints) + { + if (loaded) + { + int width = Math.max(widthPoints, 1); + int height = Math.max(heightPoints, 1); + long start = System.nanoTime(); + nResizeMetalLayer(width, height, layerScale); + ResizeTrace.slow("metal.resizeLayer", System.nanoTime() - start, + width + "x" + height + " scale=" + layerScale); + } + } - static void resizeMetalLayerSize(int width, int height) {} + private static double canvasScale(Canvas canvas) + { + if (canvas == null) + { + return 1.0; + } + GraphicsConfiguration graphicsConfiguration = canvas.getGraphicsConfiguration(); + if (graphicsConfiguration == null) + { + return 1.0; + } + AffineTransform transform = graphicsConfiguration.getDefaultTransform(); + return Math.max(transform.getScaleX(), transform.getScaleY()); + } + /** Returns [drawable, MTLTexture, width, height] or null on timeout. The drawable + * MUST reach {@link #presentDrawable} even on failure paths, or nextDrawable stalls. */ static long[] nextDrawable() { - return null; + ensureLoaded(); + long start = System.nanoTime(); + long[] drawable = nNextDrawable(); + long elapsed = System.nanoTime() - start; + ResizeTrace.slow("metal.nextDrawable", elapsed, + drawable == null ? "null" : drawable[2] + "x" + drawable[3]); + return drawable; } - static void presentDrawable(long drawable, long mtlQueue) {} + /** Schedules [drawable present] on mtlQueue and drops nextDrawable's retain. + * Must run AFTER the render's vkQueueSubmit on the same queue — Metal's + * in-queue ordering is what sequences the present behind the render. */ + static void presentDrawable(long drawable, long mtlQueue) + { + long start = System.nanoTime(); + nPresentDrawable(drawable, mtlQueue); + ResizeTrace.slow("metal.presentDrawable", System.nanoTime() - start, + "drawable=0x" + Long.toHexString(drawable)); + } - static void retainObject(long ptr) {} + /** {@code [obj retain]} on an arbitrary Objective-C handle (typically + * an {@code MTLTexture}). Pair with {@link #releaseObject}. */ + static void retainObject(long ptr) + { + nRetainObject(ptr); + } - static void releaseObject(long ptr) {} + /** {@code [obj release]}; pair with {@link #retainObject}. */ + static void releaseObject(long ptr) + { + nReleaseObject(ptr); + } } diff --git a/src/main/java/com/gpuvulkan/platform/MacOSPlatformSurface.java b/src/main/java/com/gpuvulkan/platform/MacOSPlatformSurface.java index 9ddee81..bc907e5 100644 --- a/src/main/java/com/gpuvulkan/platform/MacOSPlatformSurface.java +++ b/src/main/java/com/gpuvulkan/platform/MacOSPlatformSurface.java @@ -1,28 +1,106 @@ /* * Copyright (c) 2026, Dennis de Vulder * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR + * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.gpuvulkan; import java.awt.Canvas; +import java.nio.LongBuffer; +import lombok.extern.slf4j.Slf4j; +import org.lwjgl.system.MemoryStack; +import org.lwjgl.system.MemoryUtil; +import org.lwjgl.vulkan.EXTMetalSurface; +import org.lwjgl.vulkan.KHRPortabilityEnumeration; +import org.lwjgl.vulkan.KHRSurface; +import org.lwjgl.vulkan.VkMetalSurfaceCreateInfoEXT; + +import static org.lwjgl.system.MemoryStack.stackPush; +import static org.lwjgl.vulkan.VK13.VK_SUCCESS; /** - * macOS is not shipped in the Plugin Hub build because it needs native layer - * integration that Plugin Hub policy does not allow. + * macOS surface via {@code VK_EXT_metal_surface}: {@link MacOSMetalHelper} + * attaches the CAMetalLayer, vkCreateMetalSurfaceEXT wraps the pointer. */ +@Slf4j final class MacOSPlatformSurface implements PlatformSurface { - MacOSPlatformSurface(boolean vsync) {} + private final boolean vsync; + + MacOSPlatformSurface(boolean vsync) + { + this.vsync = vsync; + } + @Override public String[] requiredInstanceExtensions() { - return new String[0]; + return new String[] + { + KHRSurface.VK_KHR_SURFACE_EXTENSION_NAME, + EXTMetalSurface.VK_EXT_METAL_SURFACE_EXTENSION_NAME, + KHRPortabilityEnumeration.VK_KHR_PORTABILITY_ENUMERATION_EXTENSION_NAME, + }; } @Override public long createSurface(VulkanInstance instance, Canvas canvas) { - throw new UnsupportedOperationException("GPU (Vulkan) macOS support is not available in this build"); + // Hold the AWT tree lock across JAWT — the peer can mutate between + // lock and GetDrawingSurfaceInfo and crash in jni_GetObjectField. + long caMetalLayer; + synchronized (canvas.getTreeLock()) + { + if (!canvas.isValid()) + { + throw new RuntimeException("Canvas not valid at JAWT-attach time"); + } + caMetalLayer = MacOSMetalHelper.attachMetalLayer(canvas, vsync); + } + log.info("Attached CAMetalLayer 0x{}", Long.toHexString(caMetalLayer)); + + // Synthetic COMPONENT_RESIZED so AWT lays out the CALayer now. + // AWT-only dispatch — setting CALayer.bounds directly trips MoltenVK. + javax.swing.SwingUtilities.invokeLater(() -> + { + canvas.dispatchEvent(new java.awt.event.ComponentEvent( + canvas, java.awt.event.ComponentEvent.COMPONENT_RESIZED)); + }); + + try (MemoryStack stack = stackPush()) + { + VkMetalSurfaceCreateInfoEXT info = VkMetalSurfaceCreateInfoEXT.calloc(stack) + .sType$Default(); + // LWJGL 3.3.6's pLayer(PointerBuffer) writes the buffer's address, + // not the contained pointer — write the field offset directly. + MemoryUtil.memPutAddress( + info.address() + VkMetalSurfaceCreateInfoEXT.PLAYER, + caMetalLayer); + + LongBuffer pSurface = stack.mallocLong(1); + Vk.check("vkCreateMetalSurfaceEXT", EXTMetalSurface.vkCreateMetalSurfaceEXT( + instance.handle(), info, null, pSurface)); + return pSurface.get(0); + } } } diff --git a/src/main/java/com/gpuvulkan/platform/PlatformSurface.java b/src/main/java/com/gpuvulkan/platform/PlatformSurface.java index 536c21a..8e507fb 100644 --- a/src/main/java/com/gpuvulkan/platform/PlatformSurface.java +++ b/src/main/java/com/gpuvulkan/platform/PlatformSurface.java @@ -40,7 +40,8 @@ interface PlatformSurface * ownership; destroys via {@code vkDestroySurfaceKHR}. */ long createSurface(VulkanInstance instance, Canvas canvas); - /** Platforms control vsync via swapchain present modes. */ + /** {@code vsync} is macOS-only (CAMetalLayer.displaySyncEnabled); other + * platforms control vsync via swapchain present modes. */ static PlatformSurface current(boolean vsync) { String os = System.getProperty("os.name", "").toLowerCase(); @@ -52,6 +53,10 @@ static PlatformSurface current(boolean vsync) { return new Win32PlatformSurface(); } + if (os.contains("mac") || os.contains("darwin")) + { + return new MacOSPlatformSurface(vsync); + } throw new UnsupportedOperationException( "GPU (Vulkan) plugin: unsupported OS \"" + System.getProperty("os.name") + "\""); } diff --git a/src/main/native/rlmtl.m b/src/main/native/rlmtl.m new file mode 100644 index 0000000..97338e9 --- /dev/null +++ b/src/main/native/rlmtl.m @@ -0,0 +1,461 @@ +/* + * rlmtl — minimal native bridge that attaches a CAMetalLayer to the AWT + * canvas's region on macOS so MoltenVK has a valid surface to render into. + * + * Approach: walk JAWT to reach the canvas's CALayer + the NSWindow's + * contentView (AWTView, the single NSView holding all of RuneLite's UI on + * macOS JDK). Assign a CAMetalLayer through JAWT_SurfaceLayers so AWT owns + * the layer placement. Drive AppKit's compose tick with a main-run-loop + * NSTimer + [CATransaction flush] so MoltenVK's async drawable presents + * actually reach the screen. + * + * Build (vkdev does this automatically): + * clang -fno-objc-arc -shared -dynamiclib \ + * -framework Cocoa -framework QuartzCore -framework Metal \ + * -I "$JAVA_HOME/include" -I "$JAVA_HOME/include/darwin" \ + * -Wl,-undefined,dynamic_lookup \ + * rlmtl.m -o librlmtl.dylib + */ + +#import +#import +#import +#import +#import + +static CAMetalLayer* gMetalLayer = nil; +static NSTimer* gFlushTimer = nil; + +/* JAWT_SurfaceLayers integration: the protocol has a `layer` SETTER — + * the application assigns its CALayer; AWT then incorporates that layer + * into the Canvas's CALayer hierarchy and handles positioning + resize + * automatically. This is the documented pattern (same one rlawt uses for + * OpenGL). Reading `layer` before setting returns nil, which is what + * tripped the earlier "use surfaceLayers.layer" attempt. + * + * With this approach our CAMetalLayer is geometrically scoped to the AWT + * Canvas widget's bounds — anything outside the Canvas (RuneLite plugin + * panels rendered as Swing siblings, the title bar) is not covered. We + * don't need a parent-layer reference to track resizes; AWT does it. The + * display link only needs to keep CAMetalLayer.drawableSize in sync with + * the layer's bounds (CAMetalLayer doesn't auto-update drawableSize), and + * to flush CATransaction so MoltenVK's async-presented drawables actually + * reach the compositor each refresh. */ +@interface DisplayLinkTicker : NSObject +- (void)displayLinkTick:(NSTimer*)timer; +@end + +@implementation DisplayLinkTicker +- (void)displayLinkTick:(NSTimer*)timer +{ + CAMetalLayer* mtl = gMetalLayer; + if (mtl) { + /* Keep drawableSize in sync with the layer's actual on-screen + * size. AWT moves/resizes our layer (we set it via + * surfaceLayers.layer = mtl), so layer.bounds reflects the + * current AWT Canvas size in points. Multiply by contentsScale + * for the pixel-resolution backing texture. */ + CGFloat scale = mtl.contentsScale; + if (scale <= 0) scale = 1; + CGSize wanted = CGSizeMake( + mtl.bounds.size.width * scale, + mtl.bounds.size.height * scale); + if (wanted.width > 0 && wanted.height > 0 + && !CGSizeEqualToSize(wanted, mtl.drawableSize)) + { + [CATransaction begin]; + [CATransaction setDisableActions: YES]; + mtl.drawableSize = wanted; + [CATransaction commit]; + } + } + /* Force any pending implicit CATransaction (from MoltenVK's + * [drawable present] earlier this frame) to commit so the layer + * actually shows the latest drawable on this refresh. */ + [CATransaction flush]; +} +@end + +static DisplayLinkTicker* gTicker = nil; + +static void rlmtlFlushCoreAnimationAsync(void) +{ + dispatch_async(dispatch_get_main_queue(), ^{ + [CATransaction flush]; + }); +} + +static void rlmtlStopDisplayLink(void) +{ + if (gFlushTimer) { + [gFlushTimer invalidate]; + [gFlushTimer release]; + gFlushTimer = nil; + } +} + +static void rlmtlDetachMetalLayer(void) +{ + rlmtlStopDisplayLink(); + CAMetalLayer* layer = gMetalLayer; + gMetalLayer = nil; + DisplayLinkTicker* ticker = gTicker; + gTicker = nil; + if (!layer && !ticker) { + return; + } + void (^cleanup)(void) = ^{ + if (layer) { + /* AWT may still hold a reference via surfaceLayers.layer. + * removeFromSuperlayer detaches us from whatever AWT-managed + * parent layer we ended up under; our explicit -release drops + * the retain we held in nAttachMetalLayer. AWT's retain (set + * via the property assignment) is dropped when AWT tears down + * the surfaceLayers object — usually on canvas dispose. */ + [layer removeFromSuperlayer]; + [layer release]; + } + if (ticker) { + [ticker release]; + } + }; + if ([NSThread isMainThread]) { + cleanup(); + } else { + dispatch_sync(dispatch_get_main_queue(), cleanup); + } +} + +JNIEXPORT void JNICALL +Java_com_gpuvulkan_MacOSMetalHelper_nDetachMetalLayer( + JNIEnv* env, jclass cls) +{ + rlmtlDetachMetalLayer(); +} + +JNIEXPORT void JNICALL +Java_com_gpuvulkan_MacOSMetalHelper_nResizeMetalLayer( + JNIEnv* env, jclass cls, jint widthPoints, jint heightPoints, jdouble scaleHint) +{ + CAMetalLayer* layer = gMetalLayer; + if (!layer) { + return; + } + int w = widthPoints > 0 ? widthPoints : 1; + int h = heightPoints > 0 ? heightPoints : 1; + void (^resize)(void) = ^{ + CGFloat scale = scaleHint > 0 ? (CGFloat) scaleHint : layer.contentsScale; + if (scale <= 0) { + scale = [[NSScreen mainScreen] backingScaleFactor]; + if (scale <= 0) scale = 1; + } + layer.contentsScale = scale; + + [CATransaction begin]; + [CATransaction setDisableActions: YES]; + layer.contentsScale = scale; + CGSize bounds = layer.bounds.size; + CGFloat drawableWidth = bounds.width > 0 ? bounds.width : (CGFloat) w; + CGFloat drawableHeight = bounds.height > 0 ? bounds.height : (CGFloat) h; + layer.drawableSize = CGSizeMake(drawableWidth * scale, drawableHeight * scale); + [CATransaction commit]; + [CATransaction flush]; + }; + if ([NSThread isMainThread]) { + resize(); + } else { + dispatch_async(dispatch_get_main_queue(), resize); + } +} + +/* + * Retain/release an arbitrary Objective-C object pointer (used for keeping + * MTLTexture handles alive while MetalDrawableSet caches VkImages wrapping + * them — see MetalDrawableSet.java for why). + * + * The CAMetalDrawable's MTLTexture is owned by the drawable. Once the + * drawable's retain count drops to zero (typically after the present + * MTLCommandBuffer completes asynchronously), the texture is released and + * any VkImage we built around it goes dangling. CAMetalLayer's drawable + * pool may then hand back a drawable whose `texture` pointer reuses the + * freed slot — our cache lookup by pointer thinks it's a hit, returns the + * stale VkImage, and MoltenVK crashes inside command encoding the next + * time it dereferences the dead MTLTexture. + * + * Workaround: pin the MTLTexture with our own retain for as long as we + * keep the VkImage cached. Drop the retain when the cache entry is + * evicted (canvas resize) or the renderer is closed. + */ +JNIEXPORT void JNICALL +Java_com_gpuvulkan_MacOSMetalHelper_nRetainObject( + JNIEnv* env, jclass cls, jlong ptr) +{ + id obj = (id)(uintptr_t) ptr; + if (obj) [obj retain]; +} + +JNIEXPORT void JNICALL +Java_com_gpuvulkan_MacOSMetalHelper_nReleaseObject( + JNIEnv* env, jclass cls, jlong ptr) +{ + id obj = (id)(uintptr_t) ptr; + if (obj) [obj release]; +} + +/* + * Custom present path — bypasses MoltenVK's vkQueuePresentKHR entirely. + * + * The idea: MoltenVK's swapchain + present is one of the few code paths we + * can't see into. Sync validation is silent on the symptom, the geometry + * renders correctly, but a "one layer covering/uncovering" flicker persists + * tied to zoom level. The likeliest remaining suspect is the timing of the + * internal [drawable present] call MoltenVK schedules in response to + * vkQueuePresentKHR — its async present queue can hand a drawable to the + * compositor at a point that doesn't agree with where our render commands + * land in the Metal command stream. + * + * By acquiring the CAMetalDrawable directly from our CAMetalLayer, importing + * its MTLTexture as a VkImage on the Java side (via VK_EXT_metal_objects), + * rendering into that VkImage, and then presenting it via our OWN tiny + * MTLCommandBuffer ordered after the render submit, we get full ordering + * control. No async present queue between us and the compositor. + * + * Returns the drawable + its texture handles packed into a Java long[]: + * [0] = (uintptr_t)id (retained — caller must + * eventually release via Present) + * [1] = (uintptr_t)id + * [2] = (jlong)drawable.texture.width (pixels) + * [3] = (jlong)drawable.texture.height (pixels) + * Returns NULL if the layer is unavailable or nextDrawable blocks past the + * 1-second internal timeout. + */ +JNIEXPORT jlongArray JNICALL +Java_com_gpuvulkan_MacOSMetalHelper_nNextDrawable( + JNIEnv* env, jclass cls) +{ + CAMetalLayer* layer = gMetalLayer; + if (!layer) { + return NULL; + } + + /* @autoreleasepool is MANDATORY here. The JVM's JNI threads don't have + * a top-level autorelease pool, so every autoreleased Objective-C object + * that AppKit/Metal create during -[CAMetalLayer nextDrawable] (and the + * deeper compositor calls it makes) would leak forever — including big + * IOSurfaces and MoltenVK-internal Metal state — until MoltenVK trips + * on its own debris ~50 frames in and crashes inside its compositor + * code. Wrap every JNI entry that calls into AppKit/Metal in a pool. */ + __block jlongArray result = NULL; + @autoreleasepool { + /* nextDrawable can block waiting for a drawable to become available + * (CAMetalLayer.maximumDrawableCount defaults to 3 on macOS). Call + * on the calling JNI thread (the RuneLite render thread). Not main + * thread — we don't want to stall the AppKit event loop. */ + id drawable = [[layer nextDrawable] retain]; + if (!drawable) { + return NULL; + } + + id tex = drawable.texture; + if (!tex) { + [drawable release]; + return NULL; + } + + jlong values[4]; + values[0] = (jlong)(uintptr_t) drawable; + values[1] = (jlong)(uintptr_t) tex; + values[2] = (jlong) tex.width; + values[3] = (jlong) tex.height; + + result = (*env)->NewLongArray(env, 4); + if (!result) { + [drawable release]; + return NULL; + } + (*env)->SetLongArrayRegion(env, result, 0, 4, values); + } + return result; +} + +/* + * Schedules [drawable present] on a tiny MTLCommandBuffer built from the + * supplied MTLCommandQueue, then releases the retain we took in + * nNextDrawable. The Vulkan render submit must have committed (and ideally + * completed; the caller waits on its fence/semaphore) BEFORE this call — + * otherwise we'd present a half-rendered drawable. + */ +JNIEXPORT void JNICALL +Java_com_gpuvulkan_MacOSMetalHelper_nPresentDrawable( + JNIEnv* env, jclass cls, jlong drawableHandle, jlong mtlQueueHandle) +{ + id drawable = (id) (uintptr_t) drawableHandle; + id queue = (id) (uintptr_t) mtlQueueHandle; + if (!drawable || !queue) { + if (drawable) [drawable release]; + return; + } + + /* @autoreleasepool is MANDATORY (see comment in nNextDrawable). The + * MTLCommandBuffer returned by -[MTLCommandQueue commandBuffer] is + * autoreleased, as are the assorted Metal objects MoltenVK and the + * compositor create during the present path. Without a pool every + * frame these accumulate and either OOM or trip MoltenVK's internal + * resource tracking — manifesting as a SIGSEGV inside libMoltenVK + * a few dozen frames in. */ + @autoreleasepool { + id cmd = [queue commandBuffer]; + [cmd presentDrawable: drawable]; + [cmd addCompletedHandler:^(id completed) { + (void) completed; + rlmtlFlushCoreAnimationAsync(); + }]; + [cmd commit]; + /* Do NOT wait — Metal handles the present asynchronously on the + * compositor side. Ordering with our Vulkan render is guaranteed + * by both submits running on the same MTLCommandQueue. */ + [drawable release]; + } +} + +JNIEXPORT jlong JNICALL +Java_com_gpuvulkan_MacOSMetalHelper_nAttachMetalLayer( + JNIEnv* env, jclass cls, jobject canvas, jboolean vsync, + jint initialWidthPoints, jint initialHeightPoints, jdouble scaleHint) +{ + rlmtlDetachMetalLayer(); + + JAWT awt; + const jint versions[] = { + JAWT_VERSION_9 | JAWT_MACOSX_USE_CALAYER, + JAWT_VERSION_1_7 | JAWT_MACOSX_USE_CALAYER, + JAWT_VERSION_1_4 | JAWT_MACOSX_USE_CALAYER, + }; + jboolean got = JNI_FALSE; + for (int i = 0; i < 3; i++) { + awt.version = versions[i]; + if (JAWT_GetAWT(env, &awt)) { + got = JNI_TRUE; + break; + } + } + if (!got) { + return 0; + } + + JAWT_DrawingSurface* ds = awt.GetDrawingSurface(env, canvas); + if (!ds) { + return 0; + } + + jint lock = ds->Lock(ds); + if ((lock & JAWT_LOCK_ERROR) != 0) { + awt.FreeDrawingSurface(ds); + return 0; + } + + /* Retain surfaceLayers under the JAWT lock, then release the lock + * before AppKit work. surfaceLayers is the JAWT bridge object whose + * `layer` property we'll assign our CAMetalLayer to on the main + * thread. */ + id surfaceLayers = nil; + JAWT_DrawingSurfaceInfo* dsi = ds->GetDrawingSurfaceInfo(ds); + if (dsi) { + id sl = + (id) dsi->platformInfo; + if (sl) { + surfaceLayers = (id) [(NSObject*) sl retain]; + } + ds->FreeDrawingSurfaceInfo(dsi); + } + ds->Unlock(ds); + awt.FreeDrawingSurface(ds); + + if (!surfaceLayers) { + return 0; + } + + __block CAMetalLayer* metalLayer = nil; + __block CGFloat scale = scaleHint > 0 ? (CGFloat) scaleHint : 1; + dispatch_sync(dispatch_get_main_queue(), ^{ + if (scale <= 0) { + scale = [[NSScreen mainScreen] backingScaleFactor]; + if (scale <= 0) scale = 1; + } + + metalLayer = [[CAMetalLayer alloc] init]; + id device = MTLCreateSystemDefaultDevice(); + if (device) { + metalLayer.device = device; + [device release]; + } + metalLayer.pixelFormat = MTLPixelFormatBGRA8Unorm; + metalLayer.allowsNextDrawableTimeout = YES; + metalLayer.contentsScale = scale; + metalLayer.anchorPoint = CGPointZero; + metalLayer.position = CGPointZero; + metalLayer.frame = CGRectMake(0, 0, + initialWidthPoints > 0 ? initialWidthPoints : 1, + initialHeightPoints > 0 ? initialHeightPoints : 1); + /* drawableSize MUST start small here because vkCreateSwapchainKHR + * later reads surfaceCapabilities.currentExtent, which MoltenVK + * derives from drawableSize. A full-size value here makes + * MoltenVK's internal swapchain claim drawables from the same + * 3-deep pool we acquire from directly — they collide on the + * first scene render and SIGSEGV inside libMoltenVK at a fixed + * offset. 1×1 keeps the swapchain a tiny no-op (we never use + * it on macOS anyway; our custom-present path bypasses it). */ + metalLayer.drawableSize = CGSizeMake(1, 1); + /* displaySyncEnabled hardcoded to NO. The vsync param is currently + * ignored on macOS — see the comment below. The compositor still + * presents at most one drawable per display refresh, so visible + * FPS is capped at refresh rate (120 Hz on ProMotion) regardless. + * + * Why we don't honor vsync=YES yet: setting displaySyncEnabled=YES + * combined with our custom-present path (where the [drawable + * present] runs on a separate MTLCommandBuffer from the Vulkan + * render submit) reliably crashes MoltenVK on the first scene + * render at a fixed offset (libMoltenVK+0x47a78). Reverting to + * NO is the known-good configuration. If the user really wants a + * true vsync mode (battery-saver, no tearing), the right + * approach is probably to route present back through + * vkQueuePresentKHR conditionally — that's a bigger change, + * deferred. */ + (void) vsync; + metalLayer.displaySyncEnabled = NO; + surfaceLayers.layer = metalLayer; + }); + (void) initialWidthPoints; + (void) initialHeightPoints; + + /* AWT retains the metal layer via its property setter (the setter is + * declared `retain`). Our retain on surfaceLayers was only to keep + * the bridge object alive across the dispatch_sync — drop it now. */ + [(NSObject*) surfaceLayers release]; + + if (!metalLayer) { + return 0; + } + + NSLog(@"rlmtl: attached CAMetalLayer %p via JAWT_SurfaceLayers @ %.1fx scale", + metalLayer, scale); + + gMetalLayer = metalLayer; + + /* AppKit doesn't tick a CAMetalLayer's compose loop on its own when + * MoltenVK presents drawables asynchronously — without the periodic + * flush in the tick handler, drawables get queued but never displayed. + * NSTimer keeps this bridge compatible with pre-Sonoma macOS; exact + * display cadence is not required because Vulkan still drives frames. */ + dispatch_sync(dispatch_get_main_queue(), ^{ + gTicker = [[DisplayLinkTicker alloc] init]; + gFlushTimer = [[NSTimer timerWithTimeInterval: (1.0 / 120.0) + target: gTicker + selector: @selector(displayLinkTick:) + userInfo: nil + repeats: YES] retain]; + [[NSRunLoop mainRunLoop] addTimer: gFlushTimer forMode: NSRunLoopCommonModes]; + }); + + return (jlong)(uintptr_t) metalLayer; +} diff --git a/src/main/resources/com/gpuvulkan/librlmtl.dylib b/src/main/resources/com/gpuvulkan/librlmtl.dylib new file mode 100755 index 0000000000000000000000000000000000000000..d5ed4b7e6381015d8f864a8081cd49efe920e2c3 GIT binary patch literal 139584 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