mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
Optional x86-64 Steam client under FEX (VAC-secured servers)
Valve's 64-bit Linux Steam client (SteamRT3, steamrt64/steam) runs under FEX inside the guest as a second choice next to the arm64 client, which VAC-secured servers refuse. Opt-in: Steam settings > Client > Steam client (ARM64 default); arm64-only session steps are guarded. - FEX 2609 for the guest (build-fex.yml, release fex-2609-r3): openat2 ENOSYS fallback for proot, host thunks against an Arch x86_64/multilib sysroot so games' GL/Vulkan run on the guest's Mesa (Zink, Turnip). - bannerlator-steam-x64: FEX, Ubuntu 24.04 x86 rootfs and the client; own home and library; SteamRT library gaps; no container (recursion guard, check-requirements shim). The client draws in software, games get the GPU (game-gpu.env, game-gpu.local). - "DroidDeck direct" compatibility tool (scout runtime, no container) and a stand-in steam-runtime-launch-client. - Session: BL_STEAM_CLIENT_ARCH=x86_64, steam:// hook, no Deck flags (PulseAudio 13 assert in the client's audio manager). - x86-64 + i386 preload shims (SysV IPC, tracer hiding scoped to libtier0_s, x86-64 libfakeinput only - the i386 one crashes SDL2). - Fast path stands down for 32-bit FEX programs only. - Lossless.dll also taken from the x86 client's library. - tools/diag: SIGSEGV logger for x86 guest programs. Device: Left 4 Dead 2 (native Linux) on an official VAC=1 Valve server, 51 fps on the GPU, controller through Steam Input.
This commit is contained in:
1 parent
7b20b70c6b
commit
c8f4441220
29 files changed
+1356
-42
No files matched your search
@@ -0,0 +1,25 @@
|
||||
name: Build diagnostics
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- 'tools/diag/**'
|
||||
- '.github/workflows/build-diag.yml'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build
|
||||
run: |
|
||||
sudo apt-get update -qq && sudo apt-get install -y -qq gcc-multilib >/dev/null
|
||||
mkdir -p out
|
||||
gcc -m32 -O1 -fPIC -shared -fno-omit-frame-pointer -o out/libsegvlog-i386.so tools/diag/segvlog.c -ldl
|
||||
gcc -O1 -fPIC -shared -fno-omit-frame-pointer -o out/libsegvlog-x86_64.so tools/diag/segvlog.c -ldl
|
||||
file out/*
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: diag
|
||||
path: out/
|
||||
@@ -0,0 +1,57 @@
|
||||
name: Build FEX (x86-64 Steam client experiment)
|
||||
|
||||
# FEX built for the runtime in an Arch Linux ARM container on an arm64 runner (tools/fex), for the
|
||||
# opt-in x86-64 Steam client (bannerlator-steam-x64). Published as a release asset that script
|
||||
# fetches; the runtime image and the default arm64 client are untouched.
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'tools/fex/**'
|
||||
- '.github/workflows/build-fex.yml'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to publish as (e.g. fex-2609-r1); empty = artifact only'
|
||||
required: false
|
||||
default: ''
|
||||
thunks:
|
||||
description: 'Build the host thunks (Vulkan/GL/X11/... to the runtime arm64 libraries)'
|
||||
required: false
|
||||
default: 'false'
|
||||
cxxflags:
|
||||
description: 'Extra CXXFLAGS for a diagnostic build (e.g. -DDEBUG_STRACE); never published with a tag'
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-24.04-arm
|
||||
timeout-minutes: 120
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build in Arch Linux ARM
|
||||
run: |
|
||||
set -euo pipefail
|
||||
docker run --rm -e FEX_EXTRA_CXXFLAGS="${{ inputs.cxxflags }}" -e FEX_THUNKS="${{ inputs.thunks == 'true' && '1' || '0' }}" -v "$PWD:/work" -w /work menci/archlinuxarm:base-devel bash tools/fex/build-in-arch.sh
|
||||
sudo chown -R "$(id -u):$(id -g)" .
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: fex
|
||||
path: |
|
||||
fex.tzst
|
||||
fex.tzst.sha256
|
||||
fex-needed.txt
|
||||
fex-files.txt
|
||||
- name: Publish
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.tag != '' && inputs.cxxflags == ''
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gh release create "${{ inputs.tag }}" --latest=false --prerelease=false --title "${{ inputs.tag }}" --notes "FEX for the runtime (tools/fex/build-in-arch.sh); fetched by bannerlator-steam-x64. sha256 $(cut -d' ' -f1 fex.tzst.sha256)" fex.tzst fex.tzst.sha256 || gh release upload "${{ inputs.tag }}" fex.tzst fex.tzst.sha256 --clobber
|
||||
# A component the apk fetches by tag, never something users install: it must not
|
||||
# become the repo's Latest release or appear as a pre-release, also when re-uploaded.
|
||||
gh release edit "${{ inputs.tag }}" --latest=false --prerelease=false
|
||||
@@ -28,7 +28,7 @@ concurrency:
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
env:
|
||||
PRELOAD_CACHE_VERSION: v2
|
||||
PRELOAD_CACHE_VERSION: v5
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -176,6 +176,17 @@ jobs:
|
||||
aarch64-linux-gnu-gcc -shared -fPIC -O2 -Wall -pthread \
|
||||
-o "$D/libblsession.so" tools/linuxfs/preload/*.c -ldl
|
||||
aarch64-linux-gnu-strip --strip-unneeded "$D/libblsession.so"
|
||||
# The same session shim for x86-64 programs under FEX (bannerlator-steam-x64): the x86
|
||||
# Steam client needs its System V semaphores just as the arm64 one does. Native gcc on
|
||||
# this runner; Ubuntu 24.04's glibc is the one in FEX's Ubuntu 24.04 rootfs.
|
||||
gcc -shared -fPIC -O2 -Wall -pthread \
|
||||
-o "$D/libblsession-x86_64.so" tools/linuxfs/preload/*.c -ldl
|
||||
strip --strip-unneeded "$D/libblsession-x86_64.so"
|
||||
# And the controller reader: the session's pads reach x86-64 programs the same way.
|
||||
g++ -shared -fPIC -O2 -Wall -Wno-attributes -Wno-nonnull-compare \
|
||||
-pthread -std=c++17 -static-libstdc++ -static-libgcc -Wl,--exclude-libs,ALL \
|
||||
-o "$D/libfakeinput-x86_64.so" app/src/main/cpp/fakeinput_steam.cpp -ldl
|
||||
strip --strip-unneeded "$D/libfakeinput-x86_64.so"
|
||||
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"
|
||||
@@ -232,7 +243,15 @@ jobs:
|
||||
install -Dm644 tools/linuxfs/desktop/panel.conf "$D/etc/xdg/lxqt/panel.conf"
|
||||
install -Dm644 tools/linuxfs/desktop/firefox-droiddeck.js "$D/usr/lib/firefox/defaults/pref/droiddeck.js"
|
||||
ls -lR "$D"
|
||||
|
||||
# And for 32-bit x86 programs (Left 4 Dead 2 and the other Source games are i386). Last:
|
||||
# gcc-multilib conflicts with the aarch64 cross compiler and removes it.
|
||||
sudo apt-get install -y -qq gcc-multilib g++-multilib >/dev/null
|
||||
gcc -m32 -shared -fPIC -O2 -Wall -pthread \
|
||||
-o "$D/libblsession-i386.so" tools/linuxfs/preload/*.c -ldl
|
||||
g++ -m32 -shared -fPIC -O2 -Wall -Wno-attributes -Wno-nonnull-compare \
|
||||
-pthread -std=c++17 -static-libstdc++ -static-libgcc -Wl,--exclude-libs,ALL \
|
||||
-o "$D/libfakeinput-i386.so" app/src/main/cpp/fakeinput_steam.cpp -ldl
|
||||
strip --strip-unneeded "$D/libblsession-i386.so" "$D/libfakeinput-i386.so"
|
||||
- name: Cache PulseAudio 13.0 source
|
||||
id: pa-src
|
||||
uses: actions/cache@v4
|
||||
|
||||
@@ -299,6 +299,7 @@ class MainActivity : ComponentActivity() {
|
||||
private var customResolution by mutableStateOf<Pair<Int, Int>?>(null)
|
||||
private var fexPreset by mutableStateOf("")
|
||||
private var steamChannel by mutableStateOf("publicbeta")
|
||||
private var steamClientArch by mutableStateOf("arm64")
|
||||
private var runSteamAtStartup by mutableStateOf(false)
|
||||
private var theme by mutableStateOf("graphite")
|
||||
private var shapeMode by mutableStateOf(SessionPrefs.SHAPE_AUTO)
|
||||
@@ -320,6 +321,7 @@ class MainActivity : ComponentActivity() {
|
||||
private var emulatorList by mutableStateOf<List<Library.Emulator>>(emptyList())
|
||||
private var runningLabel by mutableStateOf<String?>(null)
|
||||
private var logsEnabled by mutableStateOf(true)
|
||||
private var vacDiagnostics by mutableStateOf(false)
|
||||
private var showRoms by mutableStateOf(false)
|
||||
private var homeAppSelected by mutableStateOf(false)
|
||||
private var homeScreenEnabled by mutableStateOf(false)
|
||||
@@ -404,7 +406,7 @@ class MainActivity : ComponentActivity() {
|
||||
busy = busy, stage = stage, percent = percent,
|
||||
desktopInstalled = desktopInstalled,
|
||||
offlineAccount = offlineAccount, offline = offline,
|
||||
frameGenLabel = frameGenLabel, romsDir = romsDir, logsEnabled = logsEnabled,
|
||||
frameGenLabel = frameGenLabel, romsDir = romsDir, logsEnabled = logsEnabled, vacDiagnostics = vacDiagnostics,
|
||||
steamGames = steamGames, emulators = emulatorList, running = runningLabel,
|
||||
frameGen = FrameGen.mode(this),
|
||||
lossless = lossless,
|
||||
@@ -502,6 +504,10 @@ class MainActivity : ComponentActivity() {
|
||||
SessionPrefs.setLogsEnabled(this, !SessionPrefs.logsEnabled(this))
|
||||
logsEnabled = SessionPrefs.logsEnabled(this)
|
||||
},
|
||||
onVacDiagnostics = {
|
||||
SessionPrefs.setVacDiagnostics(this, !SessionPrefs.vacDiagnostics(this))
|
||||
vacDiagnostics = SessionPrefs.vacDiagnostics(this)
|
||||
},
|
||||
onShareLogs = {
|
||||
Thread({
|
||||
val zip = runCatching { SessionLogShare.zipLatest(this) }.getOrNull()
|
||||
@@ -1003,6 +1009,7 @@ class MainActivity : ComponentActivity() {
|
||||
storageOptions = storageOptions,
|
||||
fexPreset = if (mode == SessionService.MODE_STEAM) fexPreset else null,
|
||||
steamChannel = if (mode == SessionService.MODE_STEAM) steamChannel else null,
|
||||
steamClientArch = if (mode == SessionService.MODE_STEAM) steamClientArch else null,
|
||||
steamDeckMode = mode == SessionService.MODE_STEAM && steamDeckMode,
|
||||
mangoapp = mangoapp,
|
||||
steamController = if (mode == SessionService.MODE_STEAM) steamController else null,
|
||||
@@ -1069,6 +1076,7 @@ class MainActivity : ComponentActivity() {
|
||||
},
|
||||
onFexPreset = { id -> SessionPrefs.setFexPreset(this, id); fexPreset = id },
|
||||
onSteamChannel = { id -> SessionPrefs.setSteamChannel(this, id); steamChannel = id },
|
||||
onSteamClientArch = { id -> SessionPrefs.setSteamClientArch(this, id); steamClientArch = id },
|
||||
onSteamDeckMode = { on ->
|
||||
SessionPrefs.setSteamDeckMode(this, on)
|
||||
steamDeckMode = on
|
||||
@@ -1172,6 +1180,7 @@ class MainActivity : ComponentActivity() {
|
||||
customResolution = SessionPrefs.customResolution(this, mode)
|
||||
fexPreset = SessionPrefs.fexPreset(this)
|
||||
steamChannel = SessionPrefs.steamChannel(this)
|
||||
steamClientArch = SessionPrefs.steamClientArch(this)
|
||||
steamDeckMode = SessionPrefs.steamDeckMode(this)
|
||||
mangoapp = SessionPrefs.mangoapp(this)
|
||||
steamController = SessionPrefs.steamController(this)
|
||||
@@ -1257,6 +1266,7 @@ class MainActivity : ComponentActivity() {
|
||||
frameGenLabel = FrameGen.label(this)
|
||||
romsDir = SessionPrefs.romsDir(this).takeIf { it.isNotEmpty() }
|
||||
logsEnabled = SessionPrefs.logsEnabled(this)
|
||||
vacDiagnostics = SessionPrefs.vacDiagnostics(this)
|
||||
runningLabel = if (SessionState.running) when (SessionState.mode) {
|
||||
SessionService.MODE_DESKTOP -> "Desktop"
|
||||
SessionService.MODE_RUN -> Library.nameForProgram(SessionState.program)
|
||||
|
||||
@@ -167,6 +167,8 @@ object Lossless {
|
||||
private fun steamDll(context: Context): File? {
|
||||
val libraries = listOfNotNull(
|
||||
File(LinuxRuntime.rootDir(context), "root/.local/share/Steam"),
|
||||
// The x86-64 client keeps its own library (bannerlator-steam-x64).
|
||||
File(LinuxRuntime.rootDir(context), "root/.droiddeck-x64/.local/share/Steam"),
|
||||
GameStorage.effective(context)?.path?.let(::File),
|
||||
) + SessionPrefs.addedGamesDirs(context).map(::File)
|
||||
return libraries.map { File(it, STEAM_DLL) }.filter { it.isFile }.maxByOrNull { it.lastModified() }
|
||||
|
||||
@@ -45,6 +45,7 @@ object SessionArtifacts {
|
||||
}
|
||||
LogRedactor.learnFromRuntime(LinuxRuntime.rootDir(context))
|
||||
copySteamLogs(context, dir)
|
||||
VacDiagnostics.collect(context, dir)
|
||||
// A session the system killed leaves its trace here and nowhere else.
|
||||
SessionLogCapture.dumpCrashBuffer(File(dir, "crash.log"))
|
||||
scrubFolder(dir)
|
||||
@@ -145,6 +146,7 @@ object SessionArtifacts {
|
||||
)
|
||||
if (newest) {
|
||||
copySteamLogs(context, dir)
|
||||
VacDiagnostics.collect(context, dir)
|
||||
}
|
||||
SessionLogCapture.dumpCrashBuffer(File(dir, "crash.log"))
|
||||
SessionEvents.record("session.artifacts_recovered", mapOf("newest" to newest), dir)
|
||||
|
||||
@@ -29,6 +29,10 @@ object SessionFiles {
|
||||
val files = arrayOf(
|
||||
"usr/local/bin/bannerlator-game-env" to "usr/local/bin/bannerlator-game-env",
|
||||
"libblsession.so" to "usr/local/lib/libblsession.so",
|
||||
"libblsession-x86_64.so" to "usr/local/lib/x86_64-linux-gnu/libblsession.so",
|
||||
"libfakeinput-x86_64.so" to "usr/local/lib/x86_64-linux-gnu/libfakeinput.so",
|
||||
"libblsession-i386.so" to "usr/local/lib/i386-linux-gnu/libblsession.so",
|
||||
"libfakeinput-i386.so" to "usr/local/lib/i386-linux-gnu/libfakeinput.so",
|
||||
"libfakeinput.so" to "usr/local/lib/libfakeinput.so",
|
||||
"libblfastpath.so" to "usr/local/lib/libblfastpath.so",
|
||||
"usr/local/bin/bannerlator-session" to "usr/local/bin/bannerlator-session",
|
||||
@@ -40,6 +44,8 @@ object SessionFiles {
|
||||
"usr/local/bin/bannerlator-proton-extra" to "usr/local/bin/bannerlator-proton-extra",
|
||||
"usr/local/bin/bannerlator-netmanager" to "usr/local/bin/bannerlator-netmanager",
|
||||
"usr/local/bin/bannerlator-steam-launch" to "usr/local/bin/bannerlator-steam-launch",
|
||||
// Experimental: Valve's x86-64 client under FEX (desktop menu entry).
|
||||
"usr/local/bin/bannerlator-steam-x64" to "usr/local/bin/bannerlator-steam-x64",
|
||||
"usr/local/bin/bannerlator-desktop-games" to "usr/local/bin/bannerlator-desktop-games",
|
||||
"usr/local/bin/bannerlator-steam-shim" to "usr/local/bin/bannerlator-steam-shim",
|
||||
"usr/local/bin/bannerlator-steam-shortcuts" to "usr/local/bin/bannerlator-steam-shortcuts",
|
||||
|
||||
@@ -437,6 +437,18 @@ object SessionPrefs {
|
||||
prefs(context).edit().putBoolean("logs", on).apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* VAC diagnostics (see [VacDiagnostics]): Source-engine games launched with -condebug and their
|
||||
* console plus the client's VAC lines summarised into vac.txt. Off by default: it changes a
|
||||
* game's command line.
|
||||
*/
|
||||
fun vacDiagnostics(context: Context): Boolean =
|
||||
prefs(context).getBoolean("vac_diagnostics", false)
|
||||
|
||||
fun setVacDiagnostics(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("vac_diagnostics", on).apply()
|
||||
}
|
||||
|
||||
// ── Per-mode display ────────────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -486,6 +498,17 @@ object SessionPrefs {
|
||||
prefs(context).edit().putString("steamChannel", id).apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Which Steam client a Steam session runs: "arm64" (Valve's native ARM64 client, the default) or
|
||||
* "x86_64" (Valve's x86-64 client under FEX, bannerlator-steam-x64 - experimental, for what the
|
||||
* arm64 client cannot do: it never runs VAC). Each has its own sign-in and library.
|
||||
*/
|
||||
fun steamClientArch(context: Context): String = prefs(context).getString("steamClientArch", null) ?: "arm64"
|
||||
|
||||
fun setSteamClientArch(context: Context, id: String) {
|
||||
prefs(context).edit().putString("steamClientArch", id).apply()
|
||||
}
|
||||
|
||||
/** Whether opening DroidDeck starts a Steam session instead of showing the front end. */
|
||||
fun runSteamAtStartup(context: Context): Boolean = prefs(context).getBoolean("runSteamAtStartup", false)
|
||||
|
||||
|
||||
@@ -267,6 +267,7 @@ class SessionService : Service() {
|
||||
val runtimeDir = File(filesDir, ".wayland-rt").apply { mkdirs() }
|
||||
killStragglers()
|
||||
SessionFiles.stage(this, root)
|
||||
VacDiagnostics.arm(this)
|
||||
|
||||
val sessionDir = openSessionFolder()
|
||||
val sessionLog = File(sessionDir, "session.log")
|
||||
@@ -560,6 +561,7 @@ class SessionService : Service() {
|
||||
if (SessionState.mode == MODE_STEAM) guest.add("BL_STEAMDECK=" + (if (SessionPrefs.steamDeckMode(this)) "1" else "0"))
|
||||
if (SessionState.mode == MODE_STEAM) guest.add("BL_MANGOAPP=" + (if (SessionPrefs.mangoapp(this)) "1" else "0"))
|
||||
if (steamHere) guest.add("BL_STEAM_CHANNEL=" + SessionPrefs.steamChannel(this))
|
||||
if (steamHere) guest.add("BL_STEAM_CLIENT_ARCH=" + SessionPrefs.steamClientArch(this))
|
||||
if (SessionState.mode == MODE_STEAM) {
|
||||
guest.add("BL_GAMESCOPE_FORCE_FULLSCREEN=" + (if (SessionPrefs.forceFullscreen(this)) "1" else "0"))
|
||||
guest.add("BL_GAMESCOPE_STRETCH_16X9=" + (if (SessionPrefs.stretch16x9(this)) "1" else "0"))
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.core.LogRedactor
|
||||
import com.droiddeck.launcher.runtime.LinuxRuntime
|
||||
import java.io.File
|
||||
import java.io.RandomAccessFile
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* VAC diagnostics: what a session can show about whether a game ran VAC-secure, from the files
|
||||
* Valve's own software writes - nothing is inferred from our side of the launch.
|
||||
*
|
||||
* The verdict is the game's to give. A Source-engine game decides secure or insecure once, as it
|
||||
* starts, and says so only in its own console ("You are in insecure mode. You must restart before
|
||||
* you can connect to secure servers."). That console reaches a file only when the game runs with
|
||||
* -condebug, as console.log in the game's mod folder. With this on, the launch wrapper
|
||||
* (bannerlator-proton, bl_vacdiag) adds -condebug to Source titles and notes each launch; at the end
|
||||
* of the session this copies each console.log written during it into vac/, scrubbed, and writes
|
||||
* vac.txt: the game's own secure/insecure lines, any VAC lines in the client's logs, and which
|
||||
* client files are present.
|
||||
*
|
||||
* The SteamLite lesson applies: its "INSECURE - VAC may kick" warning was our own parser reading the
|
||||
* wrong log while the client was secure all along. So vac.txt quotes the lines it found and says
|
||||
* "inconclusive" when the game never reached a server, rather than guessing either way.
|
||||
*/
|
||||
object VacDiagnostics {
|
||||
private const val TAG = "VacDiagnostics"
|
||||
|
||||
/** In the guest's home; the launch wrapper checks for it. Holds the session's start, epoch seconds. */
|
||||
private const val MARKER = "root/.droiddeck-vac-diagnostics"
|
||||
|
||||
/** One line per game launch the wrapper saw: epoch, app id, -condebug added or not, the exe. */
|
||||
private const val LAUNCHES = "root/.droiddeck-vac-launches"
|
||||
|
||||
/** The console log grows across launches; the end of it is the session that just ran. */
|
||||
private const val CONSOLE_TAIL_BYTES = 4L * 1024 * 1024
|
||||
|
||||
private const val MAX_QUOTED = 40
|
||||
|
||||
private val INSECURE = Regex("insecure mode", RegexOption.IGNORE_CASE)
|
||||
// "Connecting to" is only an attempt: L4D2 logs it, retries, and then refuses a VAC server with a
|
||||
// dialog ("Connection Blocked: Please remove plug-ins before attempting to connect to a VAC
|
||||
// secured server") that never reaches the console. Only "Connected to" means the game got in.
|
||||
private val CONNECTED = Regex("^Connected to", RegexOption.IGNORE_CASE)
|
||||
private val ATTEMPTED = Regex("^Connecting to", RegexOption.IGNORE_CASE)
|
||||
private val NOTABLE = Regex("\\bVAC\\b|secure|Connected to|Connecting to|Disconnect|challenge|game coordinator", RegexOption.IGNORE_CASE)
|
||||
private val STEAM_VAC = Regex("\\bVAC\\b|anti-?cheat", RegexOption.IGNORE_CASE)
|
||||
|
||||
fun enabled(context: Context): Boolean = SessionPrefs.vacDiagnostics(context)
|
||||
|
||||
/**
|
||||
* At session start, after the scripts are staged: arms the wrapper for this session (or disarms
|
||||
* it) and starts a fresh launch list, so the summary names only games this session ran.
|
||||
*/
|
||||
fun arm(context: Context) {
|
||||
val root = LinuxRuntime.rootDir(context)
|
||||
val marker = File(root, MARKER)
|
||||
try {
|
||||
File(root, LAUNCHES).delete()
|
||||
if (enabled(context)) marker.writeText("${System.currentTimeMillis() / 1000}\n")
|
||||
else marker.delete()
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "could not arm VAC diagnostics", e)
|
||||
}
|
||||
}
|
||||
|
||||
/** At the end of a session, into its folder. Does nothing unless the session was armed. */
|
||||
fun collect(context: Context, dir: File) {
|
||||
val root = LinuxRuntime.rootDir(context)
|
||||
val marker = File(root, MARKER)
|
||||
if (!marker.isFile) return
|
||||
try {
|
||||
val since = (marker.readText().trim().toLongOrNull() ?: 0L) * 1000 - 5_000
|
||||
val out = File(dir, "vac").apply { mkdirs() }
|
||||
val consoles = consoleLogs(context).filter { it.lastModified() >= since }
|
||||
val report = StringBuilder()
|
||||
report.appendLine("VAC diagnostics - ${now()}")
|
||||
report.appendLine("Read from the game's own console (-condebug) and the Steam client's logs.")
|
||||
report.appendLine()
|
||||
report.appendLine("== Client")
|
||||
clientFiles(root).forEach { report.appendLine(" $it") }
|
||||
report.appendLine()
|
||||
report.appendLine("== Game launches this session (from the launch wrapper)")
|
||||
val launches = File(root, LAUNCHES).takeIf { it.isFile }?.readLines().orEmpty().filter { it.isNotBlank() }
|
||||
if (launches.isEmpty()) report.appendLine(" none - no Windows game was launched through the client")
|
||||
launches.forEach { report.appendLine(" " + LogRedactor.redact(it)) }
|
||||
report.appendLine()
|
||||
report.appendLine("== Game consoles")
|
||||
if (consoles.isEmpty()) {
|
||||
report.appendLine(" No console.log was written this session. Only Source-engine games write one,")
|
||||
report.appendLine(" and only when started with -condebug (this setting adds it).")
|
||||
}
|
||||
consoles.forEach { log ->
|
||||
val name = consoleName(log)
|
||||
val lines = tail(log)
|
||||
File(out, "$name-console.log").bufferedWriter().use { w ->
|
||||
lines.forEach { w.write(LogRedactor.redact(it)); w.newLine() }
|
||||
}
|
||||
report.appendLine("-- $name (vac/$name-console.log)")
|
||||
report.appendLine(" Verdict: " + verdict(lines))
|
||||
lines.filter { NOTABLE.containsMatchIn(it) }.takeLast(MAX_QUOTED)
|
||||
.forEach { report.appendLine(" | " + LogRedactor.redact(it.trim())) }
|
||||
report.appendLine()
|
||||
}
|
||||
report.appendLine("== VAC in the Steam client's logs")
|
||||
val steamHits = File(dir, "steam").listFiles { f -> f.isFile }?.sortedBy { it.name }.orEmpty()
|
||||
.flatMap { f -> runCatching { f.readLines() }.getOrDefault(emptyList()).filter { STEAM_VAC.containsMatchIn(it) }.map { "${f.name}: ${it.trim()}" } }
|
||||
if (steamHits.isEmpty()) report.appendLine(" none - the client logged no VAC activity this session")
|
||||
steamHits.takeLast(MAX_QUOTED).forEach { report.appendLine(" $it") }
|
||||
File(dir, "vac.txt").writeText(report.toString())
|
||||
Log.i(TAG, "VAC diagnostics: ${consoles.size} console log(s), ${steamHits.size} client VAC line(s), into $dir")
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "collecting VAC diagnostics", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun verdict(lines: List<String>): String = when {
|
||||
lines.any { INSECURE.containsMatchIn(it) } ->
|
||||
"INSECURE - the game started in insecure mode and cannot join VAC-secured servers until it restarts."
|
||||
lines.any { CONNECTED.containsMatchIn(it) } ->
|
||||
"No insecure notice, and the game connected to a server. Confirm with the server's own info that it is VAC-secured."
|
||||
lines.any { ATTEMPTED.containsMatchIn(it) } ->
|
||||
"NOT CONNECTED - the game tried a server and never got in. If the screen said \"Connection Blocked: " +
|
||||
"Please remove plug-ins ... VAC secured server\" (Left 4 Dead 2's wording), the game is insecure; " +
|
||||
"that dialog never reaches the console."
|
||||
else ->
|
||||
"Inconclusive - the game never connected to a server. Join a VAC-secured server, then check again."
|
||||
}
|
||||
|
||||
/** Every console.log in a Steam library: <game>/<mod>/console.log (Source) and <game>/game/<mod>/console.log (Source 2). */
|
||||
private fun consoleLogs(context: Context): List<File> {
|
||||
val found = LinkedHashMap<String, File>()
|
||||
for (library in libraryRoots(context)) {
|
||||
val games = File(library, "steamapps/common").listFiles { f -> f.isDirectory } ?: continue
|
||||
for (game in games) {
|
||||
val mods = (game.listFiles { f -> f.isDirectory }.orEmpty().toList() +
|
||||
File(game, "game").listFiles { f -> f.isDirectory }.orEmpty().toList())
|
||||
for (mod in mods) {
|
||||
val log = File(mod, "console.log")
|
||||
if (log.isFile) found[runCatching { log.canonicalPath }.getOrDefault(log.path)] = log
|
||||
}
|
||||
}
|
||||
}
|
||||
return found.values.toList()
|
||||
}
|
||||
|
||||
private fun libraryRoots(context: Context): List<File> {
|
||||
val root = LinuxRuntime.rootDir(context)
|
||||
val steam = File(root, "root/.local/share/Steam")
|
||||
val roots = mutableListOf(steam)
|
||||
GameStorage.effective(context)?.let { roots.add(File(it.path)) }
|
||||
runCatching {
|
||||
Regex("\"path\"\\s+\"([^\"]+)\"").findAll(File(steam, "steamapps/libraryfolders.vdf").readText()).forEach {
|
||||
roots.add(File(root, it.groupValues[1].removePrefix("/")))
|
||||
}
|
||||
}
|
||||
return roots.filter { it.isDirectory }.distinctBy { runCatching { it.canonicalPath }.getOrDefault(it.path) }
|
||||
}
|
||||
|
||||
/** "Team Fortress 2-tf": the game folder and the mod, safe as a file name. */
|
||||
private fun consoleName(log: File): String {
|
||||
val mod = log.parentFile
|
||||
val game = mod?.parentFile?.let { if (it.name == "game") it.parentFile else it }
|
||||
return "${game?.name ?: "game"}-${mod?.name ?: "mod"}".replace(Regex("[^A-Za-z0-9._ +-]+"), "_")
|
||||
}
|
||||
|
||||
private fun tail(file: File): List<String> = try {
|
||||
RandomAccessFile(file, "r").use { raf ->
|
||||
val start = (raf.length() - CONSOLE_TAIL_BYTES).coerceAtLeast(0)
|
||||
raf.seek(start)
|
||||
val bytes = ByteArray((raf.length() - start).toInt())
|
||||
raf.readFully(bytes)
|
||||
String(bytes, Charsets.UTF_8).lines().let { if (start > 0) it.drop(1) else it }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
emptyList()
|
||||
}
|
||||
|
||||
/** Which Steam client builds are on disk - the arm64 one has no VAC module of its own to run. */
|
||||
private fun clientFiles(root: File): List<String> {
|
||||
val steam = File(root, "root/.local/share/Steam")
|
||||
return listOf(
|
||||
"linuxarm64/steamclient.so" to "arm64 Linux client",
|
||||
"linux64/steamclient.so" to "x86-64 Linux client",
|
||||
"ubuntu12_64/steamclient.so" to "x86-64 Linux client (ubuntu12_64)",
|
||||
).map { (path, label) -> "$label: ${if (File(steam, path).isFile) "present" else "absent"} ($path)" }
|
||||
}
|
||||
|
||||
private fun now(): String = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).format(Date())
|
||||
}
|
||||
@@ -91,6 +91,7 @@ class FrontEndState(
|
||||
val frameGenLabel: String,
|
||||
val romsDir: String?,
|
||||
val logsEnabled: Boolean,
|
||||
val vacDiagnostics: Boolean = false,
|
||||
val steamGames: List<Library.SteamGame>,
|
||||
val emulators: List<Library.Emulator>,
|
||||
val running: String?,
|
||||
@@ -152,6 +153,7 @@ class FrontEndActions(
|
||||
val onFiles: () -> Unit,
|
||||
val onBrowseFiles: (File) -> Unit = {},
|
||||
val onLogs: () -> Unit,
|
||||
val onVacDiagnostics: () -> Unit = {},
|
||||
val onShareLogs: () -> Unit = {},
|
||||
val onClearLogs: () -> Unit = {},
|
||||
val onOffline: () -> Unit,
|
||||
|
||||
@@ -289,6 +289,7 @@ internal fun SetupPanel(
|
||||
}
|
||||
}
|
||||
ToggleRow(host, "logs", stringResource(R.string.setup_logs), stringResource(R.string.setup_logs_hint), s.logsEnabled) { a.onLogs() }
|
||||
ToggleRow(host, "vacdiag", stringResource(R.string.setup_vac_diag), stringResource(R.string.setup_vac_diag_hint), s.vacDiagnostics) { a.onVacDiagnostics() }
|
||||
ActionRow(stringResource(R.string.setup_latest_logs), stringResource(R.string.drawer_logs_hint), stringResource(R.string.drawer_share_logs), a.onShareLogs)
|
||||
ActionRow(stringResource(R.string.setup_saved_logs), stringResource(R.string.setup_saved_logs_hint, com.droiddeck.launcher.session.SessionPaths.KEEP_SESSIONS), stringResource(R.string.setup_clear_logs), a.onClearLogs)
|
||||
ToggleRow(
|
||||
|
||||
@@ -72,6 +72,7 @@ class ModeSettings(
|
||||
val stretch16x9: Boolean? = null,
|
||||
/** Steam only: the client branch forced on the command line. */
|
||||
val steamChannel: String? = null,
|
||||
val steamClientArch: String? = null,
|
||||
/** Steam only: enable the SteamOS client interface and its performance controls. */
|
||||
val steamDeckMode: Boolean = false,
|
||||
/** Steam, Deck mode: the QAM's performance overlay (mangoapp) is started. */
|
||||
@@ -127,6 +128,7 @@ class ModeSettingsActions(
|
||||
val onForceFullscreen: (Boolean) -> Unit = {},
|
||||
val onStretch16x9: (Boolean) -> Unit = {},
|
||||
val onSteamChannel: (String) -> Unit = {},
|
||||
val onSteamClientArch: (String) -> Unit = {},
|
||||
val onSteamDeckMode: (Boolean) -> Unit = {},
|
||||
val onMangoapp: (Boolean) -> Unit = {},
|
||||
val onSteamController: (String) -> Unit = {},
|
||||
@@ -334,6 +336,11 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
note = stringResource(R.string.mode_branch_note),
|
||||
onPick = a.onSteamChannel,
|
||||
)
|
||||
if (s.steamClientArch != null) ChoiceRow(
|
||||
host, "clientArch", stringResource(R.string.mode_client_arch), stringResource(R.string.mode_client_arch_hint),
|
||||
listOf("arm64" to stringResource(R.string.mode_client_arch_arm64), "x86_64" to stringResource(R.string.mode_client_arch_x64)), s.steamClientArch,
|
||||
onPick = a.onSteamClientArch,
|
||||
)
|
||||
}
|
||||
if (steam && s.wifiDiscovery != null) SettingsGroup(stringResource(R.string.mode_network)) {
|
||||
val hint = when {
|
||||
|
||||
@@ -256,6 +256,10 @@
|
||||
<string name="mode_branch_public">Beta pública</string>
|
||||
<string name="mode_branch_deck_beta">Beta pública de Steam Deck</string>
|
||||
<string name="mode_branch_note">El modo Deck siempre usa la beta pública de Steam Deck.</string>
|
||||
<string name="mode_client_arch">Cliente de Steam</string>
|
||||
<string name="mode_client_arch_hint">Se aplica en la próxima sesión. El cliente x86-64 se ejecuta con FEX, con su propio inicio de sesión y biblioteca, y dibuja su interfaz sin la GPU.</string>
|
||||
<string name="mode_client_arch_arm64">ARM64 (recomendado)</string>
|
||||
<string name="mode_client_arch_x64">x86-64 (FEX, experimental)</string>
|
||||
<string name="mode_added_games">Juegos añadidos</string>
|
||||
<string name="mode_added_none">no hay carpetas de juegos con un .exe</string>
|
||||
<string name="mode_games_folder">Carpeta de juegos</string>
|
||||
@@ -483,6 +487,8 @@
|
||||
<string name="setup_session">Sesión</string>
|
||||
<string name="setup_logs">Registros de sesión</string>
|
||||
<string name="setup_logs_hint">Se guardan tras cada sesión</string>
|
||||
<string name="setup_vac_diag">Diagnóstico de VAC</string>
|
||||
<string name="setup_vac_diag_hint">Los juegos Source registran su consola (-condebug); vac.txt en los registros dice si es seguro o no</string>
|
||||
<string name="setup_latest_logs">Últimos registros de sesión</string>
|
||||
<string name="setup_saved_logs">Registros de sesión guardados</string>
|
||||
<string name="setup_saved_logs_hint">Conserva los %1$d más recientes</string>
|
||||
|
||||
@@ -269,6 +269,10 @@
|
||||
<string name="mode_branch_public">Public beta</string>
|
||||
<string name="mode_branch_deck_beta">Steam Deck public beta</string>
|
||||
<string name="mode_branch_note">Deck mode always uses Steam Deck public beta.</string>
|
||||
<string name="mode_client_arch">Steam client</string>
|
||||
<string name="mode_client_arch_hint">Applies next session. The x86-64 client runs under FEX with its own sign-in and library, and draws its interface without the GPU.</string>
|
||||
<string name="mode_client_arch_arm64">ARM64 (recommended)</string>
|
||||
<string name="mode_client_arch_x64">x86-64 (FEX, experimental)</string>
|
||||
<string name="mode_added_games">Added games</string>
|
||||
<string name="mode_added_none">no game folders with a .exe found</string>
|
||||
<string name="mode_games_folder">Games folder</string>
|
||||
@@ -517,6 +521,8 @@
|
||||
<string name="setup_session">Session</string>
|
||||
<string name="setup_logs">Session logs</string>
|
||||
<string name="setup_logs_hint">Saved after each session</string>
|
||||
<string name="setup_vac_diag">VAC diagnostics</string>
|
||||
<string name="setup_vac_diag_hint">Source games log their console (-condebug); vac.txt in the session logs says secure or insecure</string>
|
||||
<string name="setup_latest_logs">Latest session logs</string>
|
||||
<string name="setup_saved_logs">Saved session logs</string>
|
||||
<string name="setup_saved_logs_hint">Keeps the newest %1$d</string>
|
||||
|
||||
@@ -7,6 +7,105 @@ the timeline, then lessons and backlog. Companion to the README (what the app *d
|
||||
|
||||
---
|
||||
|
||||
## 2026-10-02 - `feat/x86-64-steam-client`: Valve's x86-64 Linux client under FEX (experiment)
|
||||
|
||||
Why: the arm64 Linux client never runs VAC. L4D2 (and TF2 in Bannerlator) are refused by VAC
|
||||
servers ("Connection Blocked ... VAC secured server") and no client log mentions VAC; VAC modules
|
||||
are x86 code. The question is whether Valve's x86-64 client under FEX gets a VAC module it can run.
|
||||
Branched from `feat/vac-diagnostics`. Opt-in: `bannerlator-steam-x64` (desktop menu "Steam x86-64
|
||||
(FEX, experimental)") runs the client in its own home (`~/.droiddeck-x64`) with its own rootfs,
|
||||
login and library. The arm64 client stays the default and is not touched.
|
||||
|
||||
Device-proven on the FIT (Genshin repack): **the x86-64 client runs, self-updates, renders its full
|
||||
UI (software GL), logs in, shows friends, and downloads games.** VAC itself is not tested yet.
|
||||
|
||||
What it took, in order (each step was the next failure on device):
|
||||
- **FEX for the guest** - `tools/fex/build-in-arch.sh` + `build-fex.yml` (FEX-2609, Arch Linux ARM
|
||||
container, no thunks), released as `fex-2609-r2` (not Latest). Rootfs: FEX's Ubuntu 24.04 image.
|
||||
- **openat2** - our proot answers it with ENOSYS; FEX opened every rootfs path with
|
||||
`openat2(RESOLVE_IN_ROOT)`, fell through to the arm64 guest on anything but EXDEV, and x86 bash
|
||||
could not find libtinfo. `tools/fex/patches/0001-openat2-enosys-fallback.patch`.
|
||||
- **FEXServer owns the rootfs** - a server started before `FEX_ROOTFS` (any earlier FEX run) answers
|
||||
every client with no rootfs. The launcher stops it first.
|
||||
- **32-bit x86 died with SIGILL** - libblfastpath (the proot fast path, preloaded into every guest
|
||||
process, FEX itself included) breaks FEX's 32-bit mode. `PROOT_FP_OFF=1` for FEX.
|
||||
- **"Steam now requires user namespaces"** - steam-runtime-check-requirements' bwrap probe; the
|
||||
arm64 client has no such probe. The SteamRT3 client (`steamrt64/steam`) is started directly, the
|
||||
way the session starts `steamrtarm64/steam`, with `STEAM_RUNTIME_STEAMRT` pointing at Valve's
|
||||
runtime linked file by file and only the probe replaced.
|
||||
- **tier0 assertion "Function not implemented"** - semget: Android has no System V IPC. Traced with
|
||||
a `-DDEBUG_STRACE` FEX (build-fex.yml `cxxflags` input, artifact only). The session shim
|
||||
(`preload/*.c`) is now also built natively as `libblsession-x86_64.so`, staged, and preloaded
|
||||
through the rootfs's own `/etc/ld.so.preload`.
|
||||
- **re-exec into pressure-vessel after self-update** - `STEAM_STEAMRT_RECURSION_GUARD=1`.
|
||||
- **libgtk-x11-2.0 missing / soname links / old glibc shadowing** - Valve's steamrt3c platform
|
||||
libraries linked into `~/.droiddeck-x64/steamrt-libs`, soname links from the rootfs `ldconfig`,
|
||||
and only libraries the rootfs lacks (LD_LIBRARY_PATH beats system folders whatever its order).
|
||||
- **GLX failed** - the desktop exports zink over the arm64 Turnip ICD; cleared, llvmpipe used.
|
||||
- **web helper died of SIGTRAP a second after start** - found with pid-tagged traces (FEX AppConfig
|
||||
`OutputLog=server` + a foreground FEXServer, `BL_FEXSERVER_LOG`; file logs overwrite each other):
|
||||
every web helper read `/proc/self/status` from libtier0_s.so, saw proot as a tracer, and tier0 broke
|
||||
into the "debugger" with int3. `preload/tracer.c` exempts the web helper on purpose (Chromium traps
|
||||
on a status fd that is not procfs); with `BL_WEBHELPER_HIDE_TRACER=1` only opens called from
|
||||
libtier0_s.so get the copy.
|
||||
- `SessionFiles` stages `bannerlator-steam-x64` and the x86-64 shim (they were missing from the list).
|
||||
|
||||
Dead ends worth remembering: headless VAC testing is impossible (current Steam starts the saved
|
||||
login from its UI); `-cef-*` switches are not forwarded to the x86 web helper; the KMS
|
||||
`CREATE_DUMB` errors are a red herring (40 fast ENOTTY per process); not an address-space problem
|
||||
(0 ENOMEM in 618k traced syscalls, although the device is 39-bit VA).
|
||||
|
||||
Later the same day (device-proven unless noted):
|
||||
- **Controllers**: libfakeinput is now also built for x86-64 and preloaded in the FEX rootfs; the
|
||||
client registers the pad, and **Big Picture works with it** (Xbox prompts, D-pad moves focus).
|
||||
- **Online**: the launcher starts the system bus and the NetworkManager stand-in like the session.
|
||||
- From an audit of `bannerlator-session`: stale lock cleanup, branch sync before every start, core
|
||||
re-pinning, path cache and `perms.c` for `steamrt64`, the app's FEX preset (PERFORMANCE_TSO).
|
||||
- **Play button (built, not yet device-tested)**: Steam settings > Client > "Steam client: ARM64 /
|
||||
x86-64 (FEX, experimental)" (`BL_STEAM_CLIENT_ARCH`). The session then starts the x86 client via
|
||||
`bannerlator-steam-x64 --prepare/--exec` and gives it everything it gives the arm64 one; steps
|
||||
only for the arm64 client (ARM64 Proton, mapping, seeding, Decky, steamclient.so probes) are
|
||||
skipped.
|
||||
- L4D2 hard-link reuse does not work: switching platform makes Steam redownload depot 551 anyway.
|
||||
|
||||
Evening (device-proven unless noted):
|
||||
- **Play button with the x86-64 client works** (Steam settings > Client > Steam client: x86-64).
|
||||
Deck mode is skipped for it: with -steamos3 its SteamOS audio manager aborts on the runtime's
|
||||
PulseAudio 13 (pa_operation_get_state assertion).
|
||||
- **Left 4 Dead 2 runs in the x86-64 client and joined a live online game** (Quick Match, Swamp
|
||||
Fever, other players, 138 ms) - the server type that refused the arm64 client with "Connection
|
||||
Blocked ... VAC secured server". Not yet hard proof of VAC (no client log line; -condebug added
|
||||
for the server's secure flag). Software rendering: ~11 fps.
|
||||
- What the launch needed: "DroidDeck direct", a compatibility tool running native Linux games with
|
||||
Valve's scout library runtime instead of the Steam Linux Runtime container; a stand-in
|
||||
steam-runtime-launch-client (the client, believing itself inside the SteamRT container, starts
|
||||
every game "alongside Steam" through it, and Valve's needs a launcher service on a session bus);
|
||||
i386 builds of the session shim (the i386 controller reader crashes the game and is left out for
|
||||
now). A steam:// URL written to the launch dir now reaches the running client.
|
||||
- **FEX with host thunks** (fex-2609-r3): FEX's Vulkan/GL/EGL/DRM/Wayland/ALSA thunks, 64- and
|
||||
32-bit guests, guest halves cross-compiled against an Arch x86_64/multilib sysroot. The x86 client
|
||||
uses them when installed (GPU mode, the arm64 client's ANGLE-on-Vulkan flags) - built, not yet
|
||||
device-tested.
|
||||
- The proot fast path now stands down in FEX only for 32-bit programs (it broke FEX's 32-bit mode).
|
||||
- **On the GPU, VAC proven (19:40):** games get FEX's host thunks and the session's Turnip/Zink
|
||||
settings (the client and its browser stay in software: with the thunks the browser's GPU process
|
||||
dies and the client never opens a window). Left 4 Dead 2 in The Passing on an online server:
|
||||
51 fps (was ~11 in software). The server answered the A2S info query as "Valve Left4Dead 2 US
|
||||
East Server", VAC=1 - an official VAC-secured server, which the arm64 client is refused.
|
||||
- The crash that held this up was not the GPU: the 32-bit controller reader was still preloaded
|
||||
(FEX falls through to the session's own i386 copy), and the game's SDL2 strdup()s a NULL udev
|
||||
attribute of its fake devices. Found with a SIGSEGV logger (tools/diag/segvlog.c).
|
||||
- The Linux FEX as a component file: /sdcard/Download/FEX-2609-r2-x86-64-guest.wcp (type
|
||||
FEX-Guest); the Components section does not know that type yet.
|
||||
|
||||
Open:
|
||||
- VAC test: L4D2's native Linux build in the x86 client's own library (hard-linked from the arm64
|
||||
copy, same filesystem, so only the Linux depots download). Native Linux games start inside the
|
||||
Steam Linux Runtime container (pressure-vessel) - expected to need the same treatment.
|
||||
- No x86 GPU driver: games render with llvmpipe. Playable needs FEX host thunks.
|
||||
- Windows games in the x86 client need x86 Proton = pressure-vessel.
|
||||
- One client per account: integration would be a per-title hand-off, not side by side.
|
||||
|
||||
## 2026-09-29 - `feat/controller-input`: the pad the way SteamOS has it
|
||||
|
||||
Device-tested on the AYN Thor (Katamari under Proton Experimental ARM64).
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// Diagnostic: on SIGSEGV/SIGBUS/SIGILL, write the faulting address, the instruction pointer and a
|
||||
// frame-pointer backtrace (module+offset, symbol) to $SEGVLOG_FILE, then die as before. Later
|
||||
// SIGSEGV handlers (the game's breakpad) are refused so this one sees the fault first.
|
||||
#define _GNU_SOURCE
|
||||
#include <dlfcn.h>
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <ucontext.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int (*real_sigaction)(int, const struct sigaction *, struct sigaction *);
|
||||
static int out = -1;
|
||||
|
||||
static void line(const char *tag, void *pc) {
|
||||
char buf[512];
|
||||
Dl_info di;
|
||||
if (pc && dladdr(pc, &di) && di.dli_fname)
|
||||
snprintf(buf, sizeof buf, "%s %p %s+0x%lx %s\n", tag, pc, di.dli_fname,
|
||||
(unsigned long)((char *)pc - (char *)di.dli_fbase), di.dli_sname ? di.dli_sname : "?");
|
||||
else
|
||||
snprintf(buf, sizeof buf, "%s %p ?\n", tag, pc);
|
||||
if (out >= 0) write(out, buf, strlen(buf));
|
||||
}
|
||||
|
||||
static void handler(int sig, siginfo_t *si, void *ctx) {
|
||||
ucontext_t *uc = ctx;
|
||||
char buf[160];
|
||||
#if defined(__i386__)
|
||||
void *pc = (void *)uc->uc_mcontext.gregs[REG_EIP];
|
||||
void **fp = (void **)uc->uc_mcontext.gregs[REG_EBP];
|
||||
#else
|
||||
void *pc = (void *)uc->uc_mcontext.gregs[REG_RIP];
|
||||
void **fp = (void **)uc->uc_mcontext.gregs[REG_RBP];
|
||||
#endif
|
||||
snprintf(buf, sizeof buf, "=== pid %d signal %d code %d fault addr %p\n", getpid(), sig, si->si_code, si->si_addr);
|
||||
if (out >= 0) write(out, buf, strlen(buf));
|
||||
line("pc", pc);
|
||||
// Leaf functions (strlen, memcpy) keep no frame: the caller's return address is on the stack.
|
||||
#if defined(__i386__)
|
||||
void **sp = (void **)uc->uc_mcontext.gregs[REG_ESP];
|
||||
#else
|
||||
void **sp = (void **)uc->uc_mcontext.gregs[REG_RSP];
|
||||
#endif
|
||||
for (int i = 0; i < 48 && sp; i++) {
|
||||
Dl_info di;
|
||||
if (sp[i] && dladdr(sp[i], &di) && di.dli_fname) line(" sp", sp[i]);
|
||||
}
|
||||
for (int i = 0; i < 24 && fp && ((unsigned long)fp & 3) == 0; i++) {
|
||||
void *ret = fp[1];
|
||||
if (!ret) break;
|
||||
line(" at", ret);
|
||||
void **next = (void **)fp[0];
|
||||
if (next <= fp) break;
|
||||
fp = next;
|
||||
}
|
||||
signal(sig, SIG_DFL);
|
||||
}
|
||||
|
||||
int sigaction(int sig, const struct sigaction *act, struct sigaction *old) {
|
||||
if (!real_sigaction) real_sigaction = dlsym(RTLD_NEXT, "sigaction");
|
||||
if (act && (sig == SIGSEGV || sig == SIGBUS || sig == SIGILL) && act->sa_sigaction != handler) {
|
||||
if (old) real_sigaction(sig, NULL, old);
|
||||
return 0;
|
||||
}
|
||||
return real_sigaction(sig, act, old);
|
||||
}
|
||||
|
||||
__attribute__((constructor)) static void init(void) {
|
||||
const char *f = getenv("SEGVLOG_FILE");
|
||||
if (!f) return;
|
||||
out = open(f, O_WRONLY | O_CREAT | O_APPEND, 0644);
|
||||
struct sigaction sa;
|
||||
memset(&sa, 0, sizeof sa);
|
||||
sa.sa_sigaction = handler;
|
||||
sa.sa_flags = SA_SIGINFO | SA_ONSTACK;
|
||||
real_sigaction = dlsym(RTLD_NEXT, "sigaction");
|
||||
real_sigaction(SIGSEGV, &sa, NULL);
|
||||
real_sigaction(SIGBUS, &sa, NULL);
|
||||
real_sigaction(SIGILL, &sa, NULL);
|
||||
}
|
||||
Executable
+79
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/bash
|
||||
# Runs INSIDE an Arch Linux ARM container (menci/archlinuxarm:base-devel) on an arm64 runner.
|
||||
# Builds FEX for the runtime, with tools/fex/patches, (Arch Linux ARM ships no package and FEX publishes no Linux
|
||||
# binaries), plus the runtime's own unsquashfs to unpack FEX's x86 rootfs image, and packs both
|
||||
# as fex.tzst (usr/local/...) for bannerlator-steam-x64 to stage. FEX_THUNKS=1 adds the host
|
||||
# thunks (see below).
|
||||
set -euxo pipefail
|
||||
VERSION=${FEX_VERSION:-FEX-2609}
|
||||
WORK=/work
|
||||
cd "$WORK"
|
||||
# Same pacman workarounds as tools/gamescope/build-in-arch.sh.
|
||||
grep -q '^DisableSandbox' /etc/pacman.conf || sed -i 's/^\[options\]/[options]\nDisableSandbox/' /etc/pacman.conf
|
||||
{ for m in https://ca.us.mirror.archlinuxarm.org https://fl.us.mirror.archlinuxarm.org https://de3.mirror.archlinuxarm.org https://nl.mirror.archlinuxarm.org; do echo "Server = $m/\$arch/\$repo"; done; cat /etc/pacman.d/mirrorlist; } > /etc/pacman.d/mirrorlist.new
|
||||
mv /etc/pacman.d/mirrorlist.new /etc/pacman.d/mirrorlist
|
||||
pacman -Syu --noconfirm --needed git zstd binutils cmake ninja clang lld llvm python squashfs-tools file
|
||||
|
||||
# FEX_THUNKS=1: host thunks too - an x86 program's libvulkan/libGL/libX11/... calls go to the
|
||||
# runtime's own arm64 libraries (Turnip, Mesa), so the x86-64 client draws on the GPU. The host
|
||||
# half is arm64 and needs the graphics dev packages; the guest half is cross-compiled with clang
|
||||
# against an x86 sysroot: Arch Linux x86_64 (and multilib i686) packages unpacked into a root of
|
||||
# their own by pacman - never run, so no scriptlets and no signature checks.
|
||||
THUNKS=${FEX_THUNKS:-0}
|
||||
X86ROOT=$WORK/x86root
|
||||
if [ "$THUNKS" = 1 ]; then
|
||||
pacman -S --noconfirm --needed pkgconf libx11 libxcb libxrandr libxrender libxext xorgproto \
|
||||
libglvnd mesa wayland libdrm alsa-lib libxshmfence vulkan-headers vulkan-icd-loader
|
||||
rm -rf "$X86ROOT" && mkdir -p "$X86ROOT/var/lib/pacman" "$WORK/x86-cache"
|
||||
cat > "$WORK/x86-pacman.conf" <<'CONF'
|
||||
[options]
|
||||
Architecture = x86_64
|
||||
SigLevel = Never
|
||||
DisableSandbox
|
||||
[core]
|
||||
Server = https://geo.mirror.pkgbuild.com/$repo/os/$arch
|
||||
[extra]
|
||||
Server = https://geo.mirror.pkgbuild.com/$repo/os/$arch
|
||||
[multilib]
|
||||
Server = https://geo.mirror.pkgbuild.com/$repo/os/$arch
|
||||
CONF
|
||||
pacman --config "$WORK/x86-pacman.conf" --root "$X86ROOT" --dbpath "$X86ROOT/var/lib/pacman" \
|
||||
--cachedir "$WORK/x86-cache" --noscriptlet --noconfirm --overwrite '*' -Sy \
|
||||
glibc linux-api-headers gcc gcc-libs libx11 libxcb xorgproto libxrandr libxrender libxext libxau \
|
||||
libxdmcp libglvnd mesa wayland libdrm alsa-lib libxshmfence \
|
||||
lib32-glibc lib32-gcc-libs lib32-libx11 lib32-libxcb lib32-libxrandr lib32-libxrender \
|
||||
lib32-libxext lib32-libglvnd lib32-mesa lib32-wayland lib32-libdrm lib32-alsa-lib lib32-libxshmfence
|
||||
ls "$X86ROOT/usr/include" | head -5
|
||||
fi
|
||||
|
||||
rm -rf fex-src out && git clone -q --depth 1 --branch "$VERSION" --recurse-submodules --shallow-submodules https://github.com/FEX-Emu/FEX.git fex-src
|
||||
# Our proot answers openat2 with ENOSYS (tools/proot/PATCHES.md, 0008); FEX opened every rootfs
|
||||
# path with openat2 and fell through to the arm64 guest's files on anything but EXDEV.
|
||||
for p in tools/fex/patches/*.patch; do patch -d fex-src -p1 --no-backup-if-mismatch < "$p"; done
|
||||
# FEX's x86 toolchain files assume an x86 build machine: on this arm64 one the guest thunks need the
|
||||
# x86 sysroot for their headers, C runtime objects and libgcc too, not just the generator.
|
||||
if [ "$THUNKS" = 1 ]; then
|
||||
for t in fex-src/Data/CMake/toolchain_x86_64.cmake fex-src/Data/CMake/toolchain_x86_32.cmake; do
|
||||
printf '\nif (X86_DEV_ROOTFS AND NOT X86_DEV_ROOTFS STREQUAL "/")\n set(CMAKE_SYSROOT "${X86_DEV_ROOTFS}")\nendif()\n' >> "$t"
|
||||
done
|
||||
fi
|
||||
# TUNE_CPU none: the default (native) would tune for the runner's Neoverse cores, not the device.
|
||||
cmake -S fex-src -B fex-build -G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local \
|
||||
-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DUSE_LINKER=lld \
|
||||
-DTUNE_CPU=none -DTUNE_ARCH=armv8-a \
|
||||
-DBUILD_TESTING=OFF -DBUILD_FEXCONFIG=OFF -DENABLE_ASSERTIONS=OFF \
|
||||
$( [ "$THUNKS" = 1 ] && echo "-DBUILD_THUNKS=ON -DX86_DEV_ROOTFS=$X86ROOT" || echo "-DBUILD_THUNKS=OFF" ) \
|
||||
-DENABLE_CCACHE=OFF -DENABLE_OFFLINE_TELEMETRY=OFF \
|
||||
${FEX_EXTRA_CXXFLAGS:+-DCMAKE_CXX_FLAGS="$FEX_EXTRA_CXXFLAGS"}
|
||||
ninja -C fex-build
|
||||
DESTDIR="$WORK/out" ninja -C fex-build install
|
||||
install -Dm755 /usr/bin/unsquashfs out/usr/local/bin/unsquashfs
|
||||
# Nothing outside the runtime's own libraries: every needed soname must resolve in a stock guest.
|
||||
for f in out/usr/local/bin/*; do
|
||||
file "$f" | grep -q ELF || continue
|
||||
readelf -d "$f" | awk '/NEEDED/ { gsub(/[\[\]]/, "", $5); print $5 }'
|
||||
done | sort -u | tee fex-needed.txt
|
||||
(cd out && find . -type f -o -type l | sort) | tee fex-files.txt
|
||||
tar -C out -c . | zstd -19 -T0 -o fex.tzst
|
||||
sha256sum fex.tzst | tee fex.tzst.sha256
|
||||
@@ -0,0 +1,42 @@
|
||||
diff --git a/Source/Tools/LinuxEmulation/LinuxSyscalls/FileManagement.cpp b/Source/Tools/LinuxEmulation/LinuxSyscalls/FileManagement.cpp
|
||||
index 7172460..d566e3b 100644
|
||||
--- a/Source/Tools/LinuxEmulation/LinuxSyscalls/FileManagement.cpp
|
||||
+++ b/Source/Tools/LinuxEmulation/LinuxSyscalls/FileManagement.cpp
|
||||
@@ -674,9 +674,10 @@ uint64_t FileManager::Open(const char* pathname, int flags, uint32_t mode) {
|
||||
};
|
||||
fd = ::syscall(SYSCALL_DEF(openat2), Path.FD, Path.Path, &how, sizeof(how));
|
||||
|
||||
- if (fd == -1 && errno == EXDEV) {
|
||||
- // This means a magic symlink (/proc/foo) was involved. In this case we
|
||||
- // just punt and do the access without RESOLVE_IN_ROOT.
|
||||
+ if (fd == -1 && (errno == EXDEV || errno == ENOSYS)) {
|
||||
+ // EXDEV: a magic symlink (/proc/foo) was involved. ENOSYS: no openat2 here (proot).
|
||||
+ // Either way, do the access without RESOLVE_IN_ROOT; GetEmulatedFDPath has already
|
||||
+ // followed absolute symlinks inside the rootfs.
|
||||
fd = ::syscall(SYSCALL_DEF(openat), Path.FD, Path.Path, flags, mode);
|
||||
}
|
||||
}
|
||||
@@ -927,9 +928,10 @@ uint64_t FileManager::Openat([[maybe_unused]] int dirfs, const char* pathname, i
|
||||
.resolve = (Path.FD == AT_FDCWD) ? 0u : RESOLVE_IN_ROOT, // AT_FDCWD means it's a thunk and not via RootFS
|
||||
};
|
||||
fd = ::syscall(SYSCALL_DEF(openat2), Path.FD, Path.Path, &how, sizeof(how));
|
||||
- if (fd == -1 && errno == EXDEV) {
|
||||
- // This means a magic symlink (/proc/foo) was involved. In this case we
|
||||
- // just punt and do the access without RESOLVE_IN_ROOT.
|
||||
+ if (fd == -1 && (errno == EXDEV || errno == ENOSYS)) {
|
||||
+ // EXDEV: a magic symlink (/proc/foo) was involved. ENOSYS: no openat2 here (proot).
|
||||
+ // Either way, do the access without RESOLVE_IN_ROOT; GetEmulatedFDPath has already
|
||||
+ // followed absolute symlinks inside the rootfs.
|
||||
fd = ::syscall(SYSCALL_DEF(openat), Path.FD, Path.Path, flags, mode);
|
||||
}
|
||||
}
|
||||
@@ -967,6 +969,9 @@ uint64_t FileManager::Openat2(int dirfs, const char* pathname, FEX::HLE::open_ho
|
||||
// This means a magic symlink (/proc/foo) was involved. In this case we
|
||||
// just punt and do the access without RESOLVE_IN_ROOT.
|
||||
fd = ::syscall(SYSCALL_DEF(openat2), Path.FD, Path.Path, how, usize);
|
||||
+ } else if (fd == -1 && errno == ENOSYS && how->resolve == 0) {
|
||||
+ // No openat2 here (proot): the plain openat inside the rootfs, as above.
|
||||
+ fd = ::syscall(SYSCALL_DEF(openat), Path.FD, Path.Path, (int)how->flags, (int)how->mode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,6 +119,17 @@ export BL_DESKTOP=1
|
||||
ln -sf /usr/local/bin/bannerlator-steam-launch /usr/local/bin/steamdeck-steam 2>/dev/null
|
||||
ln -sf /usr/local/bin/bannerlator-steam-shim /usr/local/bin/steam 2>/dev/null
|
||||
python3 /usr/local/bin/bannerlator-desktop-games 2>&1 | sed 's/^/[games] /'
|
||||
# EXPERIMENT: Valve's x86-64 client under FEX (bannerlator-steam-x64), in a terminal so its
|
||||
# first-run downloads show. Not a Game entry, so droiddeck-desktop-gpu leaves its Exec alone.
|
||||
cat > /usr/share/applications/droiddeck-steam-x64.desktop <<'ENTRY'
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=Steam x86-64 (FEX, experimental)
|
||||
Comment=Valve's x86-64 Linux client under FEX, in its own home with its own login
|
||||
Exec=qterminal -e /usr/local/bin/bannerlator-steam-x64
|
||||
Icon=steam
|
||||
Categories=System;
|
||||
ENTRY
|
||||
# Player 1 on the controller in every emulator that has no profile of its own yet - now, and again
|
||||
# every half minute: an emulator started for the first time writes its own config, which is then
|
||||
# given the controller for its next start without waiting for the next desktop.
|
||||
|
||||
@@ -252,10 +252,36 @@ case $mode in
|
||||
esac
|
||||
;;
|
||||
steam)
|
||||
step "checking the Steam client (first run downloads it, ~1-2 min with nothing on screen)"
|
||||
bannerlator-steam-install || exit $?
|
||||
step "Steam client ready"
|
||||
steam_root=$XDG_DATA_HOME/Steam
|
||||
# BL_STEAM_CLIENT_ARCH=x86_64 (the Steam page's Client setting): Valve's x86-64 client under FEX
|
||||
# instead of the arm64 one, in a home of its own (bannerlator-steam-x64), for what the arm64
|
||||
# client cannot do - VAC. Everything this session does around the client applies to it; what is
|
||||
# only for the arm64 client (its ARM64 Proton, the library mapping, the probes that load its
|
||||
# steamclient.so) is skipped.
|
||||
steam_x64=0
|
||||
[ "${BL_STEAM_CLIENT_ARCH:-}" = x86_64 ] && steam_x64=1
|
||||
if [ "$steam_x64" = 1 ]; then
|
||||
step "checking the x86-64 Steam client (FEX, experimental; the first run downloads ~2 GB)"
|
||||
bannerlator-steam-x64 --prepare || exit $?
|
||||
step "x86-64 Steam client ready"
|
||||
steam_root=${BL_X64_HOME:-$HOME/.droiddeck-x64}/.local/share/Steam
|
||||
steam_client=(bannerlator-steam-x64 --exec)
|
||||
# The client's browser stays in software: on the GPU its GPU process needs libgbm, which FEX
|
||||
# does not thunk, and the guest GBM cannot share buffers with the Adreno (device: GPU process
|
||||
# restart loop, no first frame). Games still get the GPU through the thunks.
|
||||
# BL_X64_CEF_GPU=1 tries it anyway.
|
||||
if [ "${BL_X64_CEF_GPU:-0}" = 1 ] && bannerlator-steam-x64 --gpu; then
|
||||
steam_cef=(-cef-force-gpu -cef-ozone-platform=x11 -cef-use-gl=angle -cef-use-angle=vulkan)
|
||||
else
|
||||
steam_cef=(-cef-disable-gpu)
|
||||
fi
|
||||
else
|
||||
step "checking the Steam client (first run downloads it, ~1-2 min with nothing on screen)"
|
||||
bannerlator-steam-install || exit $?
|
||||
step "Steam client ready"
|
||||
steam_root=$XDG_DATA_HOME/Steam
|
||||
steam_client=("$steam_root/steamrtarm64/steam")
|
||||
steam_cef=(-cef-force-gpu -cef-ozone-platform=x11 -cef-use-gl=angle -cef-use-angle=vulkan)
|
||||
fi
|
||||
# Register the card library (bound at /mnt/droiddeck-sd) with the client - internal storage is
|
||||
# its main library already - and the ARM64 Proton that lets it launch the Windows ones.
|
||||
# A request left in ~/.bl-proton-extra installs a third-party Proton before the client starts,
|
||||
@@ -263,7 +289,7 @@ case $mode in
|
||||
# arguments to bannerlator-proton-extra: "ge", "cachyos", "ge --tag GE-Proton11-7", or a URL.
|
||||
# Hundreds of megabytes each, so it is only ever done on request, and the line is dropped once
|
||||
# it has succeeded - a failed download stays and is retried next session.
|
||||
if [ -s "$HOME/.bl-proton-extra" ]; then
|
||||
if [ "$steam_x64" = 0 ] && [ -s "$HOME/.bl-proton-extra" ]; then
|
||||
_bl_left=$HOME/.bl-proton-extra.left
|
||||
: > "$_bl_left"
|
||||
while read -r _bl_extra || [ -n "$_bl_extra" ]; do
|
||||
@@ -307,7 +333,7 @@ case $mode in
|
||||
for ((_i = 0; _i < 720; _i++)); do
|
||||
for _d in /proc/[0-9]*; do
|
||||
IFS= read -r -d '' _c < "$_d/cmdline" 2>/dev/null || continue
|
||||
case $_c in *steamwebhelper*|*steamrtarm64/steam|*gamescope*) ;; *) continue ;; esac
|
||||
case $_c in *steamwebhelper*|*steamrtarm64/steam|*steamrt64/steam|*gamescope*) ;; *) continue ;; esac
|
||||
_t=("$_d"/task/*)
|
||||
_p=${_d#/proc/}
|
||||
[ "${_bl_pinned[$_p]:-}" = "${#_t[@]}" ] && continue
|
||||
@@ -319,6 +345,7 @@ case $mode in
|
||||
) &
|
||||
fi
|
||||
|
||||
if [ "$steam_x64" = 0 ]; then
|
||||
step "registering the app's game library with Steam"
|
||||
bannerlator-steam-library "$steam_root" || true
|
||||
step "setting up the ARM64 Proton compatibility tool"
|
||||
@@ -332,9 +359,10 @@ case $mode in
|
||||
# Steam runs a game's shared redistributables inside its prefix on first launch; marking
|
||||
# them as already run keeps those x86 installers from starting under emulation at all.
|
||||
bannerlator-seed-redists "$steam_root" || true
|
||||
fi
|
||||
# The compatibility tool runs Valve's arm64 Proton, which the client fetches like any depot.
|
||||
# Asked for on the command line the first time, so a Windows title has it before it is needed.
|
||||
if [ ! -f "$steam_root/steamapps/appmanifest_4427310.acf" ]; then
|
||||
if [ "$steam_x64" = 0 ] && [ ! -f "$steam_root/steamapps/appmanifest_4427310.acf" ]; then
|
||||
step "asking the client to fetch the ARM64 Proton"
|
||||
set -- "$@" steam://install/4427310
|
||||
# Ported from Bannerlator (bannerlator-session, device-proven 2026-09-22). The client decides
|
||||
@@ -354,7 +382,7 @@ case $mode in
|
||||
bannerlator-steam-compat "$steam_root" >/dev/null 2>&1 || true
|
||||
step "compatibility layer installed: restarting the Steam client once"
|
||||
: > "$HOME/.bl-restart-once"
|
||||
"$steam_root/steamrtarm64/steam" -shutdown >/dev/null 2>&1 || true
|
||||
"${steam_client[@]}" -shutdown >/dev/null 2>&1 || true
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
@@ -389,7 +417,7 @@ case $mode in
|
||||
sleep 10
|
||||
[ -f "$_bl_acf" ] && exit 0
|
||||
step "signed in: asking the client again for the ARM64 Proton (try $_bl_try of 3)"
|
||||
"$steam_root/steamrtarm64/steam" steam://install/4427310 >/dev/null 2>&1 || true
|
||||
"${steam_client[@]}" steam://install/4427310 >/dev/null 2>&1 || true
|
||||
sleep 50
|
||||
done
|
||||
exit 0
|
||||
@@ -491,7 +519,7 @@ case $mode in
|
||||
# reads from has changed since its last run - a new manifest, a new depot or tool, the client
|
||||
# rewriting config.vdf - which [ -nt ] answers without starting anything, and once a minute
|
||||
# regardless.
|
||||
(
|
||||
[ "$steam_x64" = 0 ] && (
|
||||
beat=0
|
||||
compat_stamp=$HOME/.bl-compat-beat
|
||||
rm -f "$compat_stamp"
|
||||
@@ -504,6 +532,31 @@ case $mode in
|
||||
done
|
||||
return 1
|
||||
}
|
||||
# A title the compat pass maps while the client runs only takes that mapping at the client's
|
||||
# next start (see bannerlator-steam-compat): until then the client launches it its own way -
|
||||
# a Linux build stuck on "Downloading content (0%)" for a Steam Linux Runtime and FEX that
|
||||
# cannot run here (Left 4 Dead 2, 2026-10-02). So restart the client once for it, through the
|
||||
# same marker and -shutdown as the ARM64 Proton's restart: only when every new title has
|
||||
# finished installing (StateFlags 4) and no game is running; another download just resumes.
|
||||
# Once per title and three times per session at most, so a client that rewrites config.vdf
|
||||
# can never loop it.
|
||||
compat_pending=$HOME/.bl-compat-pending
|
||||
compat_restarted=$HOME/.bl-compat-restarted
|
||||
rm -f "$compat_pending"
|
||||
: > "$compat_restarted"
|
||||
compat_restarts=0
|
||||
compat_settled() {
|
||||
local app m flags
|
||||
while read -r app; do
|
||||
[ -n "$app" ] || continue
|
||||
m="$steam_root/steamapps/appmanifest_$app.acf"
|
||||
[ -f "$m" ] || m="/mnt/droiddeck-sd/steamapps/appmanifest_$app.acf"
|
||||
[ -f "$m" ] || return 1
|
||||
flags=$(sed -n 's/^[[:space:]]*"StateFlags"[[:space:]]*"\([0-9]*\)".*/\1/p' "$m" | head -n 1)
|
||||
[ "$flags" = 4 ] || return 1
|
||||
done < "$compat_pending"
|
||||
! pgrep -f 'reaper SteamLaunch' >/dev/null 2>&1
|
||||
}
|
||||
for ((_i = 0; _i < 720; _i++)); do
|
||||
nap 5
|
||||
beat=$((beat + 1))
|
||||
@@ -511,7 +564,17 @@ case $mode in
|
||||
bannerlator-seed-redists "$steam_root" >/dev/null 2>&1 || true
|
||||
if compat_due; then
|
||||
: > "$compat_stamp"
|
||||
bannerlator-steam-compat "$steam_root" >/dev/null 2>&1 || true
|
||||
bannerlator-steam-compat "$steam_root" --live >/dev/null 2>&1 || true
|
||||
fi
|
||||
if [ -s "$compat_pending" ] && compat_settled; then
|
||||
_bl_new=$(sort -u "$compat_pending" | grep -vxF -f "$compat_restarted" || true)
|
||||
rm -f "$compat_pending"
|
||||
[ -n "$_bl_new" ] && [ "$compat_restarts" -lt 3 ] || continue
|
||||
compat_restarts=$((compat_restarts + 1))
|
||||
printf '%s\n' $_bl_new >> "$compat_restarted"
|
||||
step "newly installed title(s) $(echo $_bl_new) set to the ARM64 Proton: restarting the Steam client once so it takes effect"
|
||||
: > "$HOME/.bl-restart-once"
|
||||
"${steam_client[@]}" -shutdown >/dev/null 2>&1 || true
|
||||
fi
|
||||
done
|
||||
) &
|
||||
@@ -584,7 +647,7 @@ BUSCONF
|
||||
# reaped with the client session. Native ARM64 runs directly. A legacy x86_64 asset is used
|
||||
# only when its recorded architecture and this guest's existing FEX installation agree.
|
||||
decky_loader=$HOME/homebrew/services/PluginLoader
|
||||
if [ -f "$HOME/.droiddeck-decky-enabled" ] && [ -x "$decky_loader" ]; then
|
||||
if [ "$steam_x64" = 0 ] && [ -f "$HOME/.droiddeck-decky-enabled" ] && [ -x "$decky_loader" ]; then
|
||||
decky_arch=$(cat "$HOME/homebrew/services/.droiddeck-decky-arch" 2>/dev/null || echo unknown)
|
||||
if [ "$decky_arch" = arm64 ] || { [ "$decky_arch" = x86_64 ] && [ -x /usr/bin/FEX ] && [ -n "${FEX_ROOTFS:-}" ]; }; then
|
||||
(
|
||||
@@ -629,7 +692,10 @@ BUSCONF
|
||||
# is an update that never installs: exit 42, a restart, for ever (Bannerlator 38996ea1). The
|
||||
# loop below puts package/beta back before every start. The SteamOS helpers the client calls
|
||||
# in this mode are staged as no-op stubs under /usr/bin.
|
||||
if [ "${BL_STEAMDECK:-0}" = 1 ] && [ "${BL_STEAM_UI:-}" != desktop ]; then
|
||||
# Not for the x86-64 client yet: with -steamos3 its SteamOS audio manager aborts on this runtime's
|
||||
# PulseAudio 13 server (pa_operation_get_state assertion) right after start. Big Picture without
|
||||
# the Deck flags runs.
|
||||
if [ "${BL_STEAMDECK:-0}" = 1 ] && [ "${BL_STEAM_UI:-}" != desktop ] && [ "$steam_x64" = 0 ]; then
|
||||
_bl_ui+=(-steamdeck -steamos3)
|
||||
# The QAM's Scaling Filter lists NVIDIA Image Scaling only when told gamescope has it (it does).
|
||||
export STEAM_GAMESCOPE_NIS_SUPPORTED=1
|
||||
@@ -639,7 +705,7 @@ BUSCONF
|
||||
# low-memory killer), and the client then finds .crash - opening its update UI on the spinner -
|
||||
# and the web helper's Singleton* locks in htmlcache, owned by a process that is gone, which can
|
||||
# leave the Gamepad UI blank. No client or helper is running at this point in a session.
|
||||
if ! pgrep -f 'steamwebhelper|steamrtarm64/steam' >/dev/null 2>&1; then
|
||||
if ! pgrep -f 'steamwebhelper|steamrtarm64/steam|steamrt64/steam' >/dev/null 2>&1; then
|
||||
_stale=""
|
||||
for _f in "$steam_root/.crash" "$steam_root/steam.pid" \
|
||||
"$steam_root/config/htmlcache/SingletonLock" "$steam_root/config/htmlcache/SingletonCookie" \
|
||||
@@ -658,14 +724,14 @@ BUSCONF
|
||||
# again on the branch it just wrote.
|
||||
steam_channel=${BL_STEAM_CHANNEL:-publicbeta}
|
||||
steam_beta=$steam_root/package/beta
|
||||
if [ -n "${BL_ADDED_GAMES:-}" ] && [ -f "$BL_ADDED_GAMES" ]; then
|
||||
if [ "$steam_x64" = 0 ] && [ -n "${BL_ADDED_GAMES:-}" ] && [ -f "$BL_ADDED_GAMES" ]; then
|
||||
bannerlator-steam-games watch "$steam_root" "$BL_ADDED_GAMES" &
|
||||
games_watcher=$!
|
||||
fi
|
||||
# Lossless Scaling (Steam app 993090): whether this account owns it, asked of the signed-in
|
||||
# client once a session until the answer is yes. From then on the app offers importing
|
||||
# Lossless.dll and passes BL_LOSSLESS_OWNED=1, so the question is never asked again.
|
||||
if [ "${BL_LOSSLESS_OWNED:-0}" != 1 ]; then
|
||||
if [ "$steam_x64" = 0 ] && [ "${BL_LOSSLESS_OWNED:-0}" != 1 ]; then
|
||||
(
|
||||
owned=$HOME/.config/droiddeck/lossless-owned
|
||||
for ((_i = 0; _i < 60; _i++)); do
|
||||
@@ -680,7 +746,7 @@ BUSCONF
|
||||
done
|
||||
) &
|
||||
fi
|
||||
echo "== steam args: ${_bl_ui[*]} -clientbeta $steam_channel -no-cef-sandbox -cef-force-gpu -cef-ozone-platform=x11 -cef-use-gl=angle -cef-use-angle=vulkan $*"
|
||||
echo "== steam args: ${_bl_ui[*]} -clientbeta $steam_channel -no-cef-sandbox ${steam_cef[*]} $*$([ "$steam_x64" = 1 ] && echo ' (x86-64 client under FEX)')"
|
||||
# An update that cannot install asks for a restart (rc=42) every time; the count is capped so a
|
||||
# session can never again loop in silence, which reads as a menu opening and closing by itself.
|
||||
steam_updates=0
|
||||
@@ -689,11 +755,21 @@ BUSCONF
|
||||
rm -f "$BL_LAUNCH_DIR/steam-stop"
|
||||
(
|
||||
while :; do
|
||||
# A steam:// URL left in $BL_LAUNCH_DIR/steam-url is handed to the running client (the
|
||||
# client binary started with a URL passes it on and exits): how a game is started, or
|
||||
# anything else the client takes a URL for, without touching the screen.
|
||||
if [ -s "$BL_LAUNCH_DIR/steam-url" ]; then
|
||||
_bl_req=$(head -n 1 "$BL_LAUNCH_DIR/steam-url")
|
||||
rm -f "$BL_LAUNCH_DIR/steam-url"
|
||||
case $_bl_req in
|
||||
steam://*) echo "== steam: handing the client $_bl_req"; "${steam_client[@]}" "$_bl_req" >/dev/null 2>&1 & ;;
|
||||
esac
|
||||
fi
|
||||
if [ -e "$BL_LAUNCH_DIR/steam-stop" ]; then
|
||||
rm -f "$BL_LAUNCH_DIR/steam-stop"
|
||||
: > "$HOME/.bl-stop-requested"
|
||||
echo "== steam: the app asked for a clean exit"
|
||||
"$steam_root/steamrtarm64/steam" -shutdown >/dev/null 2>&1 || true
|
||||
"${steam_client[@]}" -shutdown >/dev/null 2>&1 || true
|
||||
exit 0
|
||||
fi
|
||||
nap 0.5
|
||||
@@ -708,13 +784,12 @@ BUSCONF
|
||||
echo "$steam_channel" > "$steam_beta"
|
||||
fi
|
||||
step "starting the Steam client${BL_STEAM_UI:+ ($BL_STEAM_UI UI)}"
|
||||
"$steam_root/steamrtarm64/steam" "${_bl_ui[@]}" -clientbeta "$steam_channel" \
|
||||
-no-cef-sandbox -cef-force-gpu -cef-ozone-platform=x11 \
|
||||
-cef-use-gl=angle -cef-use-angle=vulkan "$@"
|
||||
"${steam_client[@]}" "${_bl_ui[@]}" -clientbeta "$steam_channel" \
|
||||
-no-cef-sandbox "${steam_cef[@]}" "$@"
|
||||
rc=$?
|
||||
echo "== steam exited rc=$rc"
|
||||
collect_steam_logs
|
||||
if [ -n "${BL_ADDED_GAMES:-}" ] && [ -f "$BL_ADDED_GAMES" ]; then
|
||||
if [ "$steam_x64" = 0 ] && [ -n "${BL_ADDED_GAMES:-}" ] && [ -f "$BL_ADDED_GAMES" ]; then
|
||||
bannerlator-steam-shortcuts "$steam_root" "$BL_ADDED_GAMES" || true
|
||||
fi
|
||||
if [ -f "$HOME/.bl-stop-requested" ]; then
|
||||
@@ -725,6 +800,9 @@ BUSCONF
|
||||
# arguments, which were acted on. Not counted against the update cap.
|
||||
if [ -f "$HOME/.bl-restart-once" ]; then
|
||||
echo "== steam: restarting once so the new compatibility layer is in the client's lists"
|
||||
# The client wrote config.vdf back from its own memory as it exited, which can drop the
|
||||
# per-title entries written while it ran - the very ones this restart is for.
|
||||
bannerlator-steam-compat "$steam_root" >/dev/null 2>&1 || true
|
||||
set --
|
||||
continue
|
||||
fi
|
||||
|
||||
@@ -189,6 +189,37 @@ bl_idtech3() {
|
||||
}
|
||||
"""
|
||||
|
||||
# VAC diagnostics (the app's Setup page): a Source-engine game says whether it started VAC-secure
|
||||
# only in its own console, and writes that console to <mod>/console.log only with -condebug. Armed
|
||||
# per session by the app (~/.droiddeck-vac-diagnostics); each launch is noted for its summary.
|
||||
# Nothing else about the launch changes - this only asks the game to keep its own log.
|
||||
BL_VACDIAG_SETUP = """
|
||||
bl_vacdiag() {
|
||||
[ -f "${HOME:-/root}/.droiddeck-vac-diagnostics" ] || return 0
|
||||
[ "${verb:-}" = waitforexitandrun ] || return 0
|
||||
local exe= a dir added=no app=${SteamAppId:-${STEAM_COMPAT_APP_ID:-0}}
|
||||
for a in "$@"; do case $a in *.exe|*.EXE) exe=$a ;; esac; done
|
||||
[ -n "$exe" ] || return 0
|
||||
dir=$(dirname "$exe")
|
||||
# Source: <game>/<mod>/gameinfo.txt beside the exe. Source 2: <game>/game/bin/win64/<exe> with
|
||||
# <game>/game/<mod>/gameinfo.gi.
|
||||
if ls "$dir"/*/gameinfo.txt >/dev/null 2>&1 || ls "$dir"/../../*/gameinfo.gi >/dev/null 2>&1; then
|
||||
case " $* " in
|
||||
*" -condebug "*) echo "bannerlator-proton: vac-diag: app $app already has -condebug" >&2 ;;
|
||||
*) BL_EXTRA_ARGS+=(-condebug); added=yes
|
||||
echo "bannerlator-proton: vac-diag: app $app is a Source title; launched with -condebug" >&2 ;;
|
||||
esac
|
||||
else
|
||||
added=not-source
|
||||
echo "bannerlator-proton: vac-diag: app $app is not a Source title; it keeps no console log to read" >&2
|
||||
fi
|
||||
case " $* " in
|
||||
*" -insecure "*) echo "bannerlator-proton: vac-diag: app $app has -insecure in its launch options - it can never be VAC-secure this way" >&2 ;;
|
||||
esac
|
||||
printf '%s\tapp %s\tcondebug %s\t%s\n' "$(date +%s)" "$app" "$added" "${exe##*/}" >> "${HOME:-/root}/.droiddeck-vac-launches"
|
||||
}
|
||||
"""
|
||||
|
||||
BL_DIRECTAUDIO_SETUP = """
|
||||
bl_directaudio() {
|
||||
base=$1
|
||||
@@ -271,6 +302,7 @@ if [ -n "$depot" ]; then
|
||||
bl_directaudio "$depot"
|
||||
bl_game_cpus
|
||||
bl_idtech3 "$@"
|
||||
bl_vacdiag "$@"
|
||||
bl_libpath
|
||||
exec ${BL_TASKSET:-} /usr/local/bin/bannerlator-game-env "$depot/proton" "$verb" "$@" "${BL_EXTRA_ARGS[@]}"
|
||||
fi
|
||||
@@ -282,7 +314,7 @@ exit 1
|
||||
# something else.
|
||||
LAUNCHER_SH = LAUNCHER_SH.replace(
|
||||
"verb=$1; shift", "verb=$1; shift\n" + BL_DIRECTAUDIO_SETUP + BL_GAME_CPUS_SETUP + BL_IDTECH3_SETUP
|
||||
+ BL_COMPONENTS_SETUP + BL_LIBPATH_SETUP, 1)
|
||||
+ BL_VACDIAG_SETUP + BL_COMPONENTS_SETUP + BL_LIBPATH_SETUP, 1)
|
||||
|
||||
# A third-party Proton dropped into compatibilitytools.d - GE-Proton, proton-cachyos, anything of
|
||||
# that shape - is adopted rather than used as shipped, for the same two reasons Valve's ARM64 depot
|
||||
@@ -318,6 +350,7 @@ bl_components "$here"
|
||||
bl_directaudio "$here"
|
||||
bl_game_cpus
|
||||
bl_idtech3 "$@"
|
||||
bl_vacdiag "$@"
|
||||
bl_libpath
|
||||
# Proton takes its base directory from dirname(sys.argv[0]) without resolving it, so it has to be
|
||||
# started from its own tree.
|
||||
@@ -503,7 +536,7 @@ def adopt_extras(tools):
|
||||
body = (EXTRA_WRAPPER_SH % entry_point).replace(
|
||||
'here=$(dirname "$0")',
|
||||
'here=$(dirname "$0")\n' + BL_DIRECTAUDIO_SETUP + BL_GAME_CPUS_SETUP + BL_IDTECH3_SETUP
|
||||
+ BL_COMPONENTS_SETUP + BL_LIBPATH_SETUP, 1)
|
||||
+ BL_VACDIAG_SETUP + BL_COMPONENTS_SETUP + BL_LIBPATH_SETUP, 1)
|
||||
write_changed(wrapper, body, 0o755)
|
||||
write_changed(manifest, EXTRA_MANIFEST_VDF)
|
||||
write_changed(compat_vdf, EXTRA_COMPAT_TOOL_VDF % (name, entry))
|
||||
@@ -546,7 +579,8 @@ def installed_apps(steam_root):
|
||||
|
||||
|
||||
def register_default(config_path, apps=(), name=None, protect=()):
|
||||
"""Writes the mapping. `name` defaults to our own tool; the first-run seed passes Valve's."""
|
||||
"""Writes the mapping. `name` defaults to our own tool; the first-run seed passes Valve's.
|
||||
Returns the app ids given a per-app entry by this call."""
|
||||
tool = name or TOOL
|
||||
# Tool names the user may deliberately have chosen (see adopt_extras); never repointed.
|
||||
keep = {n.lower() for n in protect}
|
||||
@@ -555,7 +589,8 @@ def register_default(config_path, apps=(), name=None, protect=()):
|
||||
tokens = tokenize(original)
|
||||
steam = find_block(tokens, ["InstallConfigStore", "Software", "Valve", "Steam"])
|
||||
if steam is None:
|
||||
return
|
||||
return []
|
||||
added = []
|
||||
mapping = find_block(tokens[steam + 1:], ["CompatToolMapping"])
|
||||
entry = ['"0"', "{", '"name"', '"%s"' % tool, '"config"', '""', '"priority"', '"75"', "}"]
|
||||
if mapping is None:
|
||||
@@ -579,10 +614,12 @@ def register_default(config_path, apps=(), name=None, protect=()):
|
||||
continue
|
||||
tokens[mapping + 1:mapping + 1] = [
|
||||
'"%s"' % app, "{", '"name"', '"%s"' % tool, '"config"', '""', '"priority"', '"250"', "}"]
|
||||
added.append(app)
|
||||
# Nothing to change is the usual case on the session's beat: the client's own file is left alone.
|
||||
text = serialize(tokens)
|
||||
if text != serialize(tokenize(original)):
|
||||
write_changed(config_path, text)
|
||||
return added
|
||||
|
||||
|
||||
def depot_build(depot):
|
||||
@@ -633,8 +670,18 @@ def main():
|
||||
config = os.path.join(steam_root, "config", "config.vdf")
|
||||
if os.path.isfile(config):
|
||||
apps = installed_apps(steam_root)
|
||||
register_default(config, apps, protect=extras)
|
||||
added = register_default(config, apps, protect=extras)
|
||||
print("bannerlator-steam-compat: default and %d installed title(s) set to %s" % (len(apps), TOOL))
|
||||
# The client reads config.vdf once, as it starts, and keeps its own decisions in memory for
|
||||
# the rest of its run: a title installed while it runs gets the client's own pick (native
|
||||
# for a Linux build, which then waits for Steam Linux Runtime + FEX it can never run here;
|
||||
# proton-stable for a Windows one), and the entry written here only counts from the next
|
||||
# start. Run from the session's beat (--live), the new entries are noted for the session,
|
||||
# which restarts the client once the install has settled (bannerlator-session).
|
||||
if added and "--live" in sys.argv[2:]:
|
||||
with open(os.path.join(os.path.expanduser("~"), ".bl-compat-pending"), "a") as f:
|
||||
f.write("".join("%s\n" % a for a in added))
|
||||
print("bannerlator-steam-compat: mapped while the client runs: %s" % " ".join(added))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
+472
@@ -0,0 +1,472 @@
|
||||
#!/bin/bash
|
||||
# EXPERIMENT, opt-in: Valve's x86-64 Linux Steam client under FEX, beside the default arm64 one.
|
||||
# bannerlator-steam-x64 [steam:// URL or client arguments] run it (the desktop menu entry)
|
||||
# bannerlator-steam-x64 --prepare install and set up, then exit
|
||||
# bannerlator-steam-x64 --exec ARGS... run the client binary with ARGS in
|
||||
# its environment (bannerlator-session
|
||||
# with BL_STEAM_CLIENT_ARCH=x86_64)
|
||||
#
|
||||
# Why: the arm64 Linux client never runs VAC (TF2, L4D2: "Connection Blocked ... VAC secured
|
||||
# server", no VAC line in any client log) - VAC modules are x86 code. The x86-64 client under FEX
|
||||
# has a module it can run, the way SteamLite does in Wine. This runs Valve's client unchanged.
|
||||
#
|
||||
# Everything lives in its own home ($X64_HOME): the x86 client gets its own ~/.steam links, its
|
||||
# own login, config and library, and never touches the arm64 client's. Only one client runs at a
|
||||
# time (same account), so the arm64 one is shut down first.
|
||||
#
|
||||
# First run downloads FEX (this repo's release), FEX's Ubuntu 24.04 x86 rootfs (~525 MB, unpacked
|
||||
# ~1.5 GB) and the x86 client (~600 MB). Log: $X64_HOME/steam-x64.log.
|
||||
set -eu
|
||||
FEX_TAG=fex-2609-r3
|
||||
FEX_SHA256=cf3a87805ec51f8c42e197ff4883575e5bdec5247abe4c0beefb1796db358b5f
|
||||
FEX_URL=https://github.com/Droid-Deck/DroidDeck/releases/download/$FEX_TAG/fex.tzst
|
||||
ROOTFS_URL=https://rootfs.fex-emu.gg/Ubuntu_24_04/2026-08-11/Ubuntu_24_04.sqsh
|
||||
CDN=https://client-update.fastly.steamstatic.com
|
||||
MANIFEST=steam_client_publicbeta_ubuntu12
|
||||
|
||||
REAL_HOME=${HOME:-/root}
|
||||
X64_HOME=${BL_X64_HOME:-$REAL_HOME/.droiddeck-x64}
|
||||
ROOTFS=$X64_HOME/rootfs
|
||||
STEAM_X64=$X64_HOME/.local/share/Steam
|
||||
ARM_STEAM=${XDG_DATA_HOME:-$REAL_HOME/.local/share}/Steam
|
||||
LOG=$X64_HOME/steam-x64.log
|
||||
mkdir -p "$X64_HOME"
|
||||
step() { echo "== $(date -u +%H:%M:%S) $*" | tee -a "$LOG"; }
|
||||
|
||||
x64_prepare() {
|
||||
# --- FEX -------------------------------------------------------------------------------------
|
||||
if [ ! -x /usr/local/bin/FEX ] || [ "$(cat /usr/local/share/fex-emu/.droiddeck-tag 2>/dev/null)" != "$FEX_TAG" ]; then
|
||||
step "downloading FEX ($FEX_TAG)"
|
||||
tmp=$(mktemp -d)
|
||||
curl -fL --progress-bar "$FEX_URL" -o "$tmp/fex.tzst"
|
||||
if [ "$FEX_SHA256" != PENDING ]; then echo "$FEX_SHA256 $tmp/fex.tzst" | sha256sum -c --quiet; fi
|
||||
zstd -dc "$tmp/fex.tzst" | tar -C / -x
|
||||
rm -rf "$tmp"
|
||||
mkdir -p /usr/local/share/fex-emu && echo "$FEX_TAG" > /usr/local/share/fex-emu/.droiddeck-tag
|
||||
fi
|
||||
step "FEX ready ($FEX_TAG)"
|
||||
|
||||
# --- x86 rootfs --------------------------------------------------------------------------------
|
||||
if [ ! -f "$ROOTFS/.droiddeck-complete" ]; then
|
||||
step "downloading FEX's Ubuntu 24.04 x86 rootfs"
|
||||
# Resumes a part-done download; a finished one answers 416, which is not a failure here.
|
||||
curl -fL --progress-bar -C - "$ROOTFS_URL" -o "$X64_HOME/rootfs.sqsh" || {
|
||||
rc=$?
|
||||
[ "$rc" = 22 ] && [ -s "$X64_HOME/rootfs.sqsh" ] || { step "rootfs download failed (curl $rc)"; exit 1; }
|
||||
}
|
||||
step "unpacking the rootfs"
|
||||
rm -rf "$ROOTFS"
|
||||
# Ownership and xattrs mean nothing in this guest; device nodes cannot be made here and are not
|
||||
# needed (FEX passes /dev through from the host).
|
||||
/usr/local/bin/unsquashfs -no-xattrs -no-progress -ignore-errors -d "$ROOTFS" "$X64_HOME/rootfs.sqsh" >>"$LOG" 2>&1 || true
|
||||
[ -x "$ROOTFS/usr/bin/bash" ] || { step "rootfs unpack failed (no usr/bin/bash); see $LOG"; exit 1; }
|
||||
rm -f "$X64_HOME/rootfs.sqsh"
|
||||
touch "$ROOTFS/.droiddeck-complete"
|
||||
fi
|
||||
# The rootfs's own ld.so.preload: a path missing from the rootfs falls through to the guest, whose
|
||||
# list names the session's arm64 preloads. In it, the session shim built for x86-64 (System V IPC,
|
||||
# which Android kernels lack: the client's tier0 gives up on start without its semaphore).
|
||||
# Each architecture's copy under the rootfs's own multiarch folder, named once through glibc's $LIB
|
||||
# (lib/x86_64-linux-gnu or lib/i386-linux-gnu): every process loads the one built for it, so the
|
||||
# 32-bit games get them too. A missing copy is skipped by the loader.
|
||||
: > "$ROOTFS/etc/ld.so.preload"
|
||||
for lib in libblsession libfakeinput; do
|
||||
have=0
|
||||
# The 32-bit controller reader crashes Left 4 Dead 2: its fake devices reach the game's own SDL2
|
||||
# through udev with a missing attribute, and SDL strdup()s the NULL (device, segvlog: strlen in
|
||||
# libc <- __strdup <- libSDL2 <- udev_device_get_sysattr_value). Until it is fixed, 32-bit
|
||||
# programs get only the session shim; its preload line names the x86_64 copy outright, since
|
||||
# with $LIB FEX falls through to the session's own /usr/local/lib/i386-linux-gnu copy.
|
||||
arches="x86_64 i386"; [ "$lib" = libfakeinput ] && arches=x86_64
|
||||
rm -f "$ROOTFS/usr/local/lib/i386-linux-gnu/libfakeinput.so"
|
||||
for arch in $arches; do
|
||||
[ -f "/usr/local/lib/$arch-linux-gnu/$lib.so" ] || continue
|
||||
mkdir -p "$ROOTFS/usr/local/lib/$arch-linux-gnu"
|
||||
cp -f "/usr/local/lib/$arch-linux-gnu/$lib.so" "$ROOTFS/usr/local/lib/$arch-linux-gnu/$lib.so"
|
||||
have=1
|
||||
done
|
||||
if [ "$have" = 1 ] && [ "$lib" = libfakeinput ]; then
|
||||
echo "/usr/local/lib/x86_64-linux-gnu/$lib.so" >> "$ROOTFS/etc/ld.so.preload"
|
||||
elif [ "$have" = 1 ]; then echo "/usr/local/\$LIB/$lib.so" >> "$ROOTFS/etc/ld.so.preload"
|
||||
else step "warning: no x86 build of $lib (/usr/local/lib/x86_64-linux-gnu/$lib.so)"; fi
|
||||
done
|
||||
rm -f "$ROOTFS/usr/local/lib/libblsession-x86_64.so" "$ROOTFS/usr/local/lib/libfakeinput-x86_64.so"
|
||||
# --- the x86-64 client -------------------------------------------------------------------------
|
||||
if [ ! -x "$STEAM_X64/steamrt64/steam" ] || [ ! -f "$STEAM_X64/package/droiddeck-installed" ]; then
|
||||
step "downloading the x86-64 Steam client ($MANIFEST)"
|
||||
work=$(mktemp -d)
|
||||
curl -fsSL "$CDN/$MANIFEST" -o "$work/manifest"
|
||||
version=$(awk -F'"' '$2 == "version" { print $4; exit }' "$work/manifest")
|
||||
# Same parsing as bannerlator-steam-install: every component block's plain zip and sha256.
|
||||
awk -F'"' '
|
||||
/^\t"/ && NF >= 3 && $4 == "" { block = $2 }
|
||||
$2 == "file" { file[block] = $4 }
|
||||
$2 == "sha2" { sha[block] = $4 }
|
||||
END { for (b in file) if (b ~ /_all$/ || b ~ /_ubuntu12$/) print b, file[b], sha[b] }
|
||||
' "$work/manifest" | sort > "$work/parts"
|
||||
[ -s "$work/parts" ] || { step "no client components in $MANIFEST"; exit 1; }
|
||||
mkdir -p "$STEAM_X64/package"
|
||||
total=$(grep -c . "$work/parts")
|
||||
i=0
|
||||
while read -r block file sum; do
|
||||
i=$((i + 1))
|
||||
step "client: $block ($i/$total)"
|
||||
curl -fsSL "$CDN/$file" -o "$work/$file"
|
||||
echo "$sum $work/$file" | sha256sum -c --quiet
|
||||
unzip -qo "$work/$file" -d "$STEAM_X64"
|
||||
rm -f "$work/$file"
|
||||
done < "$work/parts"
|
||||
rm -rf "$work"
|
||||
echo publicbeta > "$STEAM_X64/package/beta"
|
||||
echo "$version" > "$STEAM_X64/package/droiddeck-installed"
|
||||
chmod +x "$STEAM_X64/steam.sh" "$STEAM_X64/steamrt64/steam" "$STEAM_X64/ubuntu12_32/steam" 2>/dev/null || true
|
||||
fi
|
||||
# Its own library: the arm64 client's Windows installs are not what this client runs (it would
|
||||
# switch a shared install to the Linux build and the arm64 client would download it again).
|
||||
[ -L "$STEAM_X64/steamapps" ] && rm -f "$STEAM_X64/steamapps"
|
||||
mkdir -p "$STEAM_X64/steamapps"
|
||||
# They still enumerate /dev/dri, find the session's KGSL node there, and spin on it (x86 Turnip
|
||||
# "failed to query kernel driver version", KMS dumb-buffer ioctls in a loop) - the webhelper never
|
||||
# got a browser up. FEX looks in the rootfs first: an empty /dev/dri there hides the node.
|
||||
mkdir -p "$ROOTFS/dev/dri"
|
||||
# What steam.sh sets up before it runs the client, for the x86 tree.
|
||||
mkdir -p "$X64_HOME/.steam"
|
||||
ln -sfn "$STEAM_X64" "$X64_HOME/.steam/root"
|
||||
ln -sfn "$STEAM_X64" "$X64_HOME/.steam/steam"
|
||||
ln -sfn "$STEAM_X64/ubuntu12_32" "$X64_HOME/.steam/bin32"
|
||||
ln -sfn "$STEAM_X64/ubuntu12_64" "$X64_HOME/.steam/bin64"
|
||||
ln -sfn "$STEAM_X64/linux32" "$X64_HOME/.steam/sdk32"
|
||||
ln -sfn "$STEAM_X64/linux64" "$X64_HOME/.steam/sdk64"
|
||||
# Unlike the arm64 client, this one carries pressure-vessel (steamrt64/pv-runtime) and refuses to
|
||||
# start when steam-runtime-check-requirements' bwrap probe cannot make a user namespace, which no
|
||||
# Android app can. proot is the container here, as it is for the arm64 client, which has no such
|
||||
# probe. STEAM_RUNTIME_STEAMRT names the runtime the client takes the probe from: Valve's own,
|
||||
# linked file by file, with only the probe answering that the requirements are met.
|
||||
srt_real=$STEAM_X64/steamrt64/pv-runtime/steam-runtime-steamrt
|
||||
srt_shim=$X64_HOME/steamrt-shim
|
||||
if [ -d "$srt_real" ]; then
|
||||
rm -rf "$srt_shim"
|
||||
mkdir -p "$srt_shim/bin"
|
||||
for e in "$srt_real"/* "$srt_real"/.[!.]*; do
|
||||
[ -e "$e" ] || continue
|
||||
[ "$(basename "$e")" = bin ] || ln -s "$e" "$srt_shim/$(basename "$e")"
|
||||
done
|
||||
for e in "$srt_real"/bin/*; do ln -s "$e" "$srt_shim/bin/$(basename "$e")"; done
|
||||
rm -f "$srt_shim/bin/steam-runtime-check-requirements"
|
||||
printf '#!/bin/sh
|
||||
# bannerlator-steam-x64: proot stands in for user namespaces here.
|
||||
exit 0
|
||||
' > "$srt_shim/bin/steam-runtime-check-requirements"
|
||||
chmod 755 "$srt_shim/bin/steam-runtime-check-requirements"
|
||||
# steam-runtime-launch-client --alongside-steam [options] -- COMMAND: no container to leave here.
|
||||
rm -f "$srt_shim/bin/steam-runtime-launch-client"
|
||||
cat > "$srt_shim/bin/steam-runtime-launch-client" <<'LAUNCH'
|
||||
#!/bin/sh
|
||||
# bannerlator-steam-x64: no SteamRT container here, so "alongside Steam" is right here - run it.
|
||||
while [ $# -gt 0 ]; do
|
||||
case $1 in
|
||||
--) shift; break ;;
|
||||
--directory=*) cd "${1#--directory=}" || exit 127 ;;
|
||||
--env=*) export "${1#--env=}" ;;
|
||||
--unset-env=*) unset "${1#--unset-env=}" ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
[ $# -gt 0 ] || exit 0
|
||||
exec "$@"
|
||||
LAUNCH
|
||||
chmod 755 "$srt_shim/bin/steam-runtime-launch-client"
|
||||
fi
|
||||
# The libraries the SteamRT container would give the client and the rootfs lacks (GTK 2 for
|
||||
# steamui.so, ...): Valve's own platform runtime files, for what the rootfs does not have.
|
||||
# They ship without their soname links (pressure-vessel makes those when it builds the container),
|
||||
# so they are linked into a folder of our own and the rootfs's ldconfig adds the soname links there.
|
||||
srt_libs=$X64_HOME/steamrt-libs
|
||||
rm -rf "$srt_libs"
|
||||
mkdir -p "$srt_libs"
|
||||
for d in "$STEAM_X64"/steamrt64/pv-runtime/steam-runtime-steamrt/steamrt3c_platform_*/files/lib/x86_64-linux-gnu \
|
||||
"$STEAM_X64"/steamrt64/pv-runtime/steam-runtime-steamrt/steamrt3c_platform_*/files/usr/lib/x86_64-linux-gnu; do
|
||||
[ -d "$d" ] || continue
|
||||
for f in "$d"/*.so*; do [ -e "$srt_libs/$(basename "$f")" ] || ln -s "$f" "$srt_libs/"; done
|
||||
done
|
||||
FEX_ROOTFS=$ROOTFS /usr/local/bin/FEX "$ROOTFS/usr/sbin/ldconfig.real" -n "$srt_libs" >>"$LOG" 2>&1 || step "ldconfig on $srt_libs failed"
|
||||
# LD_LIBRARY_PATH beats the system folders whatever its order, so only what the rootfs lacks stays:
|
||||
# the platform's own (older) glibc and the rest must not shadow the rootfs's.
|
||||
for f in "$srt_libs"/*; do
|
||||
n=$(basename "$f")
|
||||
if [ -e "$ROOTFS/usr/lib/x86_64-linux-gnu/$n" ] || [ -e "$ROOTFS/lib/x86_64-linux-gnu/$n" ]; then rm -f "$f"; fi
|
||||
done
|
||||
# Native Linux games: Steam runs them in the Steam Linux Runtime container (pressure-vessel), which
|
||||
# needs bwrap - no user namespaces here, as for the client. "DroidDeck direct" runs them with
|
||||
# Valve's scout library runtime instead, directly under FEX; the titles below are mapped to it.
|
||||
tool=$STEAM_X64/compatibilitytools.d/droiddeck-direct
|
||||
mkdir -p "$tool"
|
||||
cat > "$tool/compatibilitytool.vdf" <<'VDF'
|
||||
"compatibilitytools"
|
||||
{
|
||||
"compat_tools"
|
||||
{
|
||||
"droiddeck-direct"
|
||||
{
|
||||
"install_path" "."
|
||||
"display_name" "DroidDeck direct (no container)"
|
||||
"from_oslist" "linux"
|
||||
"to_oslist" "linux"
|
||||
}
|
||||
}
|
||||
}
|
||||
VDF
|
||||
cat > "$tool/toolmanifest.vdf" <<'VDF'
|
||||
"manifest"
|
||||
{
|
||||
"version" "2"
|
||||
"commandline" "/run %verb%"
|
||||
}
|
||||
VDF
|
||||
cat > "$tool/run" <<'RUN'
|
||||
#!/bin/sh
|
||||
# DroidDeck direct: a native Linux game with Valve's scout library runtime, no container.
|
||||
verb=$1; shift
|
||||
rt=$HOME/.local/share/Steam/ubuntu12_32/steam-runtime.old
|
||||
[ -x "$rt/run.sh" ] || rt=$HOME/.local/share/Steam/ubuntu12_32/steam-runtime
|
||||
# Source games keep their console in console.log (-condebug): what the VAC diagnostics read - the
|
||||
# server they joined and whether it is secure.
|
||||
case "$*" in *hl2.sh*|*hl2_linux*|*srcds*) set -- "$@" -condebug ;; esac
|
||||
# The GPU: FEX's host thunks and the session's GPU settings, saved by the launcher (the client
|
||||
# itself draws in software). BL_X64_GAME_SOFTWARE=1 keeps the game in software too. game-gpu.env is
|
||||
# rewritten whenever the launcher runs; game-gpu.local (yours, never rewritten) is read after it.
|
||||
gpu=$HOME/game-gpu.env
|
||||
if [ -f "$gpu" ] && [ "${BL_X64_GAME_SOFTWARE:-0}" != 1 ]; then
|
||||
. "$gpu"
|
||||
[ -f "$HOME/game-gpu.local" ] && . "$HOME/game-gpu.local"
|
||||
else
|
||||
# GL threading is a GPU-path tuning from the session; with software GL under FEX it is no help.
|
||||
export mesa_glthread=false
|
||||
fi
|
||||
case $verb in
|
||||
run|waitforexitandrun) exec "$rt/run.sh" "$@" ;;
|
||||
*) exec "$@" ;;
|
||||
esac
|
||||
RUN
|
||||
chmod 755 "$tool/run"
|
||||
# The mapping lives in config.vdf, which the client rewrites from memory as it exits: written
|
||||
# here, while no x86 client runs.
|
||||
if [ -f "$STEAM_X64/config/config.vdf" ] && ! pgrep -f 'steamrt64/steam( |$)' >/dev/null 2>&1; then
|
||||
python3 - "$STEAM_X64/config/config.vdf" ${BL_X64_DIRECT_APPS:-550} <<'PY' || step "could not map titles to DroidDeck direct"
|
||||
import re, sys
|
||||
path, apps = sys.argv[1], sys.argv[2:]
|
||||
s = open(path, encoding="utf-8", errors="surrogateescape").read()
|
||||
|
||||
def block(text, key):
|
||||
"""(start of the key, index after its opening brace, index of its closing brace) or None."""
|
||||
m = re.search(r'"%s"\s*\{' % re.escape(key), text)
|
||||
if not m:
|
||||
return None
|
||||
depth, k = 1, m.end()
|
||||
while depth:
|
||||
c = text[k]
|
||||
depth += c == "{"
|
||||
depth -= c == "}"
|
||||
k += 1
|
||||
return m.start(), m.end(), k - 1
|
||||
|
||||
if not block(s, "CompatToolMapping"):
|
||||
st = block(s, "Steam")
|
||||
if not st:
|
||||
sys.exit("no Steam section")
|
||||
s = s[:st[1]] + '\n\t\t\t\t"CompatToolMapping"\n\t\t\t\t{\n\t\t\t\t}' + s[st[1]:]
|
||||
for app in apps:
|
||||
_, open_, close = block(s, "CompatToolMapping")
|
||||
inner = s[open_:close]
|
||||
old = block(inner, app)
|
||||
if old:
|
||||
inner = inner[:old[0]] + inner[old[2] + 1:]
|
||||
entry = ('\n\t\t\t\t\t"%s"\n\t\t\t\t\t{\n\t\t\t\t\t\t"name"\t\t"droiddeck-direct"\n'
|
||||
'\t\t\t\t\t\t"config"\t\t""\n\t\t\t\t\t\t"priority"\t\t"250"\n\t\t\t\t\t}' % app)
|
||||
s = s[:open_] + entry + inner + s[close:]
|
||||
open(path, "w", encoding="utf-8", errors="surrogateescape").write(s)
|
||||
PY
|
||||
fi
|
||||
# A FEX process takes its rootfs from FEXServer, not from its own environment, and a server
|
||||
# started earlier without FEX_ROOTFS (any FEX run in this guest) answers with no rootfs at all:
|
||||
# every x86 library lookup then falls through to the arm64 guest. Stop it while no x86 client
|
||||
# runs, so the next FEX starts one with this rootfs.
|
||||
if ! pgrep -f 'steamrt64/steam( |$)' >/dev/null 2>&1; then
|
||||
FEX_ROOTFS=$ROOTFS /usr/local/bin/FEXServer -k >/dev/null 2>&1 || true
|
||||
fi
|
||||
}
|
||||
|
||||
# Whether the client draws on the GPU: FEX built with its host thunks (Vulkan and GL to the runtime's
|
||||
# own drivers), unless BL_X64_SOFTWARE=1. Without them, software rendering (llvmpipe).
|
||||
x64_gpu() {
|
||||
[ "${BL_X64_SOFTWARE:-0}" != 1 ] && [ -d /usr/local/share/fex-emu/GuestThunks ] && [ -d /usr/local/lib/fex-emu/HostThunks ]
|
||||
}
|
||||
|
||||
# The environment the client and everything it starts run in.
|
||||
x64_env() {
|
||||
# The arm64 session's library paths and preloads are aarch64 and mean nothing to x86 processes.
|
||||
unset LD_LIBRARY_PATH LD_PRELOAD STEAM_RUNTIME STEAM_RUNTIME_PREFER_HOST_LIBRARIES
|
||||
# The client and its browser draw in software, as they always have: with the thunks the browser's
|
||||
# GPU process (even under -cef-disable-gpu it probes GL and Vulkan) gets the host drivers, fails
|
||||
# ("MESA-LOADER: failed to retrieve device information") and dies, and the client never opens a
|
||||
# window (device: 0 windows, "Starting display and audio" forever). Games get the GPU: the session's
|
||||
# GPU settings are saved for DroidDeck direct, which hands them, and FEX's host thunks, to the game.
|
||||
gpu_vars="GALLIUM_DRIVER MESA_LOADER_DRIVER_OVERRIDE VK_ICD_FILENAMES VK_DRIVER_FILES LIBGL_KOPPER_DRI2 ZINK_DEBUG ZINK_DESCRIPTORS TU_DEBUG MESA_NO_ERROR mesa_glthread"
|
||||
rm -f "$X64_HOME/game-gpu.env"
|
||||
if x64_gpu; then
|
||||
printf '{"ThunksDB": {"GL": 1, "Vulkan": 1}}\n' > "$X64_HOME/thunks.json"
|
||||
{
|
||||
echo "export FEX_THUNKCONFIG='$X64_HOME/thunks.json'"
|
||||
echo "unset LIBGL_ALWAYS_SOFTWARE LIBGL_DRI3_DISABLE GALLIUM_DRIVER"
|
||||
for v in $gpu_vars; do
|
||||
eval "val=\${$v-}"; [ -n "$val" ] && printf "export %s='%s'\n" "$v" "$val"
|
||||
done
|
||||
} > "$X64_HOME/game-gpu.env"
|
||||
fi
|
||||
unset FEX_THUNKCONFIG $gpu_vars
|
||||
# x86 Mesa has no driver for this GPU, so software: llvmpipe for GL (the client's own window needs
|
||||
# GLX), and the rootfs's own Vulkan ICDs (lavapipe) found by the x86 loader instead of the arm64 one.
|
||||
export LIBGL_ALWAYS_SOFTWARE=1 GALLIUM_DRIVER=llvmpipe
|
||||
# The X server hands out the GPU node (DRI3 open), and Mesa's software path then tries KMS dumb
|
||||
# buffers on it in a loop: present through plain X instead.
|
||||
export LIBGL_DRI3_DISABLE=1
|
||||
# The proot fast path (libblfastpath, preloaded into every guest process, FEX included) stands down
|
||||
# by itself when FEX is about to run a 32-bit program (it broke FEX's 32-bit mode: SIGILL at start);
|
||||
# 64-bit ones keep it. BL_X64_FP_OFF=1 turns it off for everything, as before.
|
||||
[ "${BL_X64_FP_OFF:-0}" = 1 ] && export PROOT_FP_OFF=1
|
||||
# FEX tuning: the app's default preset for games (core/FexPreset.kt PERFORMANCE_TSO, WinNative's
|
||||
# default) - FEX's own defaults keep half-barrier TSO and full-precision x87, which cost every x86
|
||||
# program time. A value already in the environment wins.
|
||||
export FEX_TSOENABLED=${FEX_TSOENABLED:-1} FEX_VECTORTSOENABLED=${FEX_VECTORTSOENABLED:-0} \
|
||||
FEX_MEMCPYSETTSOENABLED=${FEX_MEMCPYSETTSOENABLED:-0} FEX_HALFBARRIERTSOENABLED=${FEX_HALFBARRIERTSOENABLED:-0} \
|
||||
FEX_X87REDUCEDPRECISION=${FEX_X87REDUCEDPRECISION:-1} FEX_MULTIBLOCK=${FEX_MULTIBLOCK:-1}
|
||||
export HOME=$X64_HOME
|
||||
# The session points these at the arm64 client's home; this client has its own.
|
||||
export XDG_DATA_HOME=$X64_HOME/.local/share XDG_CONFIG_HOME=$X64_HOME/.config XDG_CACHE_HOME=$X64_HOME/.cache
|
||||
export FEX_ROOTFS=$ROOTFS
|
||||
export STEAM_EXTRA_COMPAT_TOOLS_PATHS=
|
||||
# Valve's 64-bit SteamRT3 client (steamrt64/steam), the x86 twin of the arm64 one, started the way
|
||||
# the session starts that one (bannerlator-session): directly, STEAM_RUNTIME=1, its own library
|
||||
# folders first. Through steam.sh it would first run steam-runtime-check-requirements, whose
|
||||
# bwrap probe needs user namespaces - proot has none. Software rendering: no x86 GPU driver.
|
||||
export STEAM_RUNTIME=1
|
||||
[ -d "$X64_HOME/steamrt-shim" ] && export STEAM_RUNTIME_STEAMRT=$X64_HOME/steamrt-shim
|
||||
# The client believes it is inside the SteamRT container (the recursion guard), so it starts games
|
||||
# and system commands "alongside Steam" through steam-runtime-launch-client: Valve's asks a launcher
|
||||
# service on the session bus (there is none - the game ended at once), and without one on PATH the
|
||||
# launch fails (No such file). The shim's own launch client runs the command right here.
|
||||
[ -d "$X64_HOME/steamrt-shim/bin" ] && export PATH=$X64_HOME/steamrt-shim/bin:$PATH
|
||||
export LD_LIBRARY_PATH=$STEAM_X64/steamrt64:$STEAM_X64/steamrt64/panorama:$X64_HOME/steamrt-libs
|
||||
# After an update the client re-executes itself inside the SteamRT container through
|
||||
# pressure-vessel-wrap, which needs bwrap. The FEX rootfs with the client's own steamrt64 libraries
|
||||
# is that container here; the client's recursion guard says it is already inside one.
|
||||
export STEAM_STEAMRT_RECURSION_GUARD=1
|
||||
# proot is a tracer, so tier0 believes a debugger is attached and breaks into it with int3 on the
|
||||
# first failed assertion: the web helper died of SIGTRAP a second after every start. The session
|
||||
# shim hides the tracer from the x86 web helper too (preload/tracer.c).
|
||||
export BL_WEBHELPER_HIDE_TRACER=1
|
||||
}
|
||||
|
||||
# The client binary under FEX, for the session: the starting arguments, or -shutdown / a steam://
|
||||
# URL handed to the running instance.
|
||||
x64_exec() {
|
||||
x64_env
|
||||
cd "$STEAM_X64"
|
||||
exec "${BL_FEX:-/usr/local/bin/FEX}" "$STEAM_X64/steamrt64/steam" "$@"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
--prepare) x64_prepare; exit 0 ;;
|
||||
--exec) shift; x64_exec "$@" ;;
|
||||
--gpu) x64_gpu; exit $? ;;
|
||||
esac
|
||||
|
||||
# --- standalone (the desktop menu entry) ------------------------------------------------------
|
||||
# --- one client at a time ----------------------------------------------------------------------
|
||||
if pgrep -f 'steamrtarm64/steam( |$)' >/dev/null 2>&1; then
|
||||
step "shutting down the arm64 client first"
|
||||
"$ARM_STEAM/steamrtarm64/steam" -shutdown >/dev/null 2>&1 || true
|
||||
for _ in $(seq 1 30); do pgrep -f 'steamrtarm64/steam( |$)' >/dev/null 2>&1 || break; sleep 1; done
|
||||
fi
|
||||
x64_prepare
|
||||
step "FEX, rootfs and client ready"
|
||||
# BL_FEXSERVER_LOG: a FEXServer of our own in the foreground, logging there - every FEX process that
|
||||
# logs to "server" (FEX_OUTPUTLOG, or an AppConfig) then has its lines tagged with its pid.
|
||||
if [ -n "${BL_FEXSERVER_LOG:-}" ]; then
|
||||
FEX_ROOTFS=$ROOTFS "$(dirname "${BL_FEX:-/usr/local/bin/FEX}")/FEXServer" -f -p 600 >"$BL_FEXSERVER_LOG" 2>&1 &
|
||||
sleep 1
|
||||
fi
|
||||
# --- what bannerlator-session gives the arm64 client ---------------------------------------
|
||||
# The client's connection header and Settings > Internet come from NetworkManager on the SYSTEM
|
||||
# bus; without them it shows no connection while downloads run. Same bus config and stand-in as the
|
||||
# session, started here when no system bus is up (a desktop has none), stopped with the client.
|
||||
x64_bus= x64_nm= x64_pin=
|
||||
x64_cleanup() {
|
||||
[ -n "$x64_pin" ] && kill "$x64_pin" 2>/dev/null
|
||||
[ -n "$x64_nm" ] && kill "$x64_nm" 2>/dev/null
|
||||
[ -n "$x64_bus" ] && kill "$x64_bus" 2>/dev/null
|
||||
return 0
|
||||
}
|
||||
trap x64_cleanup EXIT
|
||||
if ! pgrep -f 'dbus-daemon --config-file=/usr/local/share/bannerlator/system-bus.conf' >/dev/null 2>&1 \
|
||||
&& command -v dbus-daemon >/dev/null 2>&1 && command -v bannerlator-netmanager >/dev/null 2>&1; then
|
||||
mkdir -p /run/dbus /usr/local/share/bannerlator
|
||||
cat > /usr/local/share/bannerlator/system-bus.conf <<'BUSCONF'
|
||||
<!DOCTYPE busconfig PUBLIC "-//freedesktop//DTD D-Bus Bus Configuration 1.0//EN"
|
||||
"http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
|
||||
<busconfig>
|
||||
<type>system</type>
|
||||
<listen>unix:path=/run/dbus/system_bus_socket</listen>
|
||||
<auth>EXTERNAL</auth>
|
||||
<policy context="default">
|
||||
<allow user="*"/>
|
||||
<allow own="*"/>
|
||||
<allow send_destination="*"/>
|
||||
<allow receive_sender="*"/>
|
||||
</policy>
|
||||
</busconfig>
|
||||
BUSCONF
|
||||
rm -f /run/dbus/system_bus_socket
|
||||
dbus-daemon --config-file=/usr/local/share/bannerlator/system-bus.conf --nofork --nopidfile &
|
||||
x64_bus=$!
|
||||
for _ in $(seq 1 50); do [ -S /run/dbus/system_bus_socket ] && break; sleep 0.1; done
|
||||
if [ -S /run/dbus/system_bus_socket ]; then
|
||||
bannerlator-netmanager &
|
||||
x64_nm=$!
|
||||
step "network: system bus up, NetworkManager stand-in running"
|
||||
else
|
||||
step "network: the system bus did not come up; the client will show no connection"
|
||||
fi
|
||||
fi
|
||||
# What a killed client leaves behind: .crash opens the update UI on its spinner, stale htmlcache
|
||||
# Singleton* locks can leave the UI blank (as in bannerlator-session).
|
||||
if ! pgrep -f 'steamrt64/steam( |$)' >/dev/null 2>&1; then
|
||||
for f in "$STEAM_X64/.crash" "$STEAM_X64/steam.pid" "$STEAM_X64/config/htmlcache/SingletonLock" \
|
||||
"$STEAM_X64/config/htmlcache/SingletonCookie" "$STEAM_X64/config/htmlcache/SingletonSocket"; do
|
||||
{ [ -e "$f" ] || [ -L "$f" ]; } && rm -f "$f"
|
||||
done
|
||||
fi
|
||||
# The session's CPU set for the client (BL_CLIENT_CPUS): Steam pins itself and its web helper to a
|
||||
# few cores; re-pin every 5 s as the session does for the arm64 client.
|
||||
if [ -n "${BL_CLIENT_CPUS:-}" ] && command -v taskset >/dev/null 2>&1; then
|
||||
(
|
||||
while sleep 5; do
|
||||
for pid in $(ps -A -o PID,ARGS | awk '$2=="FEX" && ($3 ~ /steamrt64\/steam$/ || $3 ~ /steamwebhelper$/) {print $1}'); do
|
||||
taskset -a -p -c "$BL_CLIENT_CPUS" "$pid" >/dev/null 2>&1
|
||||
done
|
||||
done
|
||||
) &
|
||||
x64_pin=$!
|
||||
fi
|
||||
# BL_FEX: another FEX build for the client and everything it starts (a DEBUG_STRACE one, say).
|
||||
# Restart on request (42, as steam.sh does), at most a few times.
|
||||
for attempt in 1 2 3 4 5; do
|
||||
# The branch on disk must match -clientbeta before every start, or the client loops on rc=42.
|
||||
echo publicbeta > "$STEAM_X64/package/beta"
|
||||
step "starting the x86-64 SteamRT3 client under FEX (attempt $attempt): $*"
|
||||
rc=0
|
||||
if [ "${BL_X64_CEF_GPU:-0}" = 1 ] && x64_gpu; then cef=(-cef-force-gpu -cef-ozone-platform=x11 -cef-use-gl=angle -cef-use-angle=vulkan); else cef=(-cef-disable-gpu); fi
|
||||
( x64_exec -clientbeta publicbeta -no-cef-sandbox "${cef[@]}" "$@" ) >>"$LOG" 2>&1 || rc=$?
|
||||
step "client exited rc=$rc"
|
||||
[ "$rc" = 42 ] || break
|
||||
done
|
||||
@@ -483,12 +483,12 @@ static FILE *synthesize(const char *table) {
|
||||
}
|
||||
|
||||
/* tracer.c */
|
||||
int bl_status_without_tracer(const char *path, int flags) __attribute__((visibility("hidden")));
|
||||
int bl_status_without_tracer(const char *path, int flags, const void *caller) __attribute__((visibility("hidden")));
|
||||
|
||||
static FILE *open_stream(const char *symbol, const char *path, const char *mode) {
|
||||
typedef FILE *(*fopen_fn)(const char *, const char *);
|
||||
int status = mode && mode[0] == 'r' && !strchr(mode, '+')
|
||||
? bl_status_without_tracer(path, O_RDONLY | O_CLOEXEC) : -1;
|
||||
? bl_status_without_tracer(path, O_RDONLY | O_CLOEXEC, NULL) : -1;
|
||||
if (status >= 0) {
|
||||
FILE *copy = fdopen(status, "r");
|
||||
if (copy == NULL) close(status);
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
#include <string.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
int bl_status_without_tracer(const char *path, int flags) __attribute__((visibility("hidden")));
|
||||
int bl_status_without_tracer(const char *path, int flags, const void *caller) __attribute__((visibility("hidden")));
|
||||
int bl_writable_retry(int dirfd, const char *path, int flags) __attribute__((visibility("hidden")));
|
||||
|
||||
static int retry_without_noatime(int fd, int flags) {
|
||||
@@ -60,7 +60,7 @@ int open(const char *path, int flags, ...) {
|
||||
errno = EACCES;
|
||||
return -1;
|
||||
}
|
||||
if ((fd = bl_status_without_tracer(path, flags)) >= 0) return fd;
|
||||
if ((fd = bl_status_without_tracer(path, flags, __builtin_return_address(0))) >= 0) return fd;
|
||||
if (real_open == NULL) real_open = dlsym(RTLD_NEXT, "open");
|
||||
if (real_open == NULL) {
|
||||
errno = ENOSYS;
|
||||
@@ -86,7 +86,7 @@ int openat(int dirfd, const char *path, int flags, ...) {
|
||||
errno = EACCES;
|
||||
return -1;
|
||||
}
|
||||
if ((fd = bl_status_without_tracer(path, flags)) >= 0) return fd;
|
||||
if ((fd = bl_status_without_tracer(path, flags, __builtin_return_address(0))) >= 0) return fd;
|
||||
if (real_openat == NULL) real_openat = dlsym(RTLD_NEXT, "openat");
|
||||
if (real_openat == NULL) {
|
||||
errno = ENOSYS;
|
||||
|
||||
@@ -57,7 +57,8 @@ static int enabled(void) {
|
||||
if (cached >= 0) return cached;
|
||||
off = getenv("BL_NO_PATH_CACHE");
|
||||
cached = strcmp(program_invocation_short_name, "steam") == 0 &&
|
||||
strstr(program_invocation_name, "steamrtarm64") != NULL &&
|
||||
(strstr(program_invocation_name, "steamrtarm64") != NULL ||
|
||||
strstr(program_invocation_name, "steamrt64") != NULL) &&
|
||||
!(off != NULL && off[0] == '1');
|
||||
return cached;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
#include <unistd.h>
|
||||
|
||||
static int in_steam_dirs(const char *path) {
|
||||
return path != NULL && (strstr(path, "/Steam/steamrtarm64/") != NULL
|
||||
return path != NULL && (strstr(path, "/Steam/steamrtarm64/") != NULL || strstr(path, "/Steam/steamrt64/") != NULL
|
||||
|| strstr(path, "/Steam/ubuntu12_64/") != NULL || strstr(path, "/Steam/linuxarm64/") != NULL);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,10 @@
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include <ctype.h>
|
||||
#include <dlfcn.h>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/syscall.h>
|
||||
@@ -47,9 +49,20 @@ static int is_status_path(const char *path) {
|
||||
}
|
||||
|
||||
/* Chromium stands its hang watchdogs down when it sees a tracer, and the client's web helper does
|
||||
* not come up within their patience here: it is left reading the truth. */
|
||||
static int is_web_helper(void) {
|
||||
return strcmp(program_invocation_short_name, "steamwebhelper") == 0;
|
||||
* not come up within their patience here: it is left reading the truth. Chromium also traps when a
|
||||
* status descriptor it opened turns out not to be procfs. The x86-64 client under FEX
|
||||
* (BL_WEBHELPER_HIDE_TRACER=1, set by bannerlator-steam-x64) still needs its tier0 told: tier0
|
||||
* reads the status, sees the tracer, and breaks into the supposed debugger with int3 on the first
|
||||
* failed assertion - the web helper died of SIGTRAP a second after it started. There tier0's own
|
||||
* opens (by caller) get the copy and Chromium's the real file. */
|
||||
static int is_web_helper(const void *caller) {
|
||||
if (strcmp(program_invocation_short_name, "steamwebhelper") != 0) return 0;
|
||||
const char *hide = getenv("BL_WEBHELPER_HIDE_TRACER");
|
||||
Dl_info info;
|
||||
if (hide && hide[0] == '1' && caller && dladdr(caller, &info) && info.dli_fname
|
||||
&& strstr(info.dli_fname, "libtier0_s.so"))
|
||||
return 0;
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int write_all(int fd, const char *data, size_t len) {
|
||||
@@ -65,13 +78,13 @@ static int write_all(int fd, const char *data, size_t len) {
|
||||
|
||||
/* The descriptor to hand back for a read-only open of a status file, or -1 with errno untouched
|
||||
* when the path is something else or the copy cannot be made, and the real open should run. */
|
||||
__attribute__((visibility("hidden"))) int bl_status_without_tracer(const char *path, int flags) {
|
||||
__attribute__((visibility("hidden"))) int bl_status_without_tracer(const char *path, int flags, const void *caller) {
|
||||
char text[STATUS_MAX];
|
||||
size_t len = 0;
|
||||
int saved = errno;
|
||||
int out = -1;
|
||||
|
||||
if ((flags & O_ACCMODE) != O_RDONLY || !is_status_path(path) || is_web_helper()) return -1;
|
||||
if ((flags & O_ACCMODE) != O_RDONLY || !is_status_path(path) || is_web_helper(caller)) return -1;
|
||||
int in = (int)syscall(SYS_openat, AT_FDCWD, path, O_RDONLY | O_CLOEXEC);
|
||||
if (in < 0) goto done;
|
||||
for (;;) {
|
||||
|
||||
@@ -137,10 +137,37 @@ static int traced_by_match(const char *key) {
|
||||
return found;
|
||||
}
|
||||
|
||||
/*
|
||||
* FEX (the x86-64 Steam client's translator) loads this library into its own process. Its 32-bit
|
||||
* mode does not survive that: every i386 program died with SIGILL, FEX's fatal trap, at start. So
|
||||
* in FEX, only stay on when the program it is about to run (argv[1]) is a 64-bit ELF; a 32-bit
|
||||
* one, a script, or anything that cannot be read gets the real calls, as before.
|
||||
*/
|
||||
static int fex_runs_32bit(void) {
|
||||
if (strcmp(program_invocation_short_name, "FEX") != 0 && strcmp(program_invocation_short_name, "FEXInterpreter") != 0)
|
||||
return 0;
|
||||
char args[PATH_MAX + 64];
|
||||
long fd = syscall(SYS_openat, AT_FDCWD, "/proc/self/cmdline", O_RDONLY | O_CLOEXEC);
|
||||
if (fd < 0) return 1;
|
||||
long n = syscall(SYS_read, fd, args, sizeof args - 1);
|
||||
syscall(SYS_close, fd);
|
||||
if (n <= 0) return 1;
|
||||
args[n] = 0;
|
||||
const char *prog = args + strlen(args) + 1;
|
||||
if (prog >= args + n || !*prog) return 1;
|
||||
unsigned char ident[5];
|
||||
fd = syscall(SYS_openat, AT_FDCWD, prog, O_RDONLY | O_CLOEXEC);
|
||||
if (fd < 0) return 1;
|
||||
n = syscall(SYS_read, fd, ident, sizeof ident);
|
||||
syscall(SYS_close, fd);
|
||||
return !(n == 5 && memcmp(ident, "\x7f" "ELF", 4) == 0 && ident[4] == 2 /* ELFCLASS64 */);
|
||||
}
|
||||
|
||||
static void fp_init(void) {
|
||||
const char *r = getenv("PROOT_FP_ROOT"), *b = getenv("PROOT_FP_BINDS"), *t = getenv("PROOT_FP_TTL_MS");
|
||||
const char *key = getenv("PROOT_FP_KEY");
|
||||
if (!r || !*r || !key || !*key || getenv("PROOT_FP_OFF")) return;
|
||||
if (fex_runs_32bit()) return;
|
||||
void *p = mmap((void *)FP_STUB_ADDR, FP_STUB_SIZE, PROT_READ | PROT_WRITE,
|
||||
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED_NOREPLACE, -1, 0);
|
||||
if (p != (void *)FP_STUB_ADDR) {
|
||||
|
||||
Reference in new issue
Block a user