diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8c79351e..eb17e9eb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -173,7 +173,7 @@ jobs: uses: actions/cache@v4 with: path: app/src/main/assets/linuxfs - key: preloads-${{ env.PRELOAD_CACHE_VERSION }}-${{ hashFiles('app/src/main/cpp/fakeinput_steam.cpp', 'tools/linuxfs/preload/*.c', 'tools/linuxfs/build-x86-preloads.sh', 'tools/linuxfs/fex/*', 'tools/linuxfs/clipboard/*.c', 'tools/proot/fastpath/*.c', 'tools/linuxfs/overlay/usr/local/bin/*', 'tools/linuxfs/overlay/usr/bin/**', 'tools/linuxfs/desktop/*', 'tools/gamescope/release.env', 'tools/wlroots/release.env', 'tools/linuxfs/uruntime.env', 'tools/linuxfs/licenses/*', 'tools/mangoapp/*') }} + key: preloads-${{ env.PRELOAD_CACHE_VERSION }}-${{ hashFiles('app/src/main/cpp/fakeinput_steam.cpp', 'tools/linuxfs/preload/*.c', 'tools/linuxfs/build-x86-preloads.sh', 'tools/linuxfs/fex/*', 'tools/linuxfs/clipboard/*.c', 'tools/linuxfs/ssbs/*.c', 'tools/proot/fastpath/*.c', 'tools/linuxfs/overlay/usr/local/bin/*', 'tools/linuxfs/overlay/usr/bin/**', 'tools/linuxfs/desktop/*', 'tools/gamescope/release.env', 'tools/wlroots/release.env', 'tools/linuxfs/uruntime.env', 'tools/linuxfs/licenses/*', 'tools/mangoapp/*') }} - name: Build libfakeinput.so and libblsession.so (glibc aarch64) if: steps.preloads.outputs.cache-hit != 'true' @@ -195,6 +195,10 @@ jobs: aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -pthread \ -o "$D/libblfastpath.so" tools/proot/fastpath/fastpath.c -ldl aarch64-linux-gnu-strip --strip-unneeded "$D/libblfastpath.so" + # Force-SSBS adapter, preloaded into a game's Wine only when the game asks for it. + aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -pthread \ + -o "$D/libssbs.so" tools/linuxfs/ssbs/ssbs_adapter.c -ldl + aarch64-linux-gnu-strip --strip-unneeded "$D/libssbs.so" mkdir -p "$D/usr/local/bin" aarch64-linux-gnu-gcc -O2 -Wall -Wextra -o "$D/usr/local/bin/droiddeck-clipboard" tools/linuxfs/clipboard/clipboard.c -ldl aarch64-linux-gnu-strip --strip-unneeded "$D/usr/local/bin/droiddeck-clipboard" @@ -362,6 +366,10 @@ jobs: for sym in socket bind getsockname setsockopt statfs statvfs syscall shm_open shm_unlink mmap mmap64 munmap drmPrimeFDToHandle drmCloseBufferHandle drmIoctl; do grep -qx "$sym" <<<"$SESSION" || { echo "::error::libblsession.so does not export $sym"; exit 1; } done + SSBS=$(syms "$D/libssbs.so") + for sym in sigaction signal bsd_signal sysv_signal; do + grep -qx "$sym" <<<"$SSBS" || { echo "::error::libssbs.so does not export $sym"; exit 1; } + done test -f "$D/usr/local/bin/droiddeck-session" test -f "$D/usr/local/bin/droiddeck-proton-extra" diff --git a/app/src/main/java/com/droiddeck/launcher/MainActivity.kt b/app/src/main/java/com/droiddeck/launcher/MainActivity.kt index 9c743938..733bc16a 100644 --- a/app/src/main/java/com/droiddeck/launcher/MainActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/MainActivity.kt @@ -311,6 +311,7 @@ class MainActivity : ComponentActivity() { private var settingsMode by mutableStateOf(null) private var resolution by mutableStateOf(com.droiddeck.launcher.session.SessionDisplay.DEFAULT_RESOLUTION) private var fexPreset by mutableStateOf("") + private var forceSsbs by mutableStateOf(false) private var steamChannel by mutableStateOf("steamdeck_publicbeta") private var runSteamAtStartup by mutableStateOf(false) private var theme by mutableStateOf("graphite") @@ -1159,6 +1160,7 @@ class MainActivity : ComponentActivity() { storageOptions = storageOptions, storageDiagnostics = mode == SessionService.MODE_STEAM && storageDiagnostics, fexPreset = if (mode == SessionService.MODE_STEAM) fexPreset else null, + forceSsbs = forceSsbs, syncBackend = if (mode == SessionService.MODE_STEAM) SessionPrefs.syncBackendOf(fastSync, fsyncFirst, syncFallback) else null, steamChannel = if (mode == SessionService.MODE_STEAM) steamChannel else null, steamDeckMode = mode == SessionService.MODE_STEAM && steamDeckMode, @@ -1232,6 +1234,7 @@ class MainActivity : ComponentActivity() { storageDiagnostics = on }, onFexPreset = { id -> SessionPrefs.setFexPreset(this, id); fexPreset = id }, + onForceSsbs = { on -> SessionPrefs.setForceSsbs(this, on); forceSsbs = on }, onSyncBackend = { id -> SessionPrefs.setSyncBackend(this, id) fastSync = SessionPrefs.fastSync(this) @@ -1353,6 +1356,7 @@ class MainActivity : ComponentActivity() { showMapping = false resolution = SessionPrefs.resolutionChoice(this, mode, com.droiddeck.launcher.session.SessionDisplay.panelSize(this)) fexPreset = SessionPrefs.fexPreset(this) + forceSsbs = SessionPrefs.forceSsbs(this) fastSync = SessionPrefs.fastSync(this) fsyncFirst = SessionPrefs.fsyncFirst(this) syncFallback = SessionPrefs.syncFallback(this) diff --git a/app/src/main/java/com/droiddeck/launcher/core/GameEnvironment.kt b/app/src/main/java/com/droiddeck/launcher/core/GameEnvironment.kt index 645898e6..a03f31d9 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/GameEnvironment.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/GameEnvironment.kt @@ -25,7 +25,7 @@ object GameEnvironment { fun validValue(value: String) = '\u0000' !in value && value.length <= 8192 fun validScope(scope: String) = scope.isEmpty() || (Regex("[1-9][0-9]*").matches(scope) && scope.toLongOrNull()?.let { it in 1..4294967295L } == true) - fun defaults(preset: String): Map = linkedMapOf( + fun defaults(preset: String, forceSsbs: Boolean = false): Map = linkedMapOf( "MESA_SHADER_CACHE_DISABLE" to "false", "VKD3D_FEATURE_LEVEL" to "12_2", "VKD3D_SHADER_MODEL" to "6_9", @@ -37,8 +37,12 @@ object GameEnvironment { "DOTNET_EnableWriteXorExecute" to "0", ).apply { FexPreset.env(preset).forEach { put(it.substringBefore('='), it.substringAfter('=')) } + // Steam settings' Force SSBS: droiddeck-game-env preloads libssbs.so into the game's Wine. + if (forceSsbs) put(FORCE_SSBS, "1") } - fun effective(config: Config, preset: String, scope: String): Map = - defaults(preset) + config.shared + if (scope.isEmpty()) emptyMap() else config.games[scope].orEmpty() + const val FORCE_SSBS = "DROIDDECK_FORCE_SSBS" + + fun effective(config: Config, preset: String, scope: String, forceSsbs: Boolean = false): Map = + defaults(preset, forceSsbs) + config.shared + if (scope.isEmpty()) emptyMap() else config.games[scope].orEmpty() } diff --git a/app/src/main/java/com/droiddeck/launcher/session/GameEnvironmentStore.kt b/app/src/main/java/com/droiddeck/launcher/session/GameEnvironmentStore.kt index e69fa1b6..affc00dc 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/GameEnvironmentStore.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/GameEnvironmentStore.kt @@ -59,7 +59,7 @@ object GameEnvironmentStore { @Synchronized fun publish(context: Context, config: GameEnvironment.Config = read(context)) { - val resolved = config.copy(shared = GameEnvironment.defaults(SessionPrefs.fexPreset(context)) + config.shared) + val resolved = config.copy(shared = GameEnvironment.defaults(SessionPrefs.fexPreset(context), SessionPrefs.forceSsbs(context)) + config.shared) val json = encode(resolved) // The Display page's texture filtering, as DXVK options the launcher appends to // DXVK_CONFIG after the profiles - a user's own DXVK_CONFIG entry keeps its options. diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionFiles.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionFiles.kt index 1a0d935c..d666d1a6 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionFiles.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionFiles.kt @@ -36,6 +36,7 @@ object SessionFiles { "libblsession.so" to "usr/local/lib/libblsession.so", "libfakeinput.so" to "usr/local/lib/libfakeinput.so", "libblfastpath.so" to "usr/local/lib/libblfastpath.so", + "libssbs.so" to "usr/local/lib/libssbs.so", "usr/local/bin/droiddeck-session" to "usr/local/bin/droiddeck-session", "usr/local/bin/steam-compatibility" to "usr/local/bin/steam-compatibility", "usr/local/bin/droiddeck-clipboard" to "usr/local/bin/droiddeck-clipboard", diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt index 2c914cc2..8f9e34e9 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt @@ -543,6 +543,22 @@ object SessionPrefs { private const val DEFAULT_FEX_PRESET = "PERFORMANCE_TSO" + /** + * Force SSBS for Proton games: Wine resumes threads from a Windows CONTEXT that never carries + * PSTATE.SSBS, so they run with speculative store bypass disabled; libssbs.so keeps it set + * (on DiRT 3 / GE-Proton: from ~99% of a game's threads running without it to none). The + * speed-up is reported on Oryon cores (Snapdragon 8 Elite) and was not measurable on an + * 8 Gen 3, so it is off unless turned on. A game's own environment can still say + * DROIDDECK_FORCE_SSBS=0. + */ + fun forceSsbs(context: Context): Boolean = prefs(context).getBoolean("forceSsbs", false) + + fun setForceSsbs(context: Context, on: Boolean) { + prefs(context).edit().putBoolean("forceSsbs", on).apply() + runCatching { GameEnvironmentStore.publish(context) } + .onFailure { android.util.Log.e("GameEnvironment", "Could not update game environment", it) } + } + fun setFexPreset(context: Context, id: String) { prefs(context).edit().putString("fexPreset", id).apply() runCatching { GameEnvironmentStore.publish(context) } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/GameEnvironmentEditor.kt b/app/src/main/java/com/droiddeck/launcher/ui/GameEnvironmentEditor.kt index 8dbaff28..db4f6f0f 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/GameEnvironmentEditor.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/GameEnvironmentEditor.kt @@ -95,6 +95,7 @@ private fun GameEnvironmentEditor(byPad: Boolean, onClose: () -> Unit) { var error by remember { mutableStateOf(false) } var editing by remember { mutableStateOf?>(null) } val preset = SessionPrefs.fexPreset(context) + val forceSsbs = SessionPrefs.forceSsbs(context) val firstFocus = remember { FocusRequester() } var editByPad by remember { mutableStateOf(false) } val dialogInput = LocalInputModeManager.current @@ -159,7 +160,7 @@ private fun GameEnvironmentEditor(byPad: Boolean, onClose: () -> Unit) { if (current == null && !error) Small(stringResource(R.string.game_env_loading)) if (current != null) { val own = current.entries(scope) - val entries = GameEnvironment.effective(current, preset, scope).toSortedMap() + val entries = GameEnvironment.effective(current, preset, scope, forceSsbs).toSortedMap() if (entries.isEmpty()) Small(stringResource(R.string.game_env_empty)) else Panel { entries.entries.forEachIndexed { i, (name, value) -> diff --git a/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt b/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt index 91b54406..25efcb37 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt @@ -70,6 +70,8 @@ class ModeSettings( /** Steam only: record allocation and sampled storage timings in the next session's Share logs. */ val storageDiagnostics: Boolean = false, val fexPreset: String? = null, + /** Steam only: Force SSBS for Proton games (SessionPrefs.forceSsbs). */ + val forceSsbs: Boolean = false, /** Steam only: SessionPrefs.SYNC_* chosen for Proton games; null outside Steam. */ val syncBackend: String? = null, /** Steam only: forces game windows fullscreen (null = not a Steam page). */ @@ -131,6 +133,7 @@ class ModeSettingsActions( val onPickGameStorageFolder: () -> Unit = {}, val onStorageDiagnostics: (Boolean) -> Unit = {}, val onFexPreset: (String) -> Unit = {}, + val onForceSsbs: (Boolean) -> Unit = {}, val onSyncBackend: (String) -> Unit = {}, val onForceFullscreen: (Boolean) -> Unit = {}, val onStretch16x9: (Boolean) -> Unit = {}, @@ -459,6 +462,7 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) { FexPreset.all.map { it.id to stringResource(it.label) }, s.fexPreset, note = stringResource(FexPreset.byId(s.fexPreset).detail), onPick = a.onFexPreset, ) + ToggleRow(host, "ssbs", stringResource(R.string.force_ssbs_title), stringResource(R.string.force_ssbs_hint), s.forceSsbs, onChange = a.onForceSsbs) GameEnvironmentRow() } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ffd72b7f..539e6ba9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1052,4 +1052,6 @@ Copy Copy these details into a bug report. Copied. + Force SSBS + Keeps a CPU speed setting on that Wine keeps turning off. Can make Windows games faster on Snapdragon 8 Elite; little change seen on older chips. Applies on the next game launch. diff --git a/app/src/test/java/com/droiddeck/launcher/core/GameEnvironmentTest.kt b/app/src/test/java/com/droiddeck/launcher/core/GameEnvironmentTest.kt index ee49f5a7..aacfcfb2 100644 --- a/app/src/test/java/com/droiddeck/launcher/core/GameEnvironmentTest.kt +++ b/app/src/test/java/com/droiddeck/launcher/core/GameEnvironmentTest.kt @@ -11,6 +11,14 @@ class GameEnvironmentTest { assertEquals("6_9", values["VKD3D_SHADER_MODEL"]) } + @Test fun forceSsbsIsADefaultAGameCanTurnOff() { + assertFalse(GameEnvironment.defaults("").containsKey(GameEnvironment.FORCE_SSBS)) + assertEquals("1", GameEnvironment.defaults("", forceSsbs = true)[GameEnvironment.FORCE_SSBS]) + val config = GameEnvironment.Config().withEntries("42", mapOf(GameEnvironment.FORCE_SSBS to "0")) + assertEquals("0", GameEnvironment.effective(config, "", "42", forceSsbs = true)[GameEnvironment.FORCE_SSBS]) + assertEquals("1", GameEnvironment.effective(config, "", "43", forceSsbs = true)[GameEnvironment.FORCE_SSBS]) + } + @Test fun gameOverridesSharedAndPresetWithoutLeakingToOtherGames() { val config = GameEnvironment.Config( shared = mapOf("FEX_MULTIBLOCK" to "0", "VKD3D_FEATURE_LEVEL" to "12_2"), diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env b/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env index 19c88873..529b5254 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-game-env @@ -196,6 +196,30 @@ def without_pack(chosen_env, env): return result +SSBS_LIB = "/usr/local/lib/libssbs.so" + + +def force_ssbs(command, env): + """DROIDDECK_FORCE_SSBS=1 (a game's or the shared environment): preload libssbs.so into this + Proton's Wine, which then keeps PSTATE.SSBS set across signal returns - speculative store + bypass stays allowed, as force_ssbs.diff does inside a rebuilt ntdll. ARM64 Protons only: the + library is aarch64 and only arm64 Wine has the signal path it fixes.""" + if env.get("DROIDDECK_FORCE_SSBS", "").lower() not in ("1", "on", "true", "yes"): + return env + proton = os.path.dirname(command[0]) if command else "" + if not os.path.isdir(os.path.join(proton, "files/lib/wine/aarch64-unix")): + print("droiddeck-game-env: force SSBS asked for, but this Proton is not an ARM64 one; left off", file=sys.stderr) + return env + if not os.path.isfile(SSBS_LIB): + print("droiddeck-game-env: force SSBS asked for, but %s is missing; left off" % SSBS_LIB, file=sys.stderr) + return env + parts = [p for p in re.split(r"[:\s]+", env.get("LD_PRELOAD", "")) if p] + if SSBS_LIB not in parts: + env = dict(env, LD_PRELOAD=":".join(parts + [SSBS_LIB])) + print("droiddeck-game-env: force SSBS on (libssbs.so preloaded)", file=sys.stderr) + return env + + def main(): command = sys.argv[1:] if not command: @@ -223,6 +247,7 @@ def main(): env = apply_config(env, {"version": 1}, app_id, fixes) except (OSError, ValueError, TypeError, AttributeError): print("droiddeck-game-env: invalid configuration; using inherited environment", file=sys.stderr) + env = force_ssbs(command, env) sync = load_sync() launch, launch_env = select_sync(command, env, sync) if launch[0] != command[0]: diff --git a/tools/linuxfs/ssbs/ssbs_adapter.c b/tools/linuxfs/ssbs/ssbs_adapter.c new file mode 100644 index 00000000..92b3000f --- /dev/null +++ b/tools/linuxfs/ssbs/ssbs_adapter.c @@ -0,0 +1,180 @@ +/* + * libssbs.so - keep PSTATE.SSBS set across signal returns, without rebuilding Wine. + * + * Wine's arm64 signal code writes PSTATE back from a Windows CONTEXT, whose Cpsr never carries + * SSBS (bit 12). Every signal return through that code (exception fixups, NtSetContextThread on + * another thread, the SIGUSR2 suspend path) therefore resumes the thread with SSBS clear, so the + * CPU runs it with speculative store bypass disabled - slower on cores such as Oryon. + * force_ssbs.diff (Sonicadvance1, FEX) fixes this inside ntdll; this library does the same from + * outside, for Protons that cannot be rebuilt: the Valve, GE and CachyOS ARM64 builds DroidDeck + * runs, which come prebuilt from Steam. Same source as Bannerlator's adapter (bionic), built here + * for glibc. + * + * How: interpose sigaction()/signal() via LD_PRELOAD. Every handler is installed behind one + * SA_SIGINFO trampoline that calls the real handler and then sets SSBS in the ucontext it is + * about to return through. The kernel accepts SSBS from userspace on sigreturn and context- + * switches it per thread, so the bit sticks until the next sigreturn. Handlers that never return + * (siglongjmp) are unaffected either way. + * + * Env: + * SSBS_ADAPTER_DISABLE=1 pass everything through untouched. + * SSBS_ADAPTER_OBSERVE=1 wrap and count, but leave SSBS as the handler left it (the baseline). + * SSBS_ADAPTER_DEBUG=1 log to stderr (implied by OBSERVE): + * - "returned with SSBS clear": a handler was about to resume a thread without SSBS. + * - "arrived with SSBS clear": a thread was RUNNING without SSBS when a signal reached it. + * Under the adapter this should stop after each thread's first fixed return; if it keeps + * counting, something other than a signal return is clearing the bit. + */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include + +#define PSTATE_SSBS 0x1000ULL +#define MAX_SIG 65 + +typedef void (*sigact_fn)(int, siginfo_t *, void *); +typedef void (*sighand_fn)(int); +typedef int (*sigaction_fn)(int, const struct sigaction *, struct sigaction *); + +/* Per signal: the handler the program asked for, and whether it wanted SA_SIGINFO. */ +static _Atomic(void *) real_handler[MAX_SIG]; +static atomic_bool real_siginfo[MAX_SIG]; + +static sigaction_fn next_sigaction; +static int disabled; +static int observe; +static int debug; +static atomic_ulong n_returns, n_fixed, n_arrived, n_arrived_clear; + +static void init(void) +{ + if (next_sigaction) return; + next_sigaction = (sigaction_fn)dlsym(RTLD_NEXT, "sigaction"); + const char *e = getenv("SSBS_ADAPTER_DISABLE"); + disabled = e && *e == '1'; + e = getenv("SSBS_ADAPTER_OBSERVE"); + observe = e && *e == '1'; + e = getenv("SSBS_ADAPTER_DEBUG"); + debug = observe || (e && *e == '1'); +} + +__attribute__((constructor)) static void ctor(void) +{ + init(); + if (debug) fprintf(stderr, "libssbs: loaded (%s)\n", disabled ? "disabled" : observe ? "observing only" : "active"); +} + +/* 1, 2, 4, 8, ...: enough lines to see a trend without flooding the log. */ +static int milestone(unsigned long n) { return (n & (n - 1)) == 0; } + +static void trampoline(int sig, siginfo_t *info, void *uc_v) +{ + ucontext_t *uc = uc_v; + if (debug) { + unsigned long a = atomic_fetch_add(&n_arrived, 1) + 1; + if (!(uc->uc_mcontext.pstate & PSTATE_SSBS)) { + unsigned long c = atomic_fetch_add(&n_arrived_clear, 1) + 1; + if (milestone(c)) + fprintf(stderr, "libssbs: sig %d arrived with SSBS clear (%lu of %lu arrivals)\n", sig, c, a); + } + } + void *h = atomic_load_explicit(&real_handler[sig], memory_order_acquire); + if (h) { + if (atomic_load_explicit(&real_siginfo[sig], memory_order_relaxed)) + ((sigact_fn)h)(sig, info, uc_v); + else + ((sighand_fn)h)(sig); + } + if (debug) { + unsigned long r = atomic_fetch_add(&n_returns, 1) + 1; + if (!(uc->uc_mcontext.pstate & PSTATE_SSBS)) { + unsigned long f = atomic_fetch_add(&n_fixed, 1) + 1; + if (milestone(f)) + fprintf(stderr, "libssbs: sig %d returned with SSBS clear, %s (%lu of %lu returns)\n", + sig, observe ? "left as is" : "set it", f, r); + } + } + if (!observe) uc->uc_mcontext.pstate |= PSTATE_SSBS; +} + +static int wrapped(void *h) +{ + return h != (void *)SIG_DFL && h != (void *)SIG_IGN && h != (void *)SIG_ERR; +} + +int sigaction(int sig, const struct sigaction *act, struct sigaction *oldact) +{ + init(); + if (disabled || sig <= 0 || sig >= MAX_SIG) + return next_sigaction(sig, act, oldact); + + struct sigaction mine; + const struct sigaction *pass = act; + void *new_h = NULL; + int new_info = 0; + if (act) { + new_info = (act->sa_flags & SA_SIGINFO) != 0; + new_h = new_info ? (void *)act->sa_sigaction : (void *)act->sa_handler; + if (wrapped(new_h)) { + mine = *act; + mine.sa_flags |= SA_SIGINFO; + mine.sa_sigaction = trampoline; + pass = &mine; + } + } + + void *prev_h = atomic_load(&real_handler[sig]); + int prev_info = atomic_load(&real_siginfo[sig]); + /* Publish before installing so the trampoline never runs with a stale handler. */ + if (act && wrapped(new_h)) { + atomic_store(&real_siginfo[sig], new_info); + atomic_store_explicit(&real_handler[sig], new_h, memory_order_release); + } + + int ret = next_sigaction(sig, pass, oldact); + if (ret != 0 && act && wrapped(new_h)) { + atomic_store(&real_siginfo[sig], prev_info); + atomic_store(&real_handler[sig], prev_h); + } + + /* Report the caller's own handler, not the trampoline, so handler chaining keeps working. */ + if (ret == 0 && oldact && oldact->sa_sigaction == trampoline && prev_h) { + if (prev_info) { + oldact->sa_sigaction = (sigact_fn)prev_h; + } else { + oldact->sa_flags &= ~SA_SIGINFO; + oldact->sa_handler = (sighand_fn)prev_h; + } + } + return ret; +} + +sighandler_t signal(int sig, sighandler_t handler) +{ + struct sigaction sa, old; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = handler; + sa.sa_flags = SA_RESTART; + sigemptyset(&sa.sa_mask); + if (sigaction(sig, &sa, &old) != 0) return SIG_ERR; + return (old.sa_flags & SA_SIGINFO) ? (sighandler_t)(void *)old.sa_sigaction : old.sa_handler; +} + +sighandler_t bsd_signal(int sig, sighandler_t handler) { return signal(sig, handler); } + +/* glibc's System V flavour: a one-shot handler with no restart. */ +sighandler_t sysv_signal(int sig, sighandler_t handler) +{ + struct sigaction sa, old; + memset(&sa, 0, sizeof(sa)); + sa.sa_handler = handler; + sa.sa_flags = SA_RESETHAND | SA_NODEFER; + sigemptyset(&sa.sa_mask); + if (sigaction(sig, &sa, &old) != 0) return SIG_ERR; + return (old.sa_flags & SA_SIGINFO) ? (sighandler_t)(void *)old.sa_sigaction : old.sa_handler; +} diff --git a/tools/tests/test_game_environment.py b/tools/tests/test_game_environment.py index a7627ee9..5d1826a3 100644 --- a/tools/tests/test_game_environment.py +++ b/tools/tests/test_game_environment.py @@ -461,3 +461,27 @@ class DirectAudioPrefixTest(unittest.TestCase): if __name__ == "__main__": unittest.main() + + +class ForceSsbsTest(unittest.TestCase): + def test_libssbs_is_preloaded_only_when_asked_and_only_into_arm64_proton(self): + with tempfile.TemporaryDirectory() as tmp: + arm = Path(tmp) / "arm" + (arm / "files/lib/wine/aarch64-unix").mkdir(parents=True) + x86 = Path(tmp) / "x86" + (x86 / "files/lib/wine/x86_64-unix").mkdir(parents=True) + lib = Path(tmp) / "libssbs.so" + lib.write_bytes(b"\x7fELF") + g = MODULE["force_ssbs"].__globals__ + old = g["SSBS_LIB"] + g["SSBS_LIB"] = str(lib) + try: + env = {"LD_PRELOAD": "/usr/local/lib/libblsession.so"} + self.assertEqual(MODULE["force_ssbs"]([str(arm / "proton"), "waitforexitandrun"], env), env) + on = dict(env, DROIDDECK_FORCE_SSBS="1") + got = MODULE["force_ssbs"]([str(arm / "proton"), "waitforexitandrun"], on) + self.assertEqual(got["LD_PRELOAD"], "/usr/local/lib/libblsession.so:" + str(lib)) + self.assertEqual(MODULE["force_ssbs"]([str(arm / "proton")], got)["LD_PRELOAD"], got["LD_PRELOAD"]) + self.assertEqual(MODULE["force_ssbs"]([str(x86 / "proton")], on), on) + finally: + g["SSBS_LIB"] = old