From d622e83a8e48b73ba82cf9b923ec7419a6527d65 Mon Sep 17 00:00:00 2001 From: bryanthaboi Date: Tue, 25 Aug 2026 09:16:59 -0400 Subject: [PATCH] shaderfix --- docs/shaderfx.md | 51 +++++++- mobile/ANDROID.md | 5 + mobile/ios/README.md | 1 + scripts/build_android.sh | 110 ++++++++++++++++ scripts/build_ios.sh | 152 ++++++++++++++++++++++- src/core/Sensors.lua | 9 +- src/import/LauncherView.lua | 25 ++-- src/import/RomImporter.lua | 1 + src/render/ShaderFX.lua | 20 ++- tests/engine/shaderfx_ffi_c_fallback.lua | 85 +++++++++++++ tools/shaderfx-bridge/Cargo.toml | 2 +- 11 files changed, 445 insertions(+), 16 deletions(-) create mode 100644 tests/engine/shaderfx_ffi_c_fallback.lua diff --git a/docs/shaderfx.md b/docs/shaderfx.md index 03e34c5b..3fe60c70 100644 --- a/docs/shaderfx.md +++ b/docs/shaderfx.md @@ -220,6 +220,54 @@ output directory; nothing packages it next to a shipped game yet. and `ShaderFX.bridgeError()` says why not. Activating an already-converted preset never needs any of this. +**Statically linked bridges.** On iOS there is no loadable library at all: +the bridge is linked straight into the app binary, so its symbols live in the +main image and are reachable only through `ffi.C`. The candidate list is empty +on iOS and, on every platform, a failed candidate walk falls back to probing +`ffi.C` for `librashader_translate_preset` after the `ffi.cdef`. If the symbol +is there, `ffi.C` becomes the library handle and `ShaderFX.translate` uses it +unchanged. If it is not, `ShaderFX.bridgeError()` reports that `ffi.C` was +probed as well, followed by the file paths that were tried (omitted on iOS, +where none are). + +**Android ships the bridge inside the APK.** `scripts/build_android.sh` +stages `liblibrashader_bridge.so` into +`mobile/android/app/src/main/jniLibs//` for the two ABIs the APK ships, +arm64-v8a and armeabi-v7a, so `ffi.load("liblibrashader_bridge.so")` finds it +by bare name in the app's native library directory. The build cross-compiles +the crate with `cargo ndk` against the same NDK the gradle project uses +(25.2.9519653), which needs `cargo install cargo-ndk` and +`rustup target add aarch64-linux-android armv7-linux-androideabi`; the script +names the exact command for any target that is missing. Set +`SHADERFX_BRIDGE_ANDROID_DIR` to a directory holding +`/liblibrashader_bridge.so` to bundle prebuilt libraries instead. As on +desktop, the bridge is only needed to CONVERT a preset: a build without it +still runs presets converted elsewhere, and the packager warns and continues +rather than failing. + +**iOS links the bridge instead of loading it.** An iOS app cannot `dlopen` a +dylib shipped beside its binary, so `tools/shaderfx-bridge` also builds as a +`staticlib` and `scripts/build_ios.sh` links it into the app executable +(`bundle_shader_bridge_ios`, mirroring `bundle_shader_bridge` in +`scripts/build.sh`). `SHADERFX_BRIDGE_IOS` points at a prebuilt archive; +otherwise cargo builds `aarch64-apple-ios` for device builds and every +installed simulator target (`aarch64-apple-ios-sim`, `x86_64-apple-ios`) for +the Simulator, with `IPHONEOS_DEPLOYMENT_TARGET=15.0`, and `ARCHS` is pinned to +what got built. The archive is never linked directly: LÖVE 12 carries its own +glslang for shader validation and the crate drags in a second, incompatible +one, and linking both crashed inside `Shader::validateInternal` at startup. +Each slice is therefore prelinked with `ld -r -all_load +-exported_symbols_list` so only `_librashader_translate_preset` and +`_librashader_free_string` stay external and every other symbol (glslang, +spirv-cross, Rust std) becomes private to the object, then `rust-objcopy` +drops the `__LLVM` bitcode sections rustup's prebuilt std carries, since +Apple's `nm` cannot read them. `OTHER_LDFLAGS` adds `-Wl,-u` on both entry +points to keep them past dead-stripping and `-lc++` for the C++ the crate +needs; no Objective-C framework is involved. A build that linked the object +fails if `nm` cannot find `_librashader_translate_preset` in the finished +binary; a build that could not produce one only warns and lands where the +desktop packages without cargo land: converted presets run, CONVERT does not. + ### Fixup `ShaderFixup.lua` mechanically rewrites the emitted GLSL into something LOVE @@ -656,7 +704,8 @@ None of these are theoretical. `bundle_shader_bridge`, building it with cargo when a prebuilt one is not supplied through `SHADERFX_BRIDGE`. A build host without cargo produces a package that can run converted presets but cannot CONVERT new ones, and says - so rather than failing. Android ships the `.so` via `jniLibs`. + so rather than failing. `scripts/build_android.sh` and `scripts/build_ios.sh` + follow the same rule with `cargo ndk` and the iOS static archive. - **The buildbot shortlist is a temporary trim.** `KEPT_PRESETS` reflects one manual pass over `handheld/` and is expected to change, most likely to shrink. - **Tilt direction is unverified.** Which way forward and back rocking moves the diff --git a/mobile/ANDROID.md b/mobile/ANDROID.md index 8718896b..0b29e2db 100644 --- a/mobile/ANDROID.md +++ b/mobile/ANDROID.md @@ -100,6 +100,11 @@ love-android 11.5a expects: Set `ANDROID_SDK_ROOT` (or `ANDROID_HOME`), or let the script write `local.properties` when it finds `~/Library/Android/sdk`. +**ShaderFX bridge**: `scripts/build_android.sh` bundles +`liblibrashader_bridge.so` for arm64-v8a and armeabi-v7a via `cargo ndk` (or +from `SHADERFX_BRIDGE_ANDROID_DIR`), and warns and continues when neither is +available; see `docs/shaderfx.md`. + Gradle flavor used: **`embedNoRecord`** (game fused into the APK, no microphone). Build task: `assembleEmbedNoRecordDebug`. diff --git a/mobile/ios/README.md b/mobile/ios/README.md index 37f48b4e..3eb908e8 100644 --- a/mobile/ios/README.md +++ b/mobile/ios/README.md @@ -52,6 +52,7 @@ The script verifies the final app before packaging it: - the public Documents plist settings are present - the native picker bridge is present +- the SHADER FX librashader bridge is linked into the app binary, when this build produced one - `game.love` exists and is non-empty If the payload is missing, the build fails instead of producing a blank app. diff --git a/scripts/build_android.sh b/scripts/build_android.sh index 7451e87a..49917a19 100755 --- a/scripts/build_android.sh +++ b/scripts/build_android.sh @@ -461,6 +461,115 @@ pack_game_love() { fi } +# --------------------------------------------------------------- ShaderFX bridge +SHADER_BRIDGE_LIB="liblibrashader_bridge.so" +SHADER_BRIDGE_ABIS="arm64-v8a armeabi-v7a" + +shader_bridge_rust_target() { + case "$1" in + arm64-v8a) printf 'aarch64-linux-android' ;; + armeabi-v7a) printf 'armv7-linux-androideabi' ;; + x86_64) printf 'x86_64-linux-android' ;; + x86) printf 'i686-linux-android' ;; + *) printf '' ;; + esac +} + +shader_bridge_staged_count() { + local jni="$1" abi count=0 + for abi in $SHADER_BRIDGE_ABIS; do + [ -f "$jni/$abi/$SHADER_BRIDGE_LIB" ] && count=$((count + 1)) + done + printf '%s' "$count" +} + +bundle_shader_bridge_android() { + local jni="$ANDROID_DIR/app/src/main/jniLibs" + local crate="$ROOT/tools/shaderfx-bridge" + local abi target + + for abi in $SHADER_BRIDGE_ABIS; do + rm -f "$jni/$abi/$SHADER_BRIDGE_LIB" + done + + local prebuilt="${SHADERFX_BRIDGE_ANDROID_DIR:-}" + if [ -n "$prebuilt" ]; then + for abi in $SHADER_BRIDGE_ABIS; do + if [ -f "$prebuilt/$abi/$SHADER_BRIDGE_LIB" ]; then + mkdir -p "$jni/$abi" + cp "$prebuilt/$abi/$SHADER_BRIDGE_LIB" "$jni/$abi/$SHADER_BRIDGE_LIB" + else + warn "SHADERFX_BRIDGE_ANDROID_DIR has no $abi/$SHADER_BRIDGE_LIB" + fi + done + if [ "$(shader_bridge_staged_count "$jni")" -gt 0 ]; then + say "bundled $SHADER_BRIDGE_LIB for SHADER FX preset conversion (prebuilt)" + return + fi + fi + + if [ ! -f "$crate/Cargo.toml" ]; then + warn "$SHADER_BRIDGE_LIB not found: this build can run converted presets but not CONVERT new ones (tools/shaderfx-bridge is missing)" + return + fi + + if ! command -v cargo >/dev/null 2>&1 || ! cargo ndk --version >/dev/null 2>&1; then + warn "$SHADER_BRIDGE_LIB not found: this build can run converted presets but not CONVERT new ones (set SHADERFX_BRIDGE_ANDROID_DIR or run 'cargo install cargo-ndk')" + return + fi + + local ndk="${ANDROID_NDK_HOME:-}" + if [ -z "$ndk" ] || [ ! -d "$ndk" ]; then + ndk="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-$HOME/Library/Android/sdk}}/ndk/$NDK_VERSION" + fi + if [ ! -d "$ndk" ]; then + warn "$SHADER_BRIDGE_LIB not built: NDK $NDK_VERSION not found (set ANDROID_NDK_HOME)" + return + fi + + local installed missing="" buildable="" + installed="$(rustup target list --installed 2>/dev/null || true)" + for abi in $SHADER_BRIDGE_ABIS; do + target="$(shader_bridge_rust_target "$abi")" + if grep -qx "$target" <<< "$installed"; then + buildable="$buildable $abi" + else + missing="$missing $target" + fi + done + if [ -n "$missing" ]; then + warn "$SHADER_BRIDGE_LIB: skipping$missing. Run: rustup target add$missing" + fi + if [ -z "$buildable" ]; then + warn "$SHADER_BRIDGE_LIB not built: this build can run converted presets but not CONVERT new ones (no Android Rust targets installed)" + return + fi + + local args=() + for abi in $buildable; do + args+=(-t "$abi") + done + + say "building the ShaderFX bridge with cargo-ndk (${buildable# })" + mkdir -p "$jni" + if ! ( + cd "$crate" + export ANDROID_NDK_HOME="$ndk" + export ANDROID_NDK_ROOT="$ndk" + export CARGO_PROFILE_RELEASE_STRIP="symbols" + cargo ndk "${args[@]}" -o "$jni" build --release + ); then + warn "$SHADER_BRIDGE_LIB failed to cross-compile: this build can run converted presets but not CONVERT new ones" + return + fi + + if [ "$(shader_bridge_staged_count "$jni")" -gt 0 ]; then + say "bundled $SHADER_BRIDGE_LIB for SHADER FX preset conversion" + else + warn "$SHADER_BRIDGE_LIB not found after cargo-ndk: this build can run converted presets but not CONVERT new ones" + fi +} + # --------------------------------------------------------------- SDK check require_android_sdk() { local sdk="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-}}" @@ -575,5 +684,6 @@ if $PACKAGE_ONLY; then fi require_android_sdk +bundle_shader_bridge_android run_gradle say "done" diff --git a/scripts/build_ios.sh b/scripts/build_ios.sh index 9c7fac93..55688193 100755 --- a/scripts/build_ios.sh +++ b/scripts/build_ios.sh @@ -615,6 +615,138 @@ verify_game_payload() { say "game.love present ($(du -h "$app/game.love" | cut -f1))" } +SHADER_BRIDGE_LIB="" +SHADER_BRIDGE_NAME="liblibrashader_bridge.a" +SHADER_BRIDGE_OBJ="librashader_bridge.o" + +SHADER_BRIDGE_ARCHS="" + +rust_targets_for_sdk() { + if [ "$1" = "iphoneos" ]; then + printf 'aarch64-apple-ios' + else + printf 'aarch64-apple-ios-sim x86_64-apple-ios' + fi +} + +build_shader_bridge_slice() { + local rust_target="$1" + local crate="$ROOT/tools/shaderfx-bridge" + local built="$crate/target/$rust_target/release/$SHADER_BRIDGE_NAME" + if [ -f "$built" ]; then + printf '%s' "$built" + return 0 + fi + if command -v cargo >/dev/null 2>&1 \ + && rustup target list --installed 2>/dev/null \ + | grep -x "$rust_target" >/dev/null; then + say "building the ShaderFX bridge with cargo ($rust_target)" >&2 + if (cd "$crate" && IPHONEOS_DEPLOYMENT_TARGET=15.0 \ + cargo build --release --target "$rust_target" >/dev/null 2>&1); then + printf '%s' "$built" + return 0 + fi + fi + return 1 +} + +prelink_shader_bridge_slice() { + local archive="$1" sdk="$2" out="$3" + local arch platform sdk_version syms objcopy tmp + arch="$(lipo -archs "$archive" 2>/dev/null | awk '{print $1}')" + [ -n "$arch" ] || return 1 + if [ "$sdk" = "iphoneos" ]; then platform="ios"; else platform="ios-simulator"; fi + sdk_version="$(xcrun --sdk "$sdk" --show-sdk-version 2>/dev/null)" + [ -n "$sdk_version" ] || return 1 + syms="$LIBS_DIR/librashader_bridge.exports" + printf '_librashader_translate_preset\n_librashader_free_string\n' > "$syms" + tmp="$out.tmp" + xcrun ld -r -arch "$arch" -platform_version "$platform" 15.0 "$sdk_version" \ + -all_load -exported_symbols_list "$syms" -o "$tmp" "$archive" || return 1 + objcopy="$(ls "$(rustc --print sysroot 2>/dev/null)"/lib/rustlib/*/bin/rust-objcopy 2>/dev/null | head -1)" + if [ -n "$objcopy" ] && "$objcopy" --remove-section __LLVM,__bitcode \ + --remove-section __LLVM,__cmdline "$tmp" "$out" 2>/dev/null; then + rm -f "$tmp" + else + mv "$tmp" "$out" + fi +} + +bundle_shader_bridge_ios() { + local rust_targets="$1" sdk="$2" + local src="${SHADERFX_BRIDGE_IOS:-}" + local slices=() objs=() target slice obj i + SHADER_BRIDGE_LIB="" + SHADER_BRIDGE_ARCHS="" + rm -f "$LIBS_DIR/$SHADER_BRIDGE_NAME" "$LIBS_DIR/$SHADER_BRIDGE_OBJ" "$LIBS_DIR"/librashader_bridge.*.o + if [ -n "$src" ]; then + if [ -f "$src" ]; then + slices+=("$src") + else + warn "SHADERFX_BRIDGE_IOS=$src does not exist" + fi + else + for target in $rust_targets; do + if slice="$(build_shader_bridge_slice "$target")"; then + slices+=("$slice") + fi + done + fi + if [ "${#slices[@]}" -gt 0 ]; then + mkdir -p "$LIBS_DIR" + i=0 + for slice in "${slices[@]}"; do + i=$((i + 1)) + obj="$LIBS_DIR/librashader_bridge.$i.o" + if prelink_shader_bridge_slice "$slice" "$sdk" "$obj"; then + objs+=("$obj") + else + warn "could not prelink $(basename "$slice") for $sdk" + fi + done + fi + if [ "${#objs[@]}" -eq 1 ]; then + mv "${objs[0]}" "$LIBS_DIR/$SHADER_BRIDGE_OBJ" + elif [ "${#objs[@]}" -gt 1 ]; then + lipo -create "${objs[@]}" -output "$LIBS_DIR/$SHADER_BRIDGE_OBJ" + rm -f "${objs[@]}" + fi + if [ -f "$LIBS_DIR/$SHADER_BRIDGE_OBJ" ]; then + SHADER_BRIDGE_LIB="$LIBS_DIR/$SHADER_BRIDGE_OBJ" + SHADER_BRIDGE_ARCHS="$(lipo -archs "$SHADER_BRIDGE_LIB" 2>/dev/null || true)" + say "linking $SHADER_BRIDGE_OBJ for SHADER FX preset conversion (${SHADER_BRIDGE_ARCHS:-unknown arch})" + else + warn "$SHADER_BRIDGE_NAME not found: this build can run converted presets but not CONVERT new ones (set SHADERFX_BRIDGE_IOS, or install cargo plus one of: rustup target add $rust_targets)" + fi +} + +verify_shader_bridge() { + local app="$1" + local exe bin + if [ -z "$SHADER_BRIDGE_LIB" ]; then + warn "no SHADER FX bridge in this build: CONVERT stays unavailable, converted presets still run" + return 0 + fi + exe="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' \ + "$app/Info.plist" 2>/dev/null || true)" + bin="$app/${exe:-love}" + [ -f "$bin" ] || bin="$app/love" + if [ ! -f "$bin" ]; then + warn "no executable inside $(basename "$app"); skipping SHADER FX bridge check" + return 0 + fi + if nm "$bin" 2>/dev/null | grep -E ' _librashader_translate_preset$' >/dev/null \ + || xcrun dyld_info -exports "$bin" 2>/dev/null \ + | grep -E ' _librashader_translate_preset$' >/dev/null; then + say "SHADER FX bridge present (librashader_translate_preset)" + return 0 + fi + fail "built app does not carry the SHADER FX bridge symbols. + $SHADER_BRIDGE_OBJ was linked but librashader_translate_preset is absent, + so SHADER FX CONVERT would fail at runtime. + Rebuild after: rm -rf tools/shaderfx-bridge/target" +} + run_xcodebuild() { local config sdk destination if $RELEASE; then @@ -633,6 +765,8 @@ run_xcodebuild() { mkdir -p "$BUILD_DIR" + bundle_shader_bridge_ios "$(rust_targets_for_sdk "$sdk")" "$sdk" + # Prefer -target + SYMROOT over -derivedDataPath: modern Xcode requires # -scheme whenever -derivedDataPath is set, and love-ios ships no shared schemes. # Always stamp both: the overlay plist expands $(MARKETING_VERSION) / @@ -661,6 +795,13 @@ run_xcodebuild() { ONLY_ACTIVE_ARCH=NO DISABLE_MANUAL_TARGET_ORDER_BUILD_WARNING=YES ) + if [ -n "$SHADER_BRIDGE_LIB" ]; then + args+=(OTHER_LDFLAGS="-Wl,-u,_librashader_translate_preset -Wl,-u,_librashader_free_string \"$SHADER_BRIDGE_LIB\" -lc++") + if [ -n "$SHADER_BRIDGE_ARCHS" ]; then + args+=(ARCHS="$SHADER_BRIDGE_ARCHS") + fi + fi + if ! $DEVICE; then # Simulator: ad-hoc signing (no certificate needed). A plain unsigned # build would drop the entitlements file, and HealthKit refuses to run @@ -723,11 +864,14 @@ run_xcodebuild() { local products="$BUILD_DIR/Build/Products/${config}-${sdk}" local app="" - local candidate + local candidate newest=0 mtime for candidate in "$products/$PRODUCT_NAME.app" "$products/$APP_NAME.app" "$products/love.app"; do if [ -d "$candidate" ]; then - app="$candidate" - break + mtime="$(stat -f %m "$candidate" 2>/dev/null || echo 0)" + if [ "$mtime" -gt "$newest" ]; then + newest="$mtime" + app="$candidate" + fi fi done if [ -z "$app" ]; then @@ -736,6 +880,7 @@ run_xcodebuild() { return 0 fi if [ "$app" != "$products/$APP_NAME.app" ]; then + rm -rf "$products/$APP_NAME.app" mv "$app" "$products/$APP_NAME.app" app="$products/$APP_NAME.app" fi @@ -754,6 +899,7 @@ run_xcodebuild() { verify_game_payload "$app" verify_native_bridge "$app" + verify_shader_bridge "$app" local dist_dir="$DIST/${config}-${sdk}" rm -rf "$dist_dir" diff --git a/src/core/Sensors.lua b/src/core/Sensors.lua index 446eb283..940f71f6 100644 --- a/src/core/Sensors.lua +++ b/src/core/Sensors.lua @@ -76,7 +76,14 @@ local function sdlFfi() ]]) if sdlCdefOk then local okLoad, lib = pcall(ffi.load, "SDL2") - sdlLib = okLoad and lib or ffi.C + lib = okLoad and lib or ffi.C + local okSyms = pcall(function() + return lib.SDL_InitSubSystem, lib.SDL_NumSensors, lib.SDL_SensorGetDeviceType, + lib.SDL_SensorOpen, lib.SDL_SensorUpdate, lib.SDL_SensorGetData, + lib.SDL_GL_GetCurrentWindow, lib.SDL_GetWindowDisplayIndex, + lib.SDL_GetDisplayOrientation + end) + sdlLib = okSyms and lib or false end end if not sdlCdefOk or not sdlLib then return nil end diff --git a/src/import/LauncherView.lua b/src/import/LauncherView.lua index dbe4778e..1c54d58b 100644 --- a/src/import/LauncherView.lua +++ b/src/import/LauncherView.lua @@ -984,6 +984,17 @@ local function modScopeChipsWidth(options, gap, m) return need end +local function profileState(imp) + if imp._profileCache == nil then + local LauncherMods = require("src.mods.LauncherMods") + local SaveData = require("src.core.SaveData") + local options = SaveData.loadOptions() + local list, cur = LauncherMods.getProfiles(options) + imp._profileCache = { options = options, list = list, active = cur } + end + return imp._profileCache +end + local function buildModScopeRow(imp, x, y, w, m) local LauncherMods = require("src.mods.LauncherMods") local h = math.max(Kit.tapMin(), math.floor(26 * m.s)) @@ -994,7 +1005,7 @@ local function buildModScopeRow(imp, x, y, w, m) local options = modScopeOptions(imp) -- Dedicated Profile control section (cycle button + gear icon button) on right side of Scope Bar - local _, activeProf = LauncherMods.getProfiles() + local activeProf = profileState(imp).active local isCompact = (w < math.floor(500 * m.s)) local nameText = tostring(activeProf or "Default") local profLabel = isCompact and nameText or Strings("Profile: %s", nameText) @@ -3698,9 +3709,8 @@ local function buildSingleProfileActionsModal(imp, m) if not pName then imp._singleProfileActions = nil return end local LauncherMods = require("src.mods.LauncherMods") - local SaveData = require("src.core.SaveData") - local options = SaveData.loadOptions() - local profiles, active = LauncherMods.getProfiles(options) + local prof = profileState(imp) + local options, profiles = prof.options, prof.list local pad = math.floor(18 * m.s) local w = math.min(math.floor(380 * m.s), m.w - 2 * m.pad) @@ -3715,6 +3725,7 @@ local function buildSingleProfileActionsModal(imp, m) action = function() LauncherMods.duplicateProfile(pName, options) imp._singleProfileActions = nil + if imp._refreshMods then imp:_refreshMods() end end }, { @@ -3736,6 +3747,7 @@ local function buildSingleProfileActionsModal(imp, m) imp:pressDelete("profile", pName, nil, function() LauncherMods.deleteProfile(pName, options) imp._singleProfileActions = nil + if imp._refreshMods then imp:_refreshMods() end end) end } @@ -3771,9 +3783,8 @@ end -- Modal for Mod Profiles (#593) - interactive profile manager (switch, edit, duplicate, delete) local function buildProfilesModal(imp, m) local LauncherMods = require("src.mods.LauncherMods") - local SaveData = require("src.core.SaveData") - local options = SaveData.loadOptions() - local profiles, active = LauncherMods.getProfiles(options) + local prof = profileState(imp) + local options, profiles, active = prof.options, prof.list, prof.active local pad = math.floor(18 * m.s) local w = math.min(math.floor(460 * m.s), m.w - 2 * m.pad) diff --git a/src/import/RomImporter.lua b/src/import/RomImporter.lua index cff73978..c385786b 100644 --- a/src/import/RomImporter.lua +++ b/src/import/RomImporter.lua @@ -4803,6 +4803,7 @@ function RomImporter:_refreshMods() local LauncherMods = require("src.mods.LauncherMods") local SaveData = require("src.core.SaveData") self._cartPlan = nil + self._profileCache = nil self.findInstalled = nil self.safeMode = SaveData.isSafeMode(SaveData.loadOptions()) -- Once per session, ahead of the first listing: pull in any mod the player diff --git a/src/render/ShaderFX.lua b/src/render/ShaderFX.lua index 5862e66d..4a4a7739 100644 --- a/src/render/ShaderFX.lua +++ b/src/render/ShaderFX.lua @@ -264,7 +264,9 @@ ShaderFX.BRIDGE_DIR = "tools/shaderfx-bridge" local function libNames() local osName = (love and love.system and love.system.getOS and love.system.getOS()) or "" - if osName == "Windows" then + if osName == "iOS" then + return {} + elseif osName == "Windows" then return { "librashader_bridge.dll" } elseif osName == "OS X" then return { "liblibrashader_bridge.dylib", "librashader_bridge.dylib" } @@ -313,11 +315,13 @@ end -- Every place the bridge may sit, most specific first. local function libCandidates() + local names = libNames() + if #names == 0 then return {} end local out = {} local override = os.getenv("LIBRASHADER_BRIDGE_DLL") if override and override ~= "" then out[#out + 1] = override end local dirs, save = sourceDirs(), saveDir() - for _, name in ipairs(libNames()) do + for _, name in ipairs(names) do for _, dir in ipairs(dirs) do out[#out + 1] = dir .. "/" .. name out[#out + 1] = dir .. "/" .. ShaderFX.BRIDGE_DIR .. "/target/release/" .. name @@ -353,7 +357,17 @@ local function ensureLib() end tried[#tried + 1] = path end - libError = "librashader bridge not found; looked in " .. table.concat(tried, ", ") + local okSym, sym = pcall(function() + return ffi.C and ffi.C.librashader_translate_preset + end) + if okSym and sym ~= nil then + lib = ffi.C + return lib + end + libError = "librashader bridge not found; ffi.C has no librashader_translate_preset" + if #tried > 0 then + libError = libError .. "; looked in " .. table.concat(tried, ", ") + end return nil, libError end diff --git a/tests/engine/shaderfx_ffi_c_fallback.lua b/tests/engine/shaderfx_ffi_c_fallback.lua new file mode 100644 index 00000000..25d7e49d --- /dev/null +++ b/tests/engine/shaderfx_ffi_c_fallback.lua @@ -0,0 +1,85 @@ +package.path = "./?.lua;./?/init.lua;" .. package.path + +local T = require("tests.harness") + +love = love or require("tests.love_stub") + +local NAME = "src.render.ShaderFX" + +local function withFfi(fakeFfi, os, fn) + local oldModule = package.loaded[NAME] + local oldFfi = package.loaded.ffi + local oldPreload = package.preload.ffi + local oldGetOS = love.system.getOS + if os then love.system.getOS = function() return os end end + package.loaded[NAME] = nil + package.loaded.ffi = nil + package.preload.ffi = function() return fakeFfi end + local ShaderFX = require(NAME) + local can, err = ShaderFX.canConvert(), ShaderFX.bridgeError() + if fn then fn(ShaderFX) end + package.loaded[NAME] = oldModule + package.loaded.ffi = oldFfi + package.preload.ffi = oldPreload + love.system.getOS = oldGetOS + return can, err, ShaderFX +end + +local function baseFfi() + return { + cdef = function() end, + load = function() error("no such library") end, + string = function(value) return value end, + } +end + +local staticFfi = baseFfi() +staticFfi.C = { + librashader_translate_preset = function() return "{}" end, + librashader_free_string = function() end, +} + +local can, err = withFfi(staticFfi) +T.eq(can, true, "a statically linked bridge is found through ffi.C") +T.eq(err, nil, "the ffi.C fallback leaves no bridge error behind") + +local emptyFfi = baseFfi() +emptyFfi.C = setmetatable({}, { + __index = function() error("undefined symbol") end, +}) + +local missing, missingErr = withFfi(emptyFfi) +T.eq(missing, false, "no library and no ffi.C symbol means no conversion") +T.check(missingErr and missingErr:find("ffi.C has no librashader_translate_preset", 1, true) ~= nil, + "libError says ffi.C was probed too (got " .. tostring(missingErr) .. ")") +T.check(missingErr and missingErr:find("looked in", 1, true) ~= nil, + "libError still lists the paths that were tried") + +local iosMissing, iosErr = withFfi(baseFfi(), "iOS") +T.eq(iosMissing, false, "iOS with no static symbol cannot convert") +T.check(iosErr and iosErr:find("ffi.C has no librashader_translate_preset", 1, true) ~= nil, + "iOS libError names the ffi.C probe") +T.check(iosErr and iosErr:find("looked in", 1, true) == nil, + "iOS lists no file candidates (got " .. tostring(iosErr) .. ")") + +local iosCan = withFfi(staticFfi, "iOS") +T.eq(iosCan, true, "iOS resolves the bridge through ffi.C alone") + +local freed = false +local translateFfi = baseFfi() +translateFfi.C = { + librashader_translate_preset = function(path, es) + return ('{"pass_count":1,"passes":[{"name":"%s","es":%d}]}'):format(path, es) + end, + librashader_free_string = function() freed = true end, +} +withFfi(translateFfi, "iOS", function(ShaderFX) + local preset, terr = ShaderFX.translate("/presets/a.slangp", true) + T.check(preset ~= nil, "translate works with lib == ffi.C (" .. tostring(terr) .. ")") + T.eq(preset and preset.passes[1].name, "/presets/a.slangp", + "the ffi.C symbol receives the preset path") + T.eq(preset and preset.passes[1].es, 1, "the es flag reaches the ffi.C symbol") + T.eq(freed, true, "the returned string is freed through ffi.C") +end) + +T.finish("shaderfx ffi.C fallback") diff --git a/tools/shaderfx-bridge/Cargo.toml b/tools/shaderfx-bridge/Cargo.toml index d9671c35..f43f56b9 100644 --- a/tools/shaderfx-bridge/Cargo.toml +++ b/tools/shaderfx-bridge/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" [lib] name = "librashader_bridge" -crate-type = ["cdylib", "rlib"] +crate-type = ["cdylib", "staticlib", "rlib"] [dependencies] librashader-preprocess = "0.12.0"