shaderfix

This commit is contained in:
bryanthaboi
2026-08-25 09:16:59 -04:00
parent 9dd38e06a5
commit d622e83a8e
11 changed files with 445 additions and 16 deletions
+50 -1
View File
@@ -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 and `ShaderFX.bridgeError()` says why not. Activating an already-converted
preset never needs any of this. 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/<abi>/` 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
`<abi>/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 ### Fixup
`ShaderFixup.lua` mechanically rewrites the emitted GLSL into something LOVE `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 `bundle_shader_bridge`, building it with cargo when a prebuilt one is not
supplied through `SHADERFX_BRIDGE`. A build host without cargo produces a supplied through `SHADERFX_BRIDGE`. A build host without cargo produces a
package that can run converted presets but cannot CONVERT new ones, and says 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 - **The buildbot shortlist is a temporary trim.** `KEPT_PRESETS` reflects one
manual pass over `handheld/` and is expected to change, most likely to shrink. 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 - **Tilt direction is unverified.** Which way forward and back rocking moves the
+5
View File
@@ -100,6 +100,11 @@ love-android 11.5a expects:
Set `ANDROID_SDK_ROOT` (or `ANDROID_HOME`), or let the script write Set `ANDROID_SDK_ROOT` (or `ANDROID_HOME`), or let the script write
`local.properties` when it finds `~/Library/Android/sdk`. `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). Gradle flavor used: **`embedNoRecord`** (game fused into the APK, no microphone).
Build task: `assembleEmbedNoRecordDebug`. Build task: `assembleEmbedNoRecordDebug`.
+1
View File
@@ -52,6 +52,7 @@ The script verifies the final app before packaging it:
- the public Documents plist settings are present - the public Documents plist settings are present
- the native picker bridge is 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 - `game.love` exists and is non-empty
If the payload is missing, the build fails instead of producing a blank app. If the payload is missing, the build fails instead of producing a blank app.
+110
View File
@@ -461,6 +461,115 @@ pack_game_love() {
fi 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 # --------------------------------------------------------------- SDK check
require_android_sdk() { require_android_sdk() {
local sdk="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-}}" local sdk="${ANDROID_SDK_ROOT:-${ANDROID_HOME:-}}"
@@ -575,5 +684,6 @@ if $PACKAGE_ONLY; then
fi fi
require_android_sdk require_android_sdk
bundle_shader_bridge_android
run_gradle run_gradle
say "done" say "done"
+148 -2
View File
@@ -615,6 +615,138 @@ verify_game_payload() {
say "game.love present ($(du -h "$app/game.love" | cut -f1))" 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() { run_xcodebuild() {
local config sdk destination local config sdk destination
if $RELEASE; then if $RELEASE; then
@@ -633,6 +765,8 @@ run_xcodebuild() {
mkdir -p "$BUILD_DIR" mkdir -p "$BUILD_DIR"
bundle_shader_bridge_ios "$(rust_targets_for_sdk "$sdk")" "$sdk"
# Prefer -target + SYMROOT over -derivedDataPath: modern Xcode requires # Prefer -target + SYMROOT over -derivedDataPath: modern Xcode requires
# -scheme whenever -derivedDataPath is set, and love-ios ships no shared schemes. # -scheme whenever -derivedDataPath is set, and love-ios ships no shared schemes.
# Always stamp both: the overlay plist expands $(MARKETING_VERSION) / # Always stamp both: the overlay plist expands $(MARKETING_VERSION) /
@@ -661,6 +795,13 @@ run_xcodebuild() {
ONLY_ACTIVE_ARCH=NO ONLY_ACTIVE_ARCH=NO
DISABLE_MANUAL_TARGET_ORDER_BUILD_WARNING=YES 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 if ! $DEVICE; then
# Simulator: ad-hoc signing (no certificate needed). A plain unsigned # Simulator: ad-hoc signing (no certificate needed). A plain unsigned
# build would drop the entitlements file, and HealthKit refuses to run # 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 products="$BUILD_DIR/Build/Products/${config}-${sdk}"
local app="" local app=""
local candidate local candidate newest=0 mtime
for candidate in "$products/$PRODUCT_NAME.app" "$products/$APP_NAME.app" "$products/love.app"; do for candidate in "$products/$PRODUCT_NAME.app" "$products/$APP_NAME.app" "$products/love.app"; do
if [ -d "$candidate" ]; then if [ -d "$candidate" ]; then
mtime="$(stat -f %m "$candidate" 2>/dev/null || echo 0)"
if [ "$mtime" -gt "$newest" ]; then
newest="$mtime"
app="$candidate" app="$candidate"
break fi
fi fi
done done
if [ -z "$app" ]; then if [ -z "$app" ]; then
@@ -736,6 +880,7 @@ run_xcodebuild() {
return 0 return 0
fi fi
if [ "$app" != "$products/$APP_NAME.app" ]; then if [ "$app" != "$products/$APP_NAME.app" ]; then
rm -rf "$products/$APP_NAME.app"
mv "$app" "$products/$APP_NAME.app" mv "$app" "$products/$APP_NAME.app"
app="$products/$APP_NAME.app" app="$products/$APP_NAME.app"
fi fi
@@ -754,6 +899,7 @@ run_xcodebuild() {
verify_game_payload "$app" verify_game_payload "$app"
verify_native_bridge "$app" verify_native_bridge "$app"
verify_shader_bridge "$app"
local dist_dir="$DIST/${config}-${sdk}" local dist_dir="$DIST/${config}-${sdk}"
rm -rf "$dist_dir" rm -rf "$dist_dir"
+8 -1
View File
@@ -76,7 +76,14 @@ local function sdlFfi()
]]) ]])
if sdlCdefOk then if sdlCdefOk then
local okLoad, lib = pcall(ffi.load, "SDL2") 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
end end
if not sdlCdefOk or not sdlLib then return nil end if not sdlCdefOk or not sdlLib then return nil end
+18 -7
View File
@@ -984,6 +984,17 @@ local function modScopeChipsWidth(options, gap, m)
return need return need
end 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 function buildModScopeRow(imp, x, y, w, m)
local LauncherMods = require("src.mods.LauncherMods") local LauncherMods = require("src.mods.LauncherMods")
local h = math.max(Kit.tapMin(), math.floor(26 * m.s)) 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) local options = modScopeOptions(imp)
-- Dedicated Profile control section (cycle button + gear icon button) on right side of Scope Bar -- 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 isCompact = (w < math.floor(500 * m.s))
local nameText = tostring(activeProf or "Default") local nameText = tostring(activeProf or "Default")
local profLabel = isCompact and nameText or Strings("Profile: %s", nameText) 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 if not pName then imp._singleProfileActions = nil return end
local LauncherMods = require("src.mods.LauncherMods") local LauncherMods = require("src.mods.LauncherMods")
local SaveData = require("src.core.SaveData") local prof = profileState(imp)
local options = SaveData.loadOptions() local options, profiles = prof.options, prof.list
local profiles, active = LauncherMods.getProfiles(options)
local pad = math.floor(18 * m.s) local pad = math.floor(18 * m.s)
local w = math.min(math.floor(380 * m.s), m.w - 2 * m.pad) 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() action = function()
LauncherMods.duplicateProfile(pName, options) LauncherMods.duplicateProfile(pName, options)
imp._singleProfileActions = nil imp._singleProfileActions = nil
if imp._refreshMods then imp:_refreshMods() end
end end
}, },
{ {
@@ -3736,6 +3747,7 @@ local function buildSingleProfileActionsModal(imp, m)
imp:pressDelete("profile", pName, nil, function() imp:pressDelete("profile", pName, nil, function()
LauncherMods.deleteProfile(pName, options) LauncherMods.deleteProfile(pName, options)
imp._singleProfileActions = nil imp._singleProfileActions = nil
if imp._refreshMods then imp:_refreshMods() end
end) end)
end end
} }
@@ -3771,9 +3783,8 @@ end
-- Modal for Mod Profiles (#593) - interactive profile manager (switch, edit, duplicate, delete) -- Modal for Mod Profiles (#593) - interactive profile manager (switch, edit, duplicate, delete)
local function buildProfilesModal(imp, m) local function buildProfilesModal(imp, m)
local LauncherMods = require("src.mods.LauncherMods") local LauncherMods = require("src.mods.LauncherMods")
local SaveData = require("src.core.SaveData") local prof = profileState(imp)
local options = SaveData.loadOptions() local options, profiles, active = prof.options, prof.list, prof.active
local profiles, active = LauncherMods.getProfiles(options)
local pad = math.floor(18 * m.s) local pad = math.floor(18 * m.s)
local w = math.min(math.floor(460 * m.s), m.w - 2 * m.pad) local w = math.min(math.floor(460 * m.s), m.w - 2 * m.pad)
+1
View File
@@ -4803,6 +4803,7 @@ function RomImporter:_refreshMods()
local LauncherMods = require("src.mods.LauncherMods") local LauncherMods = require("src.mods.LauncherMods")
local SaveData = require("src.core.SaveData") local SaveData = require("src.core.SaveData")
self._cartPlan = nil self._cartPlan = nil
self._profileCache = nil
self.findInstalled = nil self.findInstalled = nil
self.safeMode = SaveData.isSafeMode(SaveData.loadOptions()) self.safeMode = SaveData.isSafeMode(SaveData.loadOptions())
-- Once per session, ahead of the first listing: pull in any mod the player -- Once per session, ahead of the first listing: pull in any mod the player
+17 -3
View File
@@ -264,7 +264,9 @@ ShaderFX.BRIDGE_DIR = "tools/shaderfx-bridge"
local function libNames() local function libNames()
local osName = (love and love.system and love.system.getOS local osName = (love and love.system and love.system.getOS
and love.system.getOS()) or "" and love.system.getOS()) or ""
if osName == "Windows" then if osName == "iOS" then
return {}
elseif osName == "Windows" then
return { "librashader_bridge.dll" } return { "librashader_bridge.dll" }
elseif osName == "OS X" then elseif osName == "OS X" then
return { "liblibrashader_bridge.dylib", "librashader_bridge.dylib" } return { "liblibrashader_bridge.dylib", "librashader_bridge.dylib" }
@@ -313,11 +315,13 @@ end
-- Every place the bridge may sit, most specific first. -- Every place the bridge may sit, most specific first.
local function libCandidates() local function libCandidates()
local names = libNames()
if #names == 0 then return {} end
local out = {} local out = {}
local override = os.getenv("LIBRASHADER_BRIDGE_DLL") local override = os.getenv("LIBRASHADER_BRIDGE_DLL")
if override and override ~= "" then out[#out + 1] = override end if override and override ~= "" then out[#out + 1] = override end
local dirs, save = sourceDirs(), saveDir() local dirs, save = sourceDirs(), saveDir()
for _, name in ipairs(libNames()) do for _, name in ipairs(names) do
for _, dir in ipairs(dirs) do for _, dir in ipairs(dirs) do
out[#out + 1] = dir .. "/" .. name out[#out + 1] = dir .. "/" .. name
out[#out + 1] = dir .. "/" .. ShaderFX.BRIDGE_DIR .. "/target/release/" .. name out[#out + 1] = dir .. "/" .. ShaderFX.BRIDGE_DIR .. "/target/release/" .. name
@@ -353,7 +357,17 @@ local function ensureLib()
end end
tried[#tried + 1] = path tried[#tried + 1] = path
end 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 return nil, libError
end end
+85
View File
@@ -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")
+1 -1
View File
@@ -5,7 +5,7 @@ edition = "2024"
[lib] [lib]
name = "librashader_bridge" name = "librashader_bridge"
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "staticlib", "rlib"]
[dependencies] [dependencies]
librashader-preprocess = "0.12.0" librashader-preprocess = "0.12.0"