Save the Qualcomm capability checks between sessions

Every Qualcomm session started the driver through libhybris twice before
Steam: droiddeck-qualcomm-caps (up to 20 s) and glxinfo for the browser's
OpenGL level (up to 15 s). Both answers only change with the driver, so
they are now saved under $XDG_CACHE_HOME/droiddeck and reused while the
driver file (SHA-256), the selected driver, the bridge build, the layers
and the check script are unchanged. Gamescope's linux-dmabuf global is
still checked every session, failed or empty results are never saved, and
the log marks a reused result.
This commit is contained in:
The412Banner committed 2026-10-25 23:17:41 +00:00
1 parent 563dff6d75
commit dfce775514
4 files changed
+146 -11

No files matched your search

@@ -2,8 +2,12 @@
"""Decide whether this Qualcomm session can present Proton games zero-copy through Gamescope's WSI layer.
Prints one line and exits 0 only when every check passes; anything else keeps games on X11 SHM.
The driver checks start the driver through libhybris, so their result is saved and reused while the driver file,
the selected driver, the bridge build, the layers and this script are unchanged. Gamescope is checked every time.
"""
import ctypes as c
import hashlib
import json
import os
import subprocess
import sys
@@ -81,6 +85,58 @@ def decide(caps):
return not reasons, reasons
CACHE_VERSION = 1
CACHE_INPUTS = ('DROIDDECK_QUALCOMM_DRIVER', 'DROIDDECK_QUALCOMM_ROOT', 'VK_ICD_FILENAMES', 'VK_LAYER_PATH',
'VK_INSTANCE_LAYERS')
def cache_path(environ=os.environ):
root = environ.get('XDG_CACHE_HOME') or os.path.join(os.path.expanduser('~'), '.cache')
return os.path.join(root, 'droiddeck', 'qualcomm-caps.json')
def cache_key(environ=os.environ, script=__file__):
"""Fingerprint of everything the driver checks depend on, or None when the driver file cannot be read."""
digest = hashlib.sha256(str(CACHE_VERSION).encode())
try:
for path in (environ.get('HYBRIS_VULKAN_HAL', '/vendor/lib64/hw/vulkan.adreno.so'), script):
with open(path, 'rb') as data:
digest.update(hashlib.sha256(data.read()).digest())
except OSError:
return None
for name in CACHE_INPUTS:
digest.update(b'\0' + environ.get(name, '').encode())
return digest.hexdigest()
def cached_probe(path, key):
"""Return (caps, saved): the saved driver checks for this key, or a new probe saved for the next session."""
if key:
try:
with open(path) as data:
stored = json.load(data)
if stored.get('key') == key:
caps = dict(stored['caps'])
caps['extensions'] = set(caps.get('extensions', ()))
return caps, True
except (OSError, ValueError, KeyError, TypeError, AttributeError):
pass
try:
caps = probe()
except Exception as error:
return {'error': str(error)}, False
if key:
try:
os.makedirs(os.path.dirname(path), exist_ok=True)
temporary = f'{path}.{os.getpid()}'
with open(temporary, 'w') as data:
json.dump({'key': key, 'caps': dict(caps, extensions=sorted(caps.get('extensions', ())))}, data)
os.replace(temporary, path)
except OSError:
pass
return caps, False
def gamescope_dmabuf():
if not os.environ.get('WAYLAND_DISPLAY'):
return False
@@ -106,7 +162,7 @@ def probe():
def names(values):
return (c.c_char_p * len(values))(*[value.encode() for value in values])
caps = {'gamescope_dmabuf': gamescope_dmabuf()}
caps = {}
count = U()
enumerate_instance = function('vkEnumerateInstanceExtensionProperties', [c.c_char_p, c.POINTER(U), H])
check(enumerate_instance(None, c.byref(count), None), 'vkEnumerateInstanceExtensionProperties')
@@ -228,12 +284,11 @@ def export_linear_image(function, device):
def main():
try:
caps = probe()
except Exception as error:
caps = {'error': str(error)}
caps, saved = cached_probe(cache_path(), cache_key())
caps['gamescope_dmabuf'] = gamescope_dmabuf()
enabled, reasons = decide(caps)
where = f" ({caps['device']}, driver {caps['driver']})" if caps.get('device') else ''
where = f" ({caps['device']}, driver {caps['driver']}{'; driver checks saved earlier' if saved else ''})" \
if caps.get('device') else ''
print(('zero-copy on' if enabled else 'zero-copy off: ' + '; '.join(reasons)) + where)
return 0 if enabled else 1
@@ -305,15 +305,24 @@ if [ "${DROIDDECK_SYSTEM_QUALCOMM:-0}" = 1 ]; then
fi
# Chromium's GPU compositor corrupts the Steam UI on Qualcomm drivers where Zink stops below OpenGL 4.0.
# An explicit BL_STEAM_CEF_SOFTWARE decides instead.
# glxinfo starts the driver through libhybris (up to 15 s), so the version is saved per driver file and bridge build.
bl_qualcomm_browser_gpu() {
[ "${DROIDDECK_SYSTEM_QUALCOMM:-0}" = 1 ] && [ -z "${BL_STEAM_CEF_SOFTWARE+x}" ] || return 0
local gl
gl=$(timeout 15 glxinfo -B 2>/dev/null | sed -n 's/^ *Max core profile version: *\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' | head -1)
local gl key saved_key saved= cache=$XDG_CACHE_HOME/droiddeck/qualcomm-gl
key=$({ sha256sum "${HYBRIS_VULKAN_HAL:-/vendor/lib64/hw/vulkan.adreno.so}" &&
printf '%s\n' "${DROIDDECK_QUALCOMM_DRIVER:-}" "${DROIDDECK_QUALCOMM_ROOT:-}"; } 2>/dev/null |
sha256sum | cut -c1-32)
if read -r saved_key gl 2>/dev/null < "$cache" && [ "$saved_key" = "$key" ] && [ -n "$gl" ]; then
saved="; saved earlier"
else
gl=$(timeout 15 glxinfo -B 2>/dev/null | sed -n 's/^ *Max core profile version: *\([0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' | head -1)
[ -z "$gl" ] || { mkdir -p "${cache%/*}" && printf '%s %s\n' "$key" "$gl" > "$cache"; } 2>/dev/null
fi
if [ "${gl%%.*}" -ge 4 ] 2>/dev/null; then
echo "== steam: browser GPU compositing on (Zink OpenGL $gl)"
echo "== steam: browser GPU compositing on (Zink OpenGL $gl$saved)"
return 0
fi
echo "== steam: browser software compositing (Zink offers OpenGL ${gl:-unknown}; GPU compositing needs 4.0)"
echo "== steam: browser software compositing (Zink offers OpenGL ${gl:-unknown}$saved; GPU compositing needs 4.0)"
return 1
}
reap_orphans() {
+1 -1
View File
@@ -83,7 +83,7 @@ The recipes pin upstream source revisions and retain local outputs under `build/
Proton games in Qualcomm sessions present through Gamescope's WSI layer instead of X11 shared memory when the driver can export LINEAR DMA-BUFs. The Steam client, its browser and other session programs keep `WSI_X11_FORCE_SHM=1` and `DISABLE_GAMESCOPE_WSI=1`; the Proton wrapper's `bl_present` drops both for the game only when `DROIDDECK_QUALCOMM_ZERO_COPY=1`. Set `BL_QUALCOMM_SHM=1` in the session environment to keep games on the shared-memory path regardless.
`droiddeck-session` sets `DROIDDECK_QUALCOMM_ZERO_COPY` from `droiddeck-qualcomm-caps`, which runs once per session through the bridge ICD and the session's layers, so an imported AdrenoTools driver is judged rather than the system one. Zero-copy is enabled only when all of these hold:
`droiddeck-session` sets `DROIDDECK_QUALCOMM_ZERO_COPY` from `droiddeck-qualcomm-caps`, which runs once per session through the bridge ICD and the session's layers, so an imported AdrenoTools driver is judged rather than the system one. Its driver checks are saved in `$XDG_CACHE_HOME/droiddeck/qualcomm-caps.json` and reused while the driver file (by SHA-256), the selected driver, the bridge build, the layers and the script are unchanged; Gamescope's `zwp_linux_dmabuf_v1` is checked every session, and failed probes are never saved. The Steam browser's OpenGL check (`glxinfo -B`) is saved the same way in `qualcomm-gl`. Zero-copy is enabled only when all of these hold:
- The driver offers `VK_KHR_external_memory_fd`, `VK_EXT_external_memory_dma_buf`, `VK_KHR_image_format_list` and `VK_KHR_external_fence_fd`.
- LINEAR `B8G8R8A8_UNORM` images are exportable as DMA-BUF colour attachments and importable as sampled images.
+71
View File
@@ -1,9 +1,13 @@
from pathlib import Path
import json
import runpy
import tempfile
import unittest
CAPS = runpy.run_path(str(Path(__file__).resolve().parents[1] / "linuxfs/overlay/usr/local/bin/droiddeck-qualcomm-caps"))
decide = CAPS["decide"]
cache_key = CAPS["cache_key"]
cached_probe = CAPS["cached_probe"]
REQUIRED = set(CAPS["REQUIRED"])
CAPABLE = {"extensions": REQUIRED | {"VK_EXT_image_drm_format_modifier"}, "linear_export": True,
"linear_import": True, "export_fd": True, "gamescope_dmabuf": True}
@@ -35,5 +39,72 @@ class QualcommCapsTest(unittest.TestCase):
self.assertFalse(decide({})[0])
class QualcommCapsCacheTest(unittest.TestCase):
def setUp(self):
self.dir = tempfile.TemporaryDirectory()
self.addCleanup(self.dir.cleanup)
root = Path(self.dir.name)
self.hal = root / "vulkan.adreno.so"
self.hal.write_bytes(b"driver one")
self.script = root / "caps"
self.script.write_text("checks")
self.path = str(root / "cache/qualcomm-caps.json")
self.env = {"HYBRIS_VULKAN_HAL": str(self.hal), "DROIDDECK_QUALCOMM_DRIVER": "system-qualcomm",
"DROIDDECK_QUALCOMM_ROOT": "/data/qualcomm-system/abc"}
self.probes = 0
globals_ = cached_probe.__globals__
original = globals_["probe"]
self.addCleanup(globals_.__setitem__, "probe", original)
globals_["probe"] = self.fake_probe
def fake_probe(self):
self.probes += 1
return {"device": "Adreno (TM) 750", "driver": "0.762.24", "extensions": set(REQUIRED),
"linear_export": True, "linear_import": True, "export_fd": True}
def key(self, **changes):
return cache_key(dict(self.env, **changes), str(self.script))
def test_the_key_follows_the_driver_file_the_selection_and_the_checks(self):
key = self.key()
self.assertEqual(key, self.key())
self.assertNotEqual(key, self.key(DROIDDECK_QUALCOMM_DRIVER="qualcomm-adrenotools:v891"))
self.assertNotEqual(key, self.key(DROIDDECK_QUALCOMM_ROOT="/data/qualcomm-system/def"))
self.hal.write_bytes(b"driver two")
self.assertNotEqual(key, self.key())
self.hal.write_bytes(b"driver one")
self.script.write_text("new checks")
self.assertNotEqual(key, self.key())
self.assertIsNone(self.key(HYBRIS_VULKAN_HAL=str(self.hal) + ".missing"))
def test_a_saved_result_is_reused_only_for_the_same_key(self):
caps, saved = cached_probe(self.path, self.key())
self.assertEqual((saved, self.probes), (False, 1))
caps, saved = cached_probe(self.path, self.key())
self.assertEqual((saved, self.probes), (True, 1))
self.assertEqual(caps["extensions"], set(REQUIRED))
self.assertEqual(decide(dict(caps, gamescope_dmabuf=True)), (True, []))
self.assertNotIn("gamescope_dmabuf", json.loads(Path(self.path).read_text())["caps"])
_, saved = cached_probe(self.path, self.key(DROIDDECK_QUALCOMM_DRIVER="qualcomm-adrenotools:v891"))
self.assertEqual((saved, self.probes), (False, 2))
def test_failed_probes_and_unreadable_drivers_are_not_saved(self):
def failing():
raise RuntimeError("vkCreateInstance returned -9")
cached_probe.__globals__["probe"] = failing
self.assertEqual(cached_probe(self.path, self.key()), ({"error": "vkCreateInstance returned -9"}, False))
self.assertFalse(Path(self.path).exists())
cached_probe.__globals__["probe"] = self.fake_probe
self.assertEqual(cached_probe(self.path, None)[1], False)
self.assertFalse(Path(self.path).exists())
def test_a_damaged_saved_file_probes_again(self):
Path(self.path).parent.mkdir(parents=True)
Path(self.path).write_text("{not json")
self.assertEqual(cached_probe(self.path, self.key())[1], False)
self.assertEqual(self.probes, 1)
if __name__ == "__main__":
unittest.main()