mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 11:31:08 +00:00
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:
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() {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user