Skip to content

Commit b614e35

Browse files
committed
fix(ci): bundle Vulkan loader + ICD manifests, add Zink backend auto-select
The bundle shipped Mesa Zink + KosmicKrisp + MoltenVK but never worked as a hardware path because of three bugs proven on the M3 and a macos-26 CI probe: - The Vulkan loader libvulkan.1.dylib was never bundled. Zink dlopen()s it by name and failed with 'ZINK: failed to load libvulkan.1.dylib'. Now copied (cp -L) from brew vulkan-loader under the canonical name with its deps rewritten to @executable_path. - The ICD manifest step copied a brew path that does not exist, leaving share/vulkan/icd.d empty. Now generates kosmickrisp.json / MoltenVK.json with library_path relative to the manifest so it resolves post-install. - The launcher forced MoltenVK and never set GALLIUM_DRIVER, so Mesa fell back to llvmpipe. The launcher now selects the backend at runtime: llvmpipe by default (the only CI-verifiable path; hosted macOS runners have no GPU), Zink->KosmicKrisp->Metal when it detects macOS 26+ where KosmicKrisp exposes the robustness2 nullDescriptor Zink requires. BAR_FORCE_LLVMPIPE=1 overrides. Uses DYLD_FALLBACK_LIBRARY_PATH (safe; does not shadow the system OpenGL framework) so Zink finds the loader.
1 parent 15fb248 commit b614e35

1 file changed

Lines changed: 85 additions & 8 deletions

File tree

.github/workflows/macos-build.yml

Lines changed: 85 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -273,20 +273,75 @@ jobs:
273273
fi
274274
done
275275
276-
# MoltenVK / KosmicKrisp ICD + dylib.
276+
# ---- Vulkan stack for the Zink hardware-rendering path ----
277+
# On a real macOS 26 + Apple Silicon machine the engine can
278+
# render through Mesa's Zink driver onto the Metal GPU via
279+
# KosmicKrisp instead of the llvmpipe software rasteriser. The
280+
# launcher (below) activates this at runtime by setting
281+
# GALLIUM_DRIVER=zink when it detects macOS 26+. Three things
282+
# must be in the bundle for that to work; the previous version
283+
# of this step shipped none of them correctly.
277284
MOLTEN_PREFIX="$(brew --prefix molten-vk)"
278-
mkdir -p "$APP/Contents/Resources/share/vulkan/icd.d"
279-
if [ -f "$MOLTEN_PREFIX/share/vulkan/icd.d/MoltenVK_icd.json" ]; then
280-
cp -f "$MOLTEN_PREFIX/share/vulkan/icd.d/MoltenVK_icd.json" \
281-
"$APP/Contents/Resources/share/vulkan/icd.d/"
285+
LOADER_PREFIX="$(brew --prefix vulkan-loader)"
286+
287+
# (1) The Vulkan loader. Zink dlopen()s "libvulkan.1.dylib" by
288+
# name, so it MUST be bundled under that exact name — without it
289+
# Zink dies with "ZINK: failed to load libvulkan.1.dylib". brew
290+
# ships it as a symlink to a versioned file; cp -L copies the
291+
# real target.
292+
if [ -f "$LOADER_PREFIX/lib/libvulkan.1.dylib" ]; then
293+
cp -L -f "$LOADER_PREFIX/lib/libvulkan.1.dylib" "$LIBDIR/libvulkan.1.dylib"
294+
install_name_tool -id "$PREFIX/libvulkan.1.dylib" "$LIBDIR/libvulkan.1.dylib"
295+
ln -sf libvulkan.1.dylib "$LIBDIR/libvulkan.dylib"
296+
for dep in $(otool -L "$LIBDIR/libvulkan.1.dylib" | grep -oE '/opt/homebrew/[^ ]+\.dylib' | sort -u); do
297+
if [ -f "$dep" ]; then
298+
process_dylib "$dep"
299+
dep_base="$(basename "$dep")"
300+
install_name_tool -change "$dep" "$PREFIX/$dep_base" "$LIBDIR/libvulkan.1.dylib"
301+
fi
302+
done
303+
else
304+
echo "::warning::Vulkan loader libvulkan.1.dylib not found; Zink hardware path will be unavailable"
282305
fi
306+
307+
# (2) The Vulkan-on-Metal drivers (ICDs). KosmicKrisp is the
308+
# macOS-26 hardware path; MoltenVK is bundled as a secondary.
283309
if [ -f "$MESA_PREFIX/lib/libvulkan_kosmickrisp.dylib" ]; then
284310
process_dylib "$MESA_PREFIX/lib/libvulkan_kosmickrisp.dylib"
285311
fi
286312
if [ -f "$MOLTEN_PREFIX/lib/libMoltenVK.dylib" ]; then
287313
process_dylib "$MOLTEN_PREFIX/lib/libMoltenVK.dylib"
288314
fi
289315
316+
# (3) ICD manifests so the loader can find a driver. library_path
317+
# is written RELATIVE to the manifest so it resolves wherever the
318+
# user installs the .app: the manifest lives at
319+
# Resources/share/vulkan/icd.d/, the dylibs at Resources/lib/, so
320+
# the relative path is ../../../lib/<dylib>. The previous version
321+
# copied a brew ICD path that does not exist, leaving icd.d empty.
322+
ICD_DIR="$APP/Contents/Resources/share/vulkan/icd.d"
323+
mkdir -p "$ICD_DIR"
324+
if [ -f "$LIBDIR/libvulkan_kosmickrisp.dylib" ]; then
325+
printf '%s\n' \
326+
'{' \
327+
' "file_format_version": "1.0.0",' \
328+
' "ICD": {' \
329+
' "library_path": "../../../lib/libvulkan_kosmickrisp.dylib",' \
330+
' "api_version": "1.3.0"' \
331+
' }' \
332+
'}' > "$ICD_DIR/kosmickrisp.json"
333+
fi
334+
if [ -f "$LIBDIR/libMoltenVK.dylib" ]; then
335+
printf '%s\n' \
336+
'{' \
337+
' "file_format_version": "1.0.0",' \
338+
' "ICD": {' \
339+
' "library_path": "../../../lib/libMoltenVK.dylib",' \
340+
' "api_version": "1.2.0"' \
341+
' }' \
342+
'}' > "$ICD_DIR/MoltenVK.json"
343+
fi
344+
290345
# Copy docs but exclude dotfiles (like .hugo_build.lock,
291346
# .gitignore) that get dropped during artifact upload and
292347
# break the code-signed bundle seal.
@@ -414,10 +469,32 @@ jobs:
414469
# there is no X11 display server. Without this, eglInitialize
415470
# tries to connect to xcb and fails with EGL_NOT_INITIALIZED.
416471
export EGL_PLATFORM=surfaceless
417-
# Vulkan loader looks here for the KosmicKrisp / MoltenVK ICD.
418-
export VK_DRIVER_FILES="\$RES/share/vulkan/icd.d/MoltenVK_icd.json"
419-
export VK_ICD_FILENAMES="\$RES/share/vulkan/icd.d/MoltenVK_icd.json"
420472
export VULKAN_SDK="\$RES"
473+
# ---- Rendering backend selection ----
474+
# Default: Mesa llvmpipe software rendering. It is the only path
475+
# that can be verified in CI (GitHub's hosted macOS runners have
476+
# no GPU: KosmicKrisp reports "Failed to detect any valid GPUs")
477+
# and it works on every macOS version.
478+
#
479+
# On a real macOS 26+ Apple Silicon machine the GPU is reachable
480+
# through Mesa Zink -> KosmicKrisp -> Metal, which is far faster
481+
# than llvmpipe. KosmicKrisp only exposes the Vulkan features
482+
# Zink requires (VK_EXT_robustness2 nullDescriptor) on macOS 26,
483+
# so gate on the OS major version. Set BAR_FORCE_LLVMPIPE=1 to
484+
# force software rendering (e.g. inside a GPU-less VM).
485+
OS_MAJOR="\$(sw_vers -productVersion | cut -d. -f1)"
486+
KOSMIC_ICD="\$RES/share/vulkan/icd.d/kosmickrisp.json"
487+
if [ "\${BAR_FORCE_LLVMPIPE:-0}" != "1" ] && [ "\$OS_MAJOR" -ge 26 ] && [ -f "\$KOSMIC_ICD" ] && [ -f "\$RES/lib/libvulkan.1.dylib" ]; then
488+
export GALLIUM_DRIVER=zink
489+
export MESA_LOADER_DRIVER_OVERRIDE=zink
490+
export VK_ICD_FILENAMES="\$KOSMIC_ICD"
491+
export VK_DRIVER_FILES="\$KOSMIC_ICD"
492+
# Zink dlopen()s libvulkan.1.dylib by name. DYLD_FALLBACK is the
493+
# SAFE search path: unlike DYLD_LIBRARY_PATH it does not shadow
494+
# the system OpenGL framework, so it avoids the NSOpenGLContext
495+
# SIGBUS that a full DYLD_LIBRARY_PATH caused.
496+
export DYLD_FALLBACK_LIBRARY_PATH="\$RES/lib"
497+
fi
421498
cd "\$RES"
422499
exec "\$RES/bin/spring" \\
423500
--isolation-dir "\$GAME_DIR" \\

0 commit comments

Comments
 (0)