feat: Mali Vulkan support through MaliBridge (#2027)

This commit is contained in:
Kurt Himebauch authored and GitHub committed 2026-11-02 20:23:21 -05:00
commit 4b8f63d84d
83 files changed
+2331 -80

No files matched your search

+5
View File
@@ -41,6 +41,10 @@ if [[ ! -x "${java_dir}/bin/java" ]]; then
echo "Java 17 not found at ${java_dir}; set JAVA_HOME." >&2
exit 1
fi
if [[ ! -f "${repo_root}/external/MaliBridge/scripts/build-client-linux.sh" ]]; then
echo "external/MaliBridge is empty; run: git submodule update --init" >&2
exit 1
fi
if ! command -v docker >/dev/null 2>&1; then
echo "Docker is required to cross-compile the glibc ARM64 preload libraries." >&2
exit 1
@@ -135,6 +139,7 @@ docker run --rm --platform "${docker_platform}" \
-o "$d/libssbs.so" tools/linuxfs/ssbs/ssbs_adapter.c -ldl
aarch64-linux-gnu-strip --strip-unneeded "$d/libssbs.so"
mkdir -p "$d/usr/local/bin"
external/MaliBridge/scripts/build-client-linux.sh "$d" aarch64-linux-gnu-gcc
aarch64-linux-gnu-gcc -O2 -Wall -Wextra -o "$d/usr/local/bin/droiddeck-clipboard" tools/linuxfs/clipboard/clipboard.c -ldl
aarch64-linux-gnu-strip --strip-unneeded "$d/usr/local/bin/droiddeck-clipboard"
for script in tools/linuxfs/overlay/usr/local/bin/droiddeck-* tools/linuxfs/overlay/usr/local/bin/steam-compatibility; do
+3
View File
@@ -34,6 +34,9 @@ def identity(name, root=ROOT):
for name_ in sorted(set(paths)):
if '__pycache__' in name_ or name_.endswith(('.pyc', '.md')):
continue
if (root / name_).is_dir():
values[name_] = subprocess.check_output(['git', 'ls-files', '--stage', '--', name_], cwd=root, text=True).split()[1]
continue
content = (root / name_).read_bytes()
if b'\0' not in content:
content = content.replace(b'\r\n', b'\n')
+15
View File
@@ -225,6 +225,21 @@
"xz-utils"
]
},
"vkbridge": {
"script": "tools/components/recipes/vkbridge.sh",
"inputs": [
"external/MaliBridge",
"tools/components/recipes/vkbridge.sh"
],
"outputs": [
"app/src/main/assets/linuxfs/usr/local/lib/vkbridge/libvulkan_droidbridge.so",
"app/src/main/assets/linuxfs/usr/local/share/vkbridge/droidbridge_icd.json",
"app/src/main/assets/linuxfs/usr/local/bin/droiddeck-vkbridge-smoke"
],
"packages": [
"gcc-aarch64-linux-gnu"
]
},
"dirt-ws2": {
"script": "tools/components/recipes/dirt-ws2.sh",
"inputs": [
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
test -f external/MaliBridge/scripts/build-client-linux.sh
bash external/MaliBridge/scripts/build-client-linux.sh app/src/main/assets/linuxfs aarch64-linux-gnu-gcc
+4 -2
View File
@@ -114,8 +114,10 @@ fi
if [ "$(cat /usr/share/droiddeck/kwin-compositor 2>/dev/null)" = opengl ]; then
unset LIBGL_ALWAYS_SOFTWARE QT_QUICK_BACKEND
export KWIN_COMPOSE=O2 MESA_LOADER_DRIVER_OVERRIDE=zink GALLIUM_DRIVER=zink LIBGL_KOPPER_DRI2=true
if [ -z "${KWIN_RENDER_NODES:-}" ] && [ -c /dev/dri/renderD0 ]; then
export KWIN_RENDER_NODES=/dev/dri/renderD0
if [ -z "${KWIN_RENDER_NODES:-}" ]; then
for node in /dev/dri/renderD0 /dev/dri/renderD307 /dev/dri/renderD*; do
if [ -c "$node" ]; then export KWIN_RENDER_NODES="$node"; break; fi
done
fi
mkdir -p /run/droiddeck/kwin
printf '#!/bin/sh\nGALLIUM_THREAD=0 exec /usr/bin/kwin_wayland "$@"\n' > /run/droiddeck/kwin/kwin_wayland
@@ -478,6 +478,14 @@ def apply_config(env, config, app_id, fixes=None):
return result
def bridge_feature_level(env):
if env.get("BL_VKBRIDGE") != "1" or env.get("VKD3D_FEATURE_LEVEL") not in ("12_1", "12_2"):
return None
was = env["VKD3D_FEATURE_LEVEL"]
env["VKD3D_FEATURE_LEVEL"] = "12_0"
return "Mali bridge: VKD3D_FEATURE_LEVEL %s capped at 12_0 (no ray tracing or mesh shaders)" % was
GAME_DEBUG_CHANNELS = (
"timestamp", "pid", "tid", "seh", "unwind", "threadname", "debugstr", "loaddll", "mscoree",
)
@@ -974,6 +982,9 @@ def main():
env["AUDIO_DIRECT_MIC"] = "0"
env = directaudio_settings(env)
env = vram_cap(env)
note = bridge_feature_level(env)
if note:
print("droiddeck-game-env: " + note, file=sys.stderr)
env = force_ssbs(command, env)
game_cores(env)
cpu_topology(env, command)
@@ -160,7 +160,7 @@ collect_steam_logs() {
cp "$fex_state/status.json" "$fex_state/config/Config.json" "$BL_DEBUG_DIR/droiddeck-fex/" 2>/dev/null
fi
}
trap 'collect_steam_logs 2>/dev/null || true' TERM INT HUP
trap 'bl_stopping=1; collect_steam_logs 2>/dev/null || true' TERM INT HUP
if [ -n "${BL_VK_DRIVER:-}" ]; then
bl_icd_lib=$(sed -n 's/.*"library_path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$BL_VK_DRIVER" 2>/dev/null | head -1)
bl_icd_lib=$(printf '%s' "$bl_icd_lib" | sed 's|\\/|/|g')
@@ -177,6 +177,83 @@ if [ -n "${BL_VK_DRIVER:-}" ]; then
fi
unset bl_icd_lib
fi
bl_webhelper_layout() {
if [ -n "${BL_VKBRIDGE_WEBHELPER_ARGS:-}" ]; then
printf '%s\n' "$BL_VKBRIDGE_WEBHELPER_ARGS"
elif [ -z "${BL_VKBRIDGE_CEF_ANGLE:-}" ]; then
echo stock
elif [ "$BL_VKBRIDGE_CEF_ANGLE" = vulkan ] && [ "${BL_VKBRIDGE_CEF_VULKAN:-1}" != 0 ]; then
echo "--use-gl=angle --use-angle=vulkan +Vulkan +VulkanFromANGLE +DefaultANGLEVulkan"
else
echo "--use-gl=angle --use-angle=$BL_VKBRIDGE_CEF_ANGLE"
fi
}
bl_now_ms() {
if [ -n "${EPOCHREALTIME:-}" ]; then
local t=${EPOCHREALTIME/[.,]/}
echo $((10#$t / 1000))
else
date +%s%3N
fi
}
vkbridge_wait_socket() {
local sock=$1 limit=${2:-10000} start now waited
[ -n "$sock" ] || return 1
if [ -S "$sock" ]; then
echo "== vkbridge: server socket ready (waited 0 ms)"
return 0
fi
start=$(bl_now_ms)
while :; do
now=$(bl_now_ms)
waited=$((now - start))
if [ -S "$sock" ]; then
echo "== vkbridge: server socket ready (waited $waited ms; the server listens after its startup probe)"
return 0
fi
if [ -n "${bl_stopping:-}" ]; then
echo "== vkbridge: no server socket after $waited ms; the session is stopping"
return 1
fi
if [ "$waited" -ge "$limit" ]; then
echo "== vkbridge: no server socket after $waited ms (gave up at $limit ms)"
return 1
fi
sleep 0.05
done
}
if [ "${BL_VKBRIDGE:-0}" = 1 ]; then
bl_vkb_icd=/usr/local/share/vkbridge/droidbridge_icd.json
[ -z "${BL_INSIDE:-}" ] && vkbridge_wait_socket "${VKBRIDGE_SOCKET:-}" "${BL_VKBRIDGE_WAIT_MS:-10000}"
bl_vkb_missing=
[ -r "$bl_vkb_icd" ] || bl_vkb_missing="$bl_vkb_missing icd"
[ -r /usr/local/lib/vkbridge/libvulkan_droidbridge.so ] || bl_vkb_missing="$bl_vkb_missing library"
[ -S "${VKBRIDGE_SOCKET:-}" ] || bl_vkb_missing="$bl_vkb_missing socket"
if [ -z "$bl_vkb_missing" ]; then
export VK_DRIVER_FILES=$bl_vkb_icd VK_ICD_FILENAMES=$bl_vkb_icd
export VKBRIDGE_SOCKET VKBRIDGE_CLIENT_LOG
if [ -r /root/.vkbridge-env ]; then
while IFS='=' read -r _k _v; do
case "$_k" in VKBRIDGE_*|BL_VKBRIDGE_*) export "$_k=$_v" ;; esac
done < /root/.vkbridge-env
[ -z "${BL_INSIDE:-}" ] && echo "== vkbridge: development settings from /root/.vkbridge-env: $(cut -d= -f1 /root/.vkbridge-env | tr '\n' ' ')"
fi
export VKD3D_FEATURE_LEVEL=${VKD3D_FEATURE_LEVEL:-12_0}
unset TU_DEBUG
if [ -z "${BL_INSIDE:-}" ]; then
step "vkbridge self-test"
timeout 60 /usr/local/bin/droiddeck-vkbridge-smoke 2>&1 | sed 's/^/== vkbridge: /'
if command -v vulkaninfo >/dev/null 2>&1; then
timeout 30 vulkaninfo --summary 2>&1 | sed -n '/^Devices:/,$p' | head -40 | sed 's/^/== vulkaninfo: /'
fi
fi
echo "== vulkan driver: Mali bridge ($bl_vkb_icd, server at $VKBRIDGE_SOCKET)"
else
echo "== vulkan driver: Mali bridge requested but NOT usable (icd $bl_vkb_icd, socket ${VKBRIDGE_SOCKET:-unset}; missing:$bl_vkb_missing)"
[ -n "${BL_INSIDE:-}" ] || exit 69
fi
unset bl_vkb_icd bl_vkb_missing
fi
export GAMESCOPE_FORCE_VULKAN_REALTIME=${BL_GAMESCOPE_REALTIME:-0}
start_agent() {
@@ -307,6 +384,13 @@ if [ -z "${BL_INSIDE:-}" ]; then
if [ "$mode" = steam ] && [ "${BL_CURSOR_COMPOSITE:-0}" = 1 ]; then
export gamescope_cursor_composite=2
fi
# Through the Mali bridge, Steam's UI flickered while gamescope handed its layers to the phone's
# compositor as separate planes, for a reason not yet found; one composited image does not
# (Mali-G76 MC4: 145 footer flips in 6 s at rest, 0 composited).
if [ "$mode" = steam ] && [ "${BL_VKBRIDGE:-0}" = 1 ] && [ -z "${gamescope_composite_force:-}" ]; then
export gamescope_composite_force=1
echo "== gamescope: composites every frame (Mali bridge: the Steam UI flickers on separate planes)"
fi
args=(--backend wayland --expose-wayland -f -W "$width" -H "$height" -w "$nested" -h "$nested_h")
if [ "$nested" != "$width" ] || [ "$nested_h" != "$height" ]; then
args+=(-S stretch)
@@ -1023,6 +1107,28 @@ case $mode in
_bl_cef=(-cef-disable-gpu)
_bl_ui=(-nobigpicture)
fi
if [ "${BL_VKBRIDGE:-0}" = 1 ]; then
# On the GPU the Steam UI held about 400 MB through the bridge (Mali-G76 MC4, 3 GB of RAM); with
# a game loading beside it, Android's low-memory killer took the whole session down.
if [ -z "${BL_VKBRIDGE_CEF_GPU:-}" ]; then
_bl_mem_kb=$(awk '/^MemTotal:/ {print $2}' /proc/meminfo 2>/dev/null)
if [ "${_bl_mem_kb:-0}" -gt 0 ] && [ "$_bl_mem_kb" -lt 4194304 ]; then
BL_VKBRIDGE_CEF_GPU=0
echo "== steam: $((_bl_mem_kb / 1024)) MB of RAM: the browser UI draws in software to leave games the memory"
fi
fi
if [ "${BL_VKBRIDGE_CEF_GPU:-1}" = 0 ]; then
_bl_cef=(-cef-disable-gpu -cef-disable-gpu-compositing)
echo "== steam: browser UI in software (Mali bridge)"
elif [ "${_bl_cef[0]}" = -cef-force-gpu ]; then
_bl_cef=(-cef-force-gpu -cef-use-gl=angle -cef-use-angle="${BL_VKBRIDGE_CEF_ANGLE:-vulkan}")
fi
[ "${BL_VKBRIDGE_CEF_LOG:-0}" = 1 ] && _bl_cef+=(-cef-enable-logging=stderr -cef-v=1)
[ -n "${BL_VKBRIDGE_CEF_EXTRA:-}" ] && read -r -a _bl_cef_extra <<<"$BL_VKBRIDGE_CEF_EXTRA" && _bl_cef+=("${_bl_cef_extra[@]}")
BL_WEBHELPER_ARGS=$(bl_webhelper_layout)
export BL_WEBHELPER_ARGS
echo "== steam: web helper process layout: $BL_WEBHELPER_ARGS (Mali bridge)"
fi
if [ "${BL_STEAM_UI:-}" != desktop ]; then
if [ "$steam_x64" = 0 ] || [ "${BL_X64_DECK:-1}" != 0 ]; then
_bl_ui+=(-steamdeck -steamos3)
+33 -7
View File
@@ -5,6 +5,7 @@
#include <pthread.h>
#include <stdint.h>
#include <stdlib.h>
#include <sys/file.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/syscall.h>
@@ -34,7 +35,8 @@ static int is_kgsl(int fd) {
struct stat st;
pthread_mutex_lock(&lock);
if (kgsl_state == 0) {
kgsl_state = stat("/dev/kgsl-3d0", &st) == 0 ? 1 : -1;
const char *standin = getenv("BL_DRM_STANDIN");
kgsl_state = stat("/dev/kgsl-3d0", &st) == 0 || (standin && *standin && stat(standin, &st) == 0) ? 1 : -1;
kgsl_dev = st.st_rdev;
}
pthread_mutex_unlock(&lock);
@@ -49,18 +51,42 @@ static void *real(const char *name) {
return fn;
}
static int same_by_flock(int a, int b) {
struct stat sa, sb;
if (fstat(a, &sa) || fstat(b, &sb) || sa.st_dev != sb.st_dev || sa.st_ino != sb.st_ino ||
flock(a, LOCK_EX | LOCK_NB)) {
return 0;
}
int same = flock(b, LOCK_EX | LOCK_NB) == 0;
flock(a, LOCK_UN);
return same;
}
static int same_file_description(int a, int b) {
if (a == b) {
return 1;
}
#ifdef SYS_kcmp
int saved_errno = errno;
long ret = syscall(SYS_kcmp, getpid(), getpid(), 0 , a, b);
errno = saved_errno;
return ret == 0;
#else
return 0;
int ret = -1;
#ifdef SYS_kcmp
static int kcmp_missing;
if (!kcmp_missing) {
long k = syscall(SYS_kcmp, getpid(), getpid(), 0, a, b);
if (k >= 0) {
ret = k == 0;
} else if (errno == ENOSYS || errno == EPERM) {
kcmp_missing = 1;
} else {
ret = 0;
}
}
#endif
if (ret < 0) {
const char *standin = getenv("BL_DRM_STANDIN");
ret = standin && *standin ? same_by_flock(a, b) : 0;
}
errno = saved_errno;
return ret;
}
static int find_handle_locked(int fd, uint32_t handle) {
+20
View File
@@ -57,6 +57,26 @@ class ComponentPrepareTest(unittest.TestCase):
script.write_text(script.read_text() + '\nprintf changed\n')
self.assertNotEqual(changed, components.identity('fake', self.root))
def test_identity_follows_a_submodule_commit(self):
library = self.root / 'library'
subprocess.run(['git', 'init', '-q', str(library)], check=True)
def commit(text):
(library / 'source.c').write_text(text)
subprocess.run(['git', '-C', str(library), 'add', '.'], check=True)
subprocess.run(['git', '-C', str(library), '-c', 'user.name=Test Developer', '-c', 'user.email=test@example.test',
'commit', '-qm', text], check=True)
return subprocess.check_output(['git', '-C', str(library), 'rev-parse', 'HEAD'], text=True).strip()
first = commit('int a;\n')
subprocess.run(['git', '-C', str(self.root), 'add', 'library'], check=True)
spec = dict(script='tools/components/recipes/fake.sh', inputs=['library'], outputs=[])
(self.root / components.REGISTRY).write_text(json.dumps(dict(fake=spec)))
original = components.identity('fake', self.root)
commit('int b;\n')
self.assertEqual(original, components.identity('fake', self.root))
subprocess.run(['git', '-C', str(self.root), 'add', 'library'], check=True)
self.assertNotEqual(original, components.identity('fake', self.root))
self.assertNotEqual(first, subprocess.check_output(['git', '-C', str(library), 'rev-parse', 'HEAD'], text=True).strip())
def test_tool_requirements_are_installed_in_one_union(self):
definitions = dict(one=dict(packages=['gcc', 'cmake']), two=dict(packages=['gcc', 'qemu-user']))
with patch.object(components, 'registry', return_value=definitions), patch.object(components.subprocess, 'run') as run:
+9
View File
@@ -649,6 +649,15 @@ class GameEnvironmentTest(unittest.TestCase):
self.assertEqual(evaluate(), (0, 2))
self.assertEqual(evaluate("changed"), (0, 3))
def test_the_mali_bridge_caps_the_vkd3d_feature_level(self):
bridge = {"BL_VKBRIDGE": "1", "VKD3D_FEATURE_LEVEL": "12_2"}
self.assertIn("capped", MODULE["bridge_feature_level"](bridge))
self.assertEqual(bridge["VKD3D_FEATURE_LEVEL"], "12_0")
for env in ({"BL_VKBRIDGE": "1", "VKD3D_FEATURE_LEVEL": "11_1"}, {"VKD3D_FEATURE_LEVEL": "12_2"}, {"BL_VKBRIDGE": "1"}):
before = dict(env)
self.assertIsNone(MODULE["bridge_feature_level"](env))
self.assertEqual(env, before)
def test_profile_precedence_and_unset(self):
env = {"KEEP": "inherited", "REMOVE": "inherited", "CUSTOM": "launch option"}
config = {"version": 1, "shared": {"CUSTOM": "shared"}, "games": {"42": {"REMOVE": None, "CUSTOM": "game", "EMPTY": ""}}}
+127
View File
@@ -0,0 +1,127 @@
import os
import re
import shutil
import socket
import subprocess
import tempfile
import threading
import time
import unittest
from pathlib import Path
SESSION = Path(__file__).resolve().parents[1] / "linuxfs/overlay/usr/local/bin/droiddeck-session"
def wait_functions():
text = SESSION.read_text()
found = []
for name in ("bl_now_ms", "vkbridge_wait_socket"):
match = re.search(r"^%s\(\) \{\n.*?^\}\n" % name, text, re.M | re.S)
if match:
found.append(match.group(0))
return found
class VkBridgeSocketWaitTest(unittest.TestCase):
def setUp(self):
self.root = Path(tempfile.mkdtemp())
self.addCleanup(shutil.rmtree, self.root, True)
self.sock = self.root / "vkbridge" / "server.sock"
self.sock.parent.mkdir()
self.listeners = []
self.addCleanup(lambda: [s.close() for s in self.listeners])
def listen(self):
listener = socket.socket(socket.AF_UNIX)
listener.bind(str(self.sock))
listener.listen(1)
self.listeners.append(listener)
def listen_after(self, seconds):
timer = threading.Timer(seconds, self.listen)
timer.start()
self.addCleanup(timer.cancel)
def run_wait(self, limit_ms, prelude=""):
functions = wait_functions()
self.assertEqual(2, len(functions), "droiddeck-session lacks bl_now_ms() or vkbridge_wait_socket()")
script = "set -u\n" + "".join(functions) + prelude
script += 'if vkbridge_wait_socket "$1" "$2"; then echo USABLE; else echo FALLBACK; fi\n'
started = time.monotonic()
out = subprocess.run(["bash", "-c", script, "session", str(self.sock), str(limit_ms)],
capture_output=True, text=True, timeout=30, check=True).stdout
return out, time.monotonic() - started
def waited_ms(self, out):
match = re.search(r"waited (\d+) ms|after (\d+) ms", out)
self.assertIsNotNone(match, out)
return int(match.group(1) or match.group(2))
def test_a_socket_already_listening_is_used_at_once(self):
self.listen()
out, elapsed = self.run_wait(10000)
self.assertTrue(out.endswith("USABLE\n"), out)
self.assertIn("== vkbridge: server socket ready (waited 0 ms)", out)
self.assertLess(elapsed, 2)
def test_a_server_still_probing_is_waited_for(self):
self.listen_after(0.4)
out, elapsed = self.run_wait(10000)
self.assertTrue(out.endswith("USABLE\n"), out)
self.assertIn("== vkbridge: server socket ready (waited ", out)
waited = self.waited_ms(out)
self.assertGreaterEqual(waited, 300, out)
self.assertLess(waited, 5000, out)
self.assertLess(elapsed, 6)
def test_no_server_falls_back_after_the_limit(self):
out, elapsed = self.run_wait(300)
self.assertTrue(out.endswith("FALLBACK\n"), out)
self.assertIn("== vkbridge: no server socket after ", out)
self.assertIn("(gave up at 300 ms)", out)
self.assertGreaterEqual(self.waited_ms(out), 300)
self.assertLess(elapsed, 5)
def test_a_plain_file_is_not_a_socket(self):
self.sock.write_text("stale")
out, _ = self.run_wait(200)
self.assertTrue(out.endswith("FALLBACK\n"), out)
def test_a_stopping_session_does_not_wait(self):
out, elapsed = self.run_wait(10000, prelude="bl_stopping=1\n")
self.assertTrue(out.endswith("FALLBACK\n"), out)
self.assertIn("the session is stopping", out)
self.assertLess(elapsed, 2)
def test_no_socket_path_is_a_fallback_without_waiting(self):
functions = "".join(wait_functions())
out = subprocess.run(["bash", "-c", "set -u\n" + functions +
'if vkbridge_wait_socket "" 10000; then echo USABLE; else echo FALLBACK; fi\n'],
capture_output=True, text=True, timeout=10, check=True).stdout
self.assertEqual("FALLBACK\n", out)
class VkBridgeSessionCheckTest(unittest.TestCase):
def setUp(self):
self.text = SESSION.read_text()
match = re.search(r'^if \[ "\$\{BL_VKBRIDGE:-0\}" = 1 \]; then\n.*?^fi\n', self.text, re.M | re.S)
self.assertIsNotNone(match)
self.block = match.group(0)
def test_the_first_pass_waits_before_the_usability_check(self):
wait = self.block.index("vkbridge_wait_socket")
check = self.block.index('[ -S "${VKBRIDGE_SOCKET:-}" ]')
self.assertLess(wait, check)
self.assertIn('[ -z "${BL_INSIDE:-}" ] && vkbridge_wait_socket', self.block)
def test_the_fallback_message_names_what_is_missing(self):
self.assertIn("Mali bridge requested but NOT usable", self.block)
self.assertIn("missing:$bl_vkb_missing", self.block)
def test_a_stop_ends_the_wait(self):
self.assertRegex(self.text, r"trap '[^']*bl_stopping=1[^']*' TERM INT HUP")
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,48 @@
import os
import re
import subprocess
import unittest
from pathlib import Path
SESSION = Path(__file__).resolve().parents[1] / "linuxfs/overlay/usr/local/bin/droiddeck-session"
KEYS = ("BL_VKBRIDGE_WEBHELPER_ARGS", "BL_VKBRIDGE_CEF_ANGLE", "BL_VKBRIDGE_CEF_VULKAN")
def layout_function():
match = re.search(r"^bl_webhelper_layout\(\) \{\n.*?^\}\n", SESSION.read_text(), re.M | re.S)
return match.group(0) if match else None
class WebhelperLayoutTest(unittest.TestCase):
def layout(self, **settings):
function = layout_function()
self.assertIsNotNone(function, "droiddeck-session lacks bl_webhelper_layout()")
env = {k: v for k, v in os.environ.items() if k not in KEYS}
env.update(settings)
return subprocess.run(["bash", "-c", "set -u\n" + function + "bl_webhelper_layout\n"], env=env,
capture_output=True, text=True, check=True).stdout.strip()
def test_unset_keeps_steams_own_layout(self):
self.assertEqual(self.layout(), "stock")
def test_angle_on_vulkan_brings_chromiums_vulkan_compositing(self):
self.assertEqual(self.layout(BL_VKBRIDGE_CEF_ANGLE="vulkan"),
"--use-gl=angle --use-angle=vulkan +Vulkan +VulkanFromANGLE +DefaultANGLEVulkan")
def test_vulkan_compositing_can_be_left_off(self):
self.assertEqual(self.layout(BL_VKBRIDGE_CEF_ANGLE="vulkan", BL_VKBRIDGE_CEF_VULKAN="0"),
"--use-gl=angle --use-angle=vulkan")
def test_other_angle_backends_get_only_the_angle_switches(self):
self.assertEqual(self.layout(BL_VKBRIDGE_CEF_ANGLE="gl"), "--use-gl=angle --use-angle=gl")
def test_explicit_layout_wins(self):
self.assertEqual(self.layout(BL_VKBRIDGE_CEF_ANGLE="vulkan", BL_VKBRIDGE_WEBHELPER_ARGS="--x +Y"),
"--x +Y")
def test_the_steam_block_uses_it(self):
self.assertIn("BL_WEBHELPER_ARGS=$(bl_webhelper_layout)", SESSION.read_text())
if __name__ == "__main__":
unittest.main()
+13
View File
@@ -61,6 +61,19 @@ class WebhelperPreloadTest(unittest.TestCase):
args = [WEBHELPER, "--enable-features=A"]
self.assertEqual(self.argv(*args, spec="stock"), args)
def test_angle_switches_are_appended_with_the_gpu_process_kept_out_of_process(self):
out = self.argv(WEBHELPER, "-lang=en_US", "--enable-features=A", spec="--use-gl=angle --use-angle=vulkan")
self.assertEqual(out, [WEBHELPER, "-lang=en_US", "--use-gl=angle", "--use-angle=vulkan",
"--enable-features=A"])
self.assertNotIn("--in-process-gpu", out)
def test_vulkan_compositing_features_join_steams_feature_list(self):
out = self.argv(WEBHELPER, "--enable-features=PlatformHEVCDecoderSupport,V4L2VideoDecode",
spec="--use-gl=angle --use-angle=vulkan +Vulkan +VulkanFromANGLE +DefaultANGLEVulkan")
self.assertEqual(out, [WEBHELPER, "--use-gl=angle", "--use-angle=vulkan",
"--enable-features=PlatformHEVCDecoderSupport,V4L2VideoDecode,"
"Vulkan,VulkanFromANGLE,DefaultANGLEVulkan"])
def test_an_empty_setting_means_the_default(self):
self.assertIn("--no-zygote", self.argv(WEBHELPER, spec=""))