fix(desktop): Flatpak and Snap apps get the game GPU setting (#2295)

The session's TU_DEBUG=sysmem is meant for the Steam client and
Chromium-based apps. Flatpak and Snap apps inherited it, so emulators
and other programs installed that way rendered through system memory.

droiddeck-game-env now holds the Chromium check next to game_gpu, and a
small droiddeck-game-gpu wrapper applies both to any program. The
AppImage, Flatpak and Snap launchers use it, so the rule lives in one
place.
This commit is contained in:
MaxsTechReview authored and GitHub committed 2026-11-08 14:39:31 -05:00
1 parent 3bb279f635
commit 1760f008ae
9 files changed
+221 -11

No files matched your search

+1 -1
View File
@@ -80,7 +80,7 @@ DroidDeck is [GPL-3.0](LICENSE). Steam and Proton belong to Valve Corporation. V
- The proot fast path orders bindings like [PRoot](https://github.com/proot-me/proot)'s `path/binding.c` (GPL-2.0-or-later).
- DiRT 3 and DiRT Showdown keep their telemetry socket the way RoccoC's DiRTTelemetryErrorFix (MIT) does, based on the fix by Carsten Gottschling.
- Zink on PowerVR keeps colour textures in colour with Icenowy Zheng's upstream [Mesa](https://gitlab.freedesktop.org/mesa/mesa) fix d55418dae7 (MIT), carried as a runtime patch.
- Games and AppImage programs such as emulators use [Mesa](https://gitlab.freedesktop.org/mesa/mesa) Turnip's per render pass memory choice. The Steam client and Chromium-based apps keep `TU_DEBUG=sysmem`.
- Games and AppImage, Flatpak and Snap programs such as emulators use [Mesa](https://gitlab.freedesktop.org/mesa/mesa) Turnip's per render pass memory choice. The Steam client and Chromium-based apps keep `TU_DEBUG=sysmem`.
- Black Ops III and Dark Souls Remastered start without [umu-protonfixes](https://github.com/Open-Wine-Components/umu-protonfixes) (BSD-2-Clause), whose `311210.py` and `570940.py` fail under ARM64 Proton.
- [ReSkate](https://github.com/Dingo-Shenanigans/ReSkate) (GPL-3.0) by Dingo Shenanigans uses the launch option from its README and D3D12 feature level 12_1.
- The arm64 client's sign in was checked by reading Steam's saved token with the `ConnectCache` format from [deadlock-api-ingest](https://github.com/deadlock-api/deadlock-api-ingest).
@@ -40,6 +40,7 @@ object SessionFiles {
WinComponents.publish(context)
val files = arrayOf(
"usr/local/bin/droiddeck-game-env" to "usr/local/bin/droiddeck-game-env",
"usr/local/bin/droiddeck-game-gpu" to "usr/local/bin/droiddeck-game-gpu",
"usr/local/bin/droiddeck-diagnostic-inventory" to "usr/local/bin/droiddeck-diagnostic-inventory",
"usr/local/bin/droiddeck-wincomponents" to "usr/local/bin/droiddeck-wincomponents",
"usr/local/bin/droiddeck-msi-install" to "usr/local/bin/droiddeck-msi-install",
@@ -25,12 +25,6 @@ if [ -n "$(find -H "$APPDIR" -maxdepth 4 \( -name chrome-sandbox -o -name chrome
flags+=(--ozone-platform=x11)
fi
fi
if [ -n "${DROIDDECK_GAME_TU_DEBUG+set}" ]; then
if [ ${#flags[@]} -eq 0 ]; then
if [ -n "$DROIDDECK_GAME_TU_DEBUG" ]; then export TU_DEBUG=$DROIDDECK_GAME_TU_DEBUG; else unset TU_DEBUG; fi
fi
unset DROIDDECK_GAME_TU_DEBUG
fi
qt=$(cat "$dir/qt" 2>/dev/null)
case $qt in wayland|xcb) ;; *) qt=auto ;; esac
wayland=${GAMESCOPE_WAYLAND_DISPLAY:-${WAYLAND_DISPLAY:-}}
@@ -61,6 +55,9 @@ if [ -x /usr/local/bin/droiddeck-fex ]; then
*) run=(/usr/local/bin/droiddeck-fex run --mode "$fex" --for "$APPDIR" -- "$APPDIR/AppRun") ;;
esac
fi
gpu=("${BASH_SOURCE[0]%/*}/droiddeck-game-gpu")
[ ${#flags[@]} -gt 0 ] && gpu+=(--chromium)
run=("${gpu[@]}" -- "${run[@]}")
echo "== appimage $ARGV0 ($APPDIR) ${flags[*]} FEX $fex Qt $qt${QT_QPA_PLATFORM:+ ($QT_QPA_PLATFORM)}"
cd "$APPDIR" || exit 1
started=$(mktemp "${TMPDIR:-/tmp}/droiddeck-appimage.XXXXXX")
@@ -11,8 +11,16 @@ if [ -n "${BL_INSIDE:-}" ] || [ -n "${GAMESCOPE_WAYLAND_DISPLAY:-}" ]; then
export XDG_SESSION_TYPE=x11
socket=(--nosocket=wayland --socket=x11)
fi
app=
for arg in "$@"; do
case $arg in -*) ;; *) app=$arg; break ;; esac
done
gpu=("${BASH_SOURCE[0]%/*}/droiddeck-game-gpu")
location=
[ -n "$app" ] && location=$(flatpak info --show-location "$app" 2>/dev/null)
[ -n "$location" ] && gpu+=(--for "$location/files")
echo "== flatpak run ${socket[*]} $*"
if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ] && command -v dbus-run-session >/dev/null 2>&1; then
exec dbus-run-session -- flatpak run "${socket[@]}" "$@"
exec "${gpu[@]}" -- dbus-run-session -- flatpak run "${socket[@]}" "$@"
fi
exec flatpak run "${socket[@]}" "$@"
exec "${gpu[@]}" -- flatpak run "${socket[@]}" "$@"
@@ -11,6 +11,7 @@ import tempfile
from pathlib import Path
GAME_TU_DEBUG = "DROIDDECK_GAME_TU_DEBUG"
CHROMIUM_FILES = {"chrome-sandbox", "chrome_100_percent.pak", "v8_context_snapshot.bin"}
SYNC_KEYS = ("BL_SYNC", "BL_SYNC_FALLBACK", "BL_FSYNC_FIRST", "BL_SYNC_MIN_NOFILE", "PROTON_NO_ESYNC", "PROTON_NO_FSYNC", "PROTON_NO_NTSYNC", "WINEESYNC")
SEED_REDISTS = "/usr/local/bin/droiddeck-seed-redists"
PACK_KEYS = ("WINESERVER", "BL_SYNC_PACK", "WINEESYNC", "PROTON_NO_ESYNC", "PROTON_NO_FSYNC")
@@ -346,11 +347,23 @@ def win_components(compat, app_id):
return ""
def game_gpu(env):
def chromium_app(folder):
root = os.path.realpath(folder)
for top, dirs, files in os.walk(root):
if CHROMIUM_FILES.intersection(files) or CHROMIUM_FILES.intersection(dirs):
return True
if os.path.relpath(top, root).count(os.sep) >= 2:
dirs[:] = []
return False
def game_gpu(env, keep=False):
if GAME_TU_DEBUG not in env:
return env
result = env.copy()
value = result.pop(GAME_TU_DEBUG)
if keep:
return result
if value:
result["TU_DEBUG"] = value
else:
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/python3
import os
import runpy
import sys
BIN_DIR = os.path.dirname(os.path.abspath(__file__))
def main(argv):
keep = False
folder = None
while argv and argv[0] != "--":
if argv[0] == "--chromium":
keep = True
argv = argv[1:]
elif argv[0] == "--for" and len(argv) > 1:
folder = argv[1]
argv = argv[2:]
else:
break
if argv and argv[0] == "--":
argv = argv[1:]
if not argv:
print("usage: droiddeck-game-gpu [--chromium] [--for FOLDER] -- PROGRAM [ARGS...]", file=sys.stderr)
return 2
env = dict(os.environ)
try:
game_env = runpy.run_path(os.path.join(BIN_DIR, "droiddeck-game-env"))
if folder and not keep:
keep = game_env["chromium_app"](folder)
env = game_env["game_gpu"](env, keep)
except Exception as error:
print("droiddeck-game-gpu: the game GPU setting was not applied (%s)" % error, file=sys.stderr)
try:
os.execvpe(argv[0], argv, env)
except OSError as error:
print("droiddeck-game-gpu: %s: %s" % (argv[0], error.strerror), file=sys.stderr)
return 127
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
@@ -1,11 +1,13 @@
#!/usr/bin/python3
import os
import re
import runpy
import shlex
import sys
import yaml
BIN_DIR = os.path.dirname(os.path.abspath(__file__))
SNAP_DIR = "/snap"
HOST_DIRS = ("/etc", "/home", "/root", "/tmp", "/run", "/var/snap", "/var/lib/snapd", "/var/tmp", "/var/log",
"/media", "/mnt", "/snap", "/usr/src", "/lib/modules")
@@ -59,6 +61,15 @@ def content_binds(name, meta, env):
return out
def game_gpu(env, snap):
try:
game_env = runpy.run_path(os.path.join(BIN_DIR, "droiddeck-game-env"))
return game_env["game_gpu"](env, game_env["chromium_app"](snap))
except Exception as error:
print(f"droiddeck-snap-run: the game GPU setting was not applied ({error})", file=sys.stderr)
return env
def main(argv):
if len(argv) < 3:
print("usage: droiddeck-snap-run SNAP APP [ARG...]", file=sys.stderr)
@@ -72,7 +83,7 @@ def main(argv):
revision = os.path.basename(os.path.realpath(f"{SNAP_DIR}/{name}/current"))
home = os.environ.get("HOME", "/root")
snap = f"{SNAP_DIR}/{name}/{revision}"
env = dict(os.environ)
env = game_gpu(dict(os.environ), snap)
for k in ("LD_PRELOAD", "LD_LIBRARY_PATH"):
env.pop(k, None)
env.update({
+40
View File
@@ -902,6 +902,46 @@ class GameEnvironmentTest(unittest.TestCase):
self.assertEqual(MODULE["apply_config"](env, config, "42")["TU_DEBUG"], "sysmem")
self.assertNotIn("TU_DEBUG", MODULE["apply_config"](env, config, "43"))
def test_a_chromium_app_keeps_the_sessions_turnip_debug(self):
gpu = MODULE["game_gpu"]
self.assertEqual(gpu({"TU_DEBUG": "sysmem", "DROIDDECK_GAME_TU_DEBUG": "", "KEEP": "1"}, True),
{"TU_DEBUG": "sysmem", "KEEP": "1"})
self.assertEqual(gpu({"TU_DEBUG": "sysmem", "DROIDDECK_GAME_TU_DEBUG": "nolrz"}, True), {"TU_DEBUG": "sysmem"})
self.assertEqual(gpu({"TU_DEBUG": "sysmem"}, True), {"TU_DEBUG": "sysmem"})
def test_chromium_files_are_found_four_levels_deep_and_no_further(self):
chromium = MODULE["chromium_app"]
with tempfile.TemporaryDirectory() as tmp:
root = Path(tmp) / "app"
(root / "usr/lib/tool").mkdir(parents=True)
(root / "usr/lib/tool/libtool.so").write_bytes(b"")
self.assertFalse(chromium(str(root)))
for name in ("chrome-sandbox", "chrome_100_percent.pak", "v8_context_snapshot.bin"):
found = root / "a/b/c" / name
found.parent.mkdir(parents=True, exist_ok=True)
found.write_bytes(b"")
self.assertTrue(chromium(str(root)), name)
found.unlink()
deep = root / "a/b/c/d/chrome-sandbox"
deep.parent.mkdir(parents=True)
deep.write_bytes(b"")
self.assertFalse(chromium(str(root)))
self.assertFalse(chromium(str(Path(tmp) / "missing")))
def test_chromium_detection_does_not_follow_links_inside_the_app(self):
chromium = MODULE["chromium_app"]
with tempfile.TemporaryDirectory() as tmp:
other = Path(tmp) / "other"
other.mkdir()
(other / "chrome-sandbox").write_bytes(b"")
root = Path(tmp) / "app"
root.mkdir()
(root / "linked").symlink_to(other, target_is_directory=True)
self.assertFalse(chromium(str(root)))
link = Path(tmp) / "app-link"
link.symlink_to(other, target_is_directory=True)
self.assertTrue(chromium(str(link)))
def test_the_vram_cap_comes_before_the_games_own_dxvk_settings(self):
cap = MODULE["vram_cap"]
self.assertEqual(cap({"BL_VRAM_MB": "4096"})["DXVK_CONFIG"], "dxgi.maxDeviceMemory = 4096")
+98
View File
@@ -0,0 +1,98 @@
import os
import runpy
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
BIN = Path(__file__).resolve().parents[1] / "linuxfs/overlay/usr/local/bin"
KEYS = ("TU_DEBUG", "DROIDDECK_GAME_TU_DEBUG")
SESSION = {"TU_DEBUG": "sysmem", "DROIDDECK_GAME_TU_DEBUG": ""}
class GameGpuLaunchersTest(unittest.TestCase):
def setUp(self):
self.tmp = Path(tempfile.mkdtemp())
self.addCleanup(shutil.rmtree, self.tmp, True)
self.out = self.tmp / "env"
self.tools = self.tmp / "tools"
self.tools.mkdir()
self.app = self.tmp / "deploy"
(self.app / "files/bin").mkdir(parents=True)
self.report = self.tools / "report"
self.report.write_text("#!/bin/sh\nfor k in %s; do eval \"echo $k=\\${$k-unset}\"; done > %s\n"
% (" ".join(KEYS), self.out))
self.report.chmod(0o755)
flatpak = self.tools / "flatpak"
flatpak.write_text("#!/bin/sh\n"
"if [ \"$1\" = info ]; then [ \"$3\" = org.example.App ] && echo %s; exit 0; fi\n"
"exec %s\n" % (self.app, self.report))
flatpak.chmod(0o755)
def chromium(self):
(self.app / "files/lib/browser").mkdir(parents=True)
(self.app / "files/lib/browser/chrome_100_percent.pak").write_bytes(b"")
def run_env(self, command, session):
env = {k: v for k, v in os.environ.items() if k not in KEYS + ("DBUS_SESSION_BUS_ADDRESS", "GAMESCOPE_WAYLAND_DISPLAY")}
env.update(session, PATH="%s:%s" % (self.tools, env.get("PATH", "/usr/bin:/bin")),
DBUS_SESSION_BUS_ADDRESS="unix:path=/dev/null")
result = subprocess.run(command, env=env, capture_output=True, text=True, timeout=30)
self.assertEqual(0, result.returncode, result.stderr)
return dict(line.split("=", 1) for line in self.out.read_text().splitlines())
def wrapper(self, *args, session=SESSION):
return self.run_env([sys.executable, str(BIN / "droiddeck-game-gpu"), *args, "--", str(self.report)], session)
def flatpak(self, session=SESSION):
return self.run_env(["bash", str(BIN / "droiddeck-flatpak-run"), "--command=app", "org.example.App"], session)
def test_the_wrapper_gives_a_program_the_game_setting(self):
self.assertEqual({"TU_DEBUG": "unset", "DROIDDECK_GAME_TU_DEBUG": "unset"}, self.wrapper())
self.assertEqual("gmem", self.wrapper(session={"TU_DEBUG": "sysmem", "DROIDDECK_GAME_TU_DEBUG": "gmem"})["TU_DEBUG"])
def test_the_wrapper_keeps_the_session_setting_for_chromium(self):
self.assertEqual({"TU_DEBUG": "sysmem", "DROIDDECK_GAME_TU_DEBUG": "unset"}, self.wrapper("--chromium"))
self.assertEqual("unset", self.wrapper("--for", str(self.app / "files"))["TU_DEBUG"])
self.chromium()
self.assertEqual("sysmem", self.wrapper("--for", str(self.app / "files"))["TU_DEBUG"])
def test_without_the_game_setting_the_wrapper_changes_nothing(self):
self.assertEqual("sysmem", self.wrapper(session={"TU_DEBUG": "sysmem"})["TU_DEBUG"])
self.assertEqual("unset", self.wrapper(session={})["TU_DEBUG"])
def test_the_wrapper_reports_a_missing_program(self):
result = subprocess.run([sys.executable, str(BIN / "droiddeck-game-gpu"), "--", str(self.tmp / "missing")],
capture_output=True, text=True, timeout=30)
self.assertEqual(127, result.returncode)
self.assertEqual(2, subprocess.run([sys.executable, str(BIN / "droiddeck-game-gpu")], capture_output=True,
timeout=30).returncode)
def test_a_flatpak_app_gets_the_game_setting(self):
self.assertEqual({"TU_DEBUG": "unset", "DROIDDECK_GAME_TU_DEBUG": "unset"}, self.flatpak())
self.assertEqual("nolrz", self.flatpak({"TU_DEBUG": "sysmem", "DROIDDECK_GAME_TU_DEBUG": "nolrz"})["TU_DEBUG"])
def test_a_chromium_flatpak_keeps_the_session_setting(self):
self.chromium()
self.assertEqual({"TU_DEBUG": "sysmem", "DROIDDECK_GAME_TU_DEBUG": "unset"}, self.flatpak())
def test_a_flatpak_without_the_game_setting_is_unchanged(self):
self.assertEqual("sysmem", self.flatpak({"TU_DEBUG": "sysmem"})["TU_DEBUG"])
def test_a_snap_gets_the_game_setting_unless_it_is_chromium(self):
try:
snap_run = runpy.run_path(str(BIN / "droiddeck-snap-run"))
except ImportError:
self.skipTest("PyYAML is not installed")
gpu = snap_run["game_gpu"]
snap = self.app / "files"
self.assertEqual({"KEEP": "1"}, gpu(dict(SESSION, KEEP="1"), str(snap)))
self.assertEqual({"TU_DEBUG": "nolrz"}, gpu({"TU_DEBUG": "sysmem", "DROIDDECK_GAME_TU_DEBUG": "nolrz"}, str(snap)))
self.chromium()
self.assertEqual({"TU_DEBUG": "sysmem"}, gpu(dict(SESSION), str(snap)))
if __name__ == "__main__":
unittest.main()