mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
Integrate the experimental Quest 3 and 3S port
This commit is contained in:
1 parent
10c12b2998
commit
883e8d40f6
23 files changed
+488
-9
No files matched your search
@@ -186,7 +186,7 @@ jobs:
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: app/src/main/assets/linuxfs
|
||||
key: preloads-${{ env.PRELOAD_CACHE_VERSION }}-${{ hashFiles('app/src/main/cpp/fakeinput_steam.cpp', 'tools/linuxfs/preload/*.c', 'tools/linuxfs/build-x86-preloads.sh', 'tools/linuxfs/fex/*', 'tools/linuxfs/clipboard/*.c', 'tools/linuxfs/ssbs/*.c', 'tools/proot/fastpath/*.c', 'tools/linuxfs/overlay/usr/local/bin/*', 'tools/linuxfs/overlay/usr/bin/**', 'tools/linuxfs/desktop/*', 'tools/gamescope/release.env', 'tools/wlroots/release.env', 'tools/linuxfs/uruntime.env', 'tools/linuxfs/licenses/*', 'tools/mangoapp/*', 'tools/msitools/**', 'tools/linuxfs/debian-image.env') }}
|
||||
key: preloads-${{ env.PRELOAD_CACHE_VERSION }}-${{ hashFiles('app/src/main/cpp/fakeinput_steam.cpp', 'app/src/main/cpp/adrenotools/src/hook/kgsl.h', 'tools/quest/*', 'tools/linuxfs/preload/*.c', 'tools/linuxfs/build-x86-preloads.sh', 'tools/linuxfs/fex/*', 'tools/linuxfs/clipboard/*.c', 'tools/linuxfs/ssbs/*.c', 'tools/proot/fastpath/*.c', 'tools/linuxfs/overlay/usr/local/bin/*', 'tools/linuxfs/overlay/usr/bin/**', 'tools/linuxfs/desktop/*', 'tools/gamescope/release.env', 'tools/wlroots/release.env', 'tools/linuxfs/uruntime.env', 'tools/linuxfs/licenses/*', 'tools/mangoapp/*', 'tools/msitools/**', 'tools/linuxfs/debian-image.env') }}
|
||||
|
||||
- name: Build libfakeinput.so and libblsession.so (glibc aarch64)
|
||||
if: steps.preloads.outputs.cache-hit != 'true'
|
||||
@@ -457,6 +457,7 @@ jobs:
|
||||
SGDB_API_KEY: ${{ secrets.SGDB_API_KEY }}
|
||||
run: |
|
||||
set -o pipefail
|
||||
python3 tools/quest/build-driver.py "$ANDROID_HOME/ndk/$NDK_VERSION"
|
||||
python3 tools/release/runtime_inputs.py record
|
||||
./gradlew assembleRelease --console=plain -PndkVersion="$NDK_VERSION" 2>&1 | tee /tmp/build.log
|
||||
python3 tools/release/check_session_assets.py app/build/outputs/apk/release/app-release.apk
|
||||
|
||||
@@ -22,6 +22,7 @@ class App : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
CrashHandler.install(this)
|
||||
com.droiddeck.launcher.session.QuestExperimental.initialize(this)
|
||||
com.droiddeck.launcher.session.SessionPrefs.settleGpuDriverMode(this)
|
||||
com.droiddeck.launcher.update.AppUpdates.init(this)
|
||||
GpuClockPin.clearLeftover(this)
|
||||
|
||||
@@ -50,7 +50,8 @@ public final class TurnipDriver {
|
||||
/** Adreno 8xx (8 Elite, e.g. a Fold 8's A830). */
|
||||
private static final String DRIVER_A8XX = "turnip-sdk36";
|
||||
/** The two the apk carries; never removable, unpacked on demand. */
|
||||
public static final List<String> BUNDLED = Collections.unmodifiableList(Arrays.asList(DRIVER_A7XX, DRIVER_A8XX));
|
||||
public static final List<String> BUNDLED = Collections.unmodifiableList(Arrays.asList(
|
||||
DRIVER_A7XX, DRIVER_A8XX, com.droiddeck.launcher.session.QuestExperimental.DRIVER));
|
||||
/** Overrides the pick, for a device we cannot reach: a7xx, a8xx or system. */
|
||||
private static final String OVERRIDE_FILE = "Download/droiddeck-driver";
|
||||
/** Bundled drivers the user deleted: hidden from the list, their unpacked copy removed. */
|
||||
|
||||
@@ -33,7 +33,7 @@ import java.util.concurrent.atomic.AtomicInteger
|
||||
*/
|
||||
object BwrapSpawner {
|
||||
private const val TAG = "BwrapSpawner"
|
||||
const val SOCKET = "com.droiddeck.launcher.bwrap"
|
||||
fun socket(context: Context): String = "${context.packageName}.bwrap"
|
||||
private val seq = AtomicInteger()
|
||||
@Volatile private var server: LocalServerSocket? = null
|
||||
|
||||
@@ -42,10 +42,11 @@ object BwrapSpawner {
|
||||
fun start(context: Context) {
|
||||
if (server != null) return
|
||||
val app = context.applicationContext
|
||||
val name = socket(app)
|
||||
val socket = try {
|
||||
LocalServerSocket(SOCKET)
|
||||
LocalServerSocket(name)
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "cannot listen on $SOCKET", e)
|
||||
Log.e(TAG, "cannot listen on $name", e)
|
||||
return
|
||||
}
|
||||
server = socket
|
||||
|
||||
@@ -51,6 +51,7 @@ object GuestCommand {
|
||||
"/usr/bin/env", "-i", "HOME=/root", "USER=root", "LANG=C.UTF-8",
|
||||
"PATH=/usr/local/bin:/usr/bin:/bin", "XDG_RUNTIME_DIR=${LinuxRuntime.GUEST_RUNTIME_DIR}",
|
||||
"XDG_DATA_HOME=/root/.local/share", "FLATPAK_BWRAP=${FlatpakManager.BWRAP}",
|
||||
"DROIDDECK_BWRAP_SOCKET=${BwrapSpawner.socket(context)}",
|
||||
"XDG_DATA_DIRS=/root/.local/share/flatpak/exports/share:/usr/local/share:/usr/share",
|
||||
)
|
||||
cmd += argv
|
||||
|
||||
@@ -24,6 +24,12 @@ object CrashHandler {
|
||||
Thread.setDefaultUncaughtExceptionHandler { thread, error ->
|
||||
try {
|
||||
Log.e(TAG, "uncaught on ${thread.name}", error)
|
||||
if (QuestExperimental.isSupported()) runCatching {
|
||||
File(context.filesDir, "quest-last-crash.txt").writeText(
|
||||
"${java.time.Instant.now()} thread=${thread.name}\n" +
|
||||
StringWriter().also { error.printStackTrace(PrintWriter(it)) }.toString()
|
||||
)
|
||||
}
|
||||
val dir = SessionPaths.current()
|
||||
if (dir != null && dir.isDirectory) {
|
||||
val trace = StringWriter().also { error.printStackTrace(PrintWriter(it)) }.toString()
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import com.droiddeck.launcher.BuildConfig
|
||||
import java.io.File
|
||||
|
||||
/** Defaults and launch evidence for this explicitly experimental Quest build. */
|
||||
object QuestExperimental {
|
||||
private const val TAG = "QuestExperimental"
|
||||
const val DRIVER = "turnip25.1.0-quest1"
|
||||
|
||||
// Quest 2 has an A6xx GPU and cannot use this bundled A7xx driver.
|
||||
internal fun supported(model: String, device: String, manufacturer: String): Boolean =
|
||||
model.equals("Quest 3", ignoreCase = true) || model.equals("Quest 3S", ignoreCase = true) ||
|
||||
((manufacturer.equals("Oculus", ignoreCase = true) || manufacturer.equals("Meta", ignoreCase = true)) &&
|
||||
(device == "eureka" || device == "panther"))
|
||||
|
||||
fun isSupported(): Boolean = supported(Build.MODEL, Build.DEVICE, Build.MANUFACTURER)
|
||||
|
||||
fun initialize(context: Context) {
|
||||
if (!isSupported()) return
|
||||
val prefs = context.getSharedPreferences("session", Context.MODE_PRIVATE)
|
||||
if (!prefs.getBoolean("questDefaultsV1", false)) {
|
||||
// Auto would download an unwrapped display driver and lose the compatibility path.
|
||||
// Apply once so deliberate changes made during headset testing remain possible.
|
||||
prefs.edit()
|
||||
.putString("gpuDriverMode", SessionPrefs.GPU_DRIVERS_MANUAL)
|
||||
.putString("androidDriver", DRIVER)
|
||||
.putString("linuxDriver", "")
|
||||
// The existing HDR option uses SurfaceControl presentation. This is a
|
||||
// corruption workaround from upstream issue #245, not a claim of HDR accuracy.
|
||||
.putBoolean("hdr.steam", true)
|
||||
.putBoolean("questDefaultsV1", true)
|
||||
.apply()
|
||||
}
|
||||
val report = buildString {
|
||||
appendLine("DroidDeck Quest Experimental ${BuildConfig.VERSION_NAME}")
|
||||
appendLine("package=${context.packageName}")
|
||||
appendLine("manufacturer=${Build.MANUFACTURER} model=${Build.MODEL}")
|
||||
appendLine("device=${Build.DEVICE} board=${Build.BOARD} hardware=${Build.HARDWARE}")
|
||||
appendLine("android=${Build.VERSION.RELEASE} sdk=${Build.VERSION.SDK_INT}")
|
||||
appendLine("abis=${Build.SUPPORTED_ABIS.joinToString()}")
|
||||
for (name in listOf("gpu_model", "gpu_chipid")) {
|
||||
val value = runCatching { File("/sys/class/kgsl/kgsl-3d0/$name").readText().trim() }
|
||||
.getOrDefault("unavailable")
|
||||
appendLine("$name=$value")
|
||||
}
|
||||
appendLine("driverMode=${SessionPrefs.gpuDriverMode(context)}")
|
||||
appendLine("androidDriver=${SessionPrefs.androidDriver(context)}")
|
||||
appendLine("steamSurfaceControlWorkaround=${SessionPrefs.hdr(context, "steam")}")
|
||||
appendLine("GPUOBJ fallback installed in Android driver and ARM64 libblsession")
|
||||
appendLine("Experimental Quest 3/3S compatibility; 2D panel application")
|
||||
}
|
||||
Log.i(TAG, report)
|
||||
runCatching { File(context.filesDir, "quest-startup.txt").writeText(report) }
|
||||
.onFailure { Log.w(TAG, "Could not write startup report", it) }
|
||||
}
|
||||
}
|
||||
@@ -343,6 +343,7 @@ class SessionService : Service() {
|
||||
|
||||
val size = SessionState.outputSize
|
||||
val guest = ArrayList<String>()
|
||||
guest.add("DROIDDECK_BWRAP_SOCKET=" + com.droiddeck.launcher.runtime.BwrapSpawner.socket(this))
|
||||
// The desktop's Steam launchers start the client there (droiddeck-steam-launch), through the
|
||||
// same set-up as a Steam session: it gets what the client and its games are started with.
|
||||
val steamHere = SessionState.mode == MODE_STEAM || SessionState.mode == MODE_DESKTOP
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
import android.content.Context
|
||||
import android.content.ContextWrapper
|
||||
import com.droiddeck.launcher.runtime.BwrapSpawner
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.annotation.Config
|
||||
import org.robolectric.shadows.ShadowBuild
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(sdk = [28], manifest = Config.NONE)
|
||||
class QuestExperimentalTest {
|
||||
private val context: Context get() = RuntimeEnvironment.getApplication()
|
||||
|
||||
@Before fun reset() {
|
||||
context.getSharedPreferences("session", Context.MODE_PRIVATE).edit().clear().commit()
|
||||
ShadowBuild.setModel("Phone")
|
||||
ShadowBuild.setDevice("phone")
|
||||
}
|
||||
|
||||
@Test fun otherDevicesAndQuest2KeepTheirSettings() {
|
||||
for (model in listOf("Phone", "Quest 2")) {
|
||||
ShadowBuild.setModel(model)
|
||||
QuestExperimental.initialize(context)
|
||||
assertEquals(SessionPrefs.GPU_DRIVERS_AUTO, SessionPrefs.gpuDriverMode(context))
|
||||
assertEquals("", SessionPrefs.androidDriver(context))
|
||||
assertFalse(SessionPrefs.hdr(context, "steam"))
|
||||
}
|
||||
assertFalse(QuestExperimental.supported("Quest 2", "hollywood", "Oculus"))
|
||||
}
|
||||
|
||||
@Test fun questDefaultsSelectBothCompatibilityPathsAndAreAppliedOnlyOnce() {
|
||||
ShadowBuild.setModel("Quest 3")
|
||||
QuestExperimental.initialize(context)
|
||||
assertEquals(SessionPrefs.GPU_DRIVERS_MANUAL, SessionPrefs.gpuDriverMode(context))
|
||||
assertEquals(QuestExperimental.DRIVER, SessionPrefs.androidDriver(context))
|
||||
assertEquals("", SessionPrefs.linuxDriver(context))
|
||||
assertTrue(SessionPrefs.hdr(context, "steam"))
|
||||
assertTrue(context.filesDir.resolve("quest-startup.txt").readText().contains("model=Quest 3"))
|
||||
SessionPrefs.setAndroidDriver(context, "chosen-driver")
|
||||
SessionPrefs.setHdr(context, "steam", false)
|
||||
QuestExperimental.initialize(context)
|
||||
assertEquals("chosen-driver", SessionPrefs.androidDriver(context))
|
||||
assertFalse(SessionPrefs.hdr(context, "steam"))
|
||||
}
|
||||
|
||||
@Test fun quest3sIsRecognizedByModelOrDevice() {
|
||||
assertTrue(QuestExperimental.supported("Quest 3S", "unknown", "Oculus"))
|
||||
assertTrue(QuestExperimental.supported("unknown", "panther", "Oculus"))
|
||||
assertTrue(QuestExperimental.supported("unknown", "eureka", "Meta"))
|
||||
assertFalse(QuestExperimental.supported("Pixel 7", "panther", "Google"))
|
||||
}
|
||||
|
||||
@Test fun installedPackagesUseSeparateSandboxSockets() {
|
||||
val quest = object : ContextWrapper(context) {
|
||||
override fun getPackageName() = "com.droiddeck.quest"
|
||||
}
|
||||
assertEquals("com.droiddeck.quest.bwrap", BwrapSpawner.socket(quest))
|
||||
assertNotEquals(BwrapSpawner.socket(context), BwrapSpawner.socket(quest))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
# Experimental Quest 3/3S support
|
||||
|
||||
Quest 3/3S runs DroidDeck as a 2D panel. This does not add immersive VR or
|
||||
OpenXR support. Quest 2 is outside the bundled Adreno 7xx driver path.
|
||||
|
||||
The first launch on Quest 3/3S selects manual drivers, the bundled
|
||||
`turnip25.1.0-quest1` Android driver and the default Linux driver. It enables
|
||||
`hdr.steam` to use the existing SurfaceControl presentation workaround from
|
||||
[the Quest report](https://github.com/Droid-Deck/DroidDeck/issues/245).
|
||||
These defaults apply once; subsequent user changes are retained. Colour
|
||||
accuracy and headset performance still need testing.
|
||||
|
||||
Both the Android driver wrapper and the Linux session preload use
|
||||
`tools/quest/kgsl_compat.c`. Successful legacy allocation/free calls are
|
||||
unchanged. Only `EACCES`/`EPERM` failures retry through `GPUOBJ_ALLOC`,
|
||||
`GPUOBJ_INFO` and `GPUOBJ_FREE`. The Android wrapper depends on the original
|
||||
bundled Turnip binary. Unhandled controller ioctls forward to the next
|
||||
preload hook rather than skipping it with a raw syscall.
|
||||
|
||||
`tools/build_local.sh` and CI build the wrapper from source using the pinned
|
||||
Android NDK; the normal preload builds include the shared Linux adapter.
|
||||
Runtime input verification checks the resulting driver archive and invalidates
|
||||
stale prepared assets after compatibility sources change. To rebuild only the
|
||||
Android driver archive:
|
||||
|
||||
```sh
|
||||
python3 tools/quest/build-driver.py "$ANDROID_HOME/ndk/27.3.13750724"
|
||||
```
|
||||
|
||||
The release variant `quest` produces a separate `com.droiddeck.quest` APK
|
||||
with its own app data. All installed packages use their own bwrap socket.
|
||||
The standard APK also applies Quest defaults when running on Quest 3/3S.
|
||||
|
||||
On a headset, check the Steam login panel, sign-in, game installation and
|
||||
then an owned game's rendering/input. Capture the device/OS, session logs,
|
||||
`files/quest-startup.txt` and any `files/quest-last-crash.txt` failure.
|
||||
Host allocation and preload-chain tests do not establish headset operation.
|
||||
@@ -316,6 +316,7 @@ else
|
||||
fi
|
||||
|
||||
cd "${repo_root}"
|
||||
python3 tools/quest/build-driver.py "${NDK}"
|
||||
python3 tools/release/runtime_inputs.py record
|
||||
./gradlew "${gradle_task}" --console=plain -PndkVersion="${ndk_version}" ${gradle_rust_arg}
|
||||
python3 tools/release/check_session_assets.py "app/build/outputs/apk/${build_variant}/app-${build_variant}.apk"
|
||||
|
||||
@@ -20,7 +20,7 @@ import socket
|
||||
import struct
|
||||
import sys
|
||||
|
||||
SOCKET = "\0com.droiddeck.launcher.bwrap"
|
||||
SOCKET = "\0" + os.environ.get("DROIDDECK_BWRAP_SOCKET", "com.droiddeck.launcher.bwrap")
|
||||
VERSION = "bubblewrap 0.11.0 (droiddeck)"
|
||||
|
||||
# Options and how many arguments each takes; bwrap(1).
|
||||
|
||||
@@ -421,6 +421,7 @@ static int is_link_query(unsigned long request) {
|
||||
}
|
||||
|
||||
void bl_kgsl_chip_id_fixup(unsigned long request, void *arg, int rc); /* kgslid.c */
|
||||
#include "../../quest/kgsl_compat.h"
|
||||
int bl_kgsl_poll(int (*real)(int, unsigned long, void *), int fd, unsigned long request,
|
||||
void *arg, int *rc) __attribute__((visibility("hidden")));
|
||||
int bl_ntsync_ioctl(int fd, unsigned long request, void *arg, int *rc) __attribute__((visibility("hidden")));
|
||||
@@ -440,7 +441,7 @@ int ioctl(int fd, unsigned long request, ...) {
|
||||
if (bl_ntsync_ioctl(fd, request, arg, &rc)) return rc;
|
||||
if (bl_drm_gem_close(fd, request, arg, &rc)) return rc;
|
||||
if (real == NULL) real = (ioctl_fn) dlsym(RTLD_NEXT, "ioctl");
|
||||
if (!bl_kgsl_poll(real, fd, request, arg, &rc)) rc = real(fd, request, arg);
|
||||
if (!bl_kgsl_poll(real, fd, request, arg, &rc)) rc = quest_kgsl_ioctl(real, fd, request, arg);
|
||||
bl_kgsl_chip_id_fixup(request, arg, rc);
|
||||
if (arg == NULL || !is_link_query(request)) return rc;
|
||||
int saved = errno;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
/* Shared with the Android driver wrapper so the two translation paths cannot drift. */
|
||||
#include "../../quest/kgsl_compat.c"
|
||||
@@ -0,0 +1,26 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* Loaded as the AdrenoTools driver; DT_NEEDED points to the original Turnip.
|
||||
* Vulkan/HAL symbols are resolved from that dependency. Its ioctl calls resolve here.
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include "kgsl_compat.h"
|
||||
#include <dlfcn.h>
|
||||
#include <pthread.h>
|
||||
#include <stdarg.h>
|
||||
#include <sys/syscall.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int (*next_ioctl)(int, int, ...);
|
||||
static pthread_once_t once = PTHREAD_ONCE_INIT;
|
||||
static void resolve_ioctl(void) { next_ioctl = dlsym(RTLD_NEXT, "ioctl"); }
|
||||
static int forward(int fd, unsigned long request, void *arg) {
|
||||
pthread_once(&once, resolve_ioctl);
|
||||
return next_ioctl ? next_ioctl(fd, (int)request, arg) : (int)syscall(SYS_ioctl, fd, request, arg);
|
||||
}
|
||||
__attribute__((visibility("default"))) int ioctl(int fd, int request, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, request);
|
||||
void *arg = va_arg(ap, void *);
|
||||
va_end(ap);
|
||||
return quest_kgsl_ioctl(forward, fd, (unsigned int)request, arg);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build the Quest allocation adapter around the unchanged bundled Android Turnip."""
|
||||
import io
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
OUTPUT = ROOT / 'app/build/prepared-assets/graphics_driver/adrenotools-turnip25.1.0-quest1.tzst'
|
||||
|
||||
|
||||
def build(ndk, output=OUTPUT):
|
||||
hosts = list((Path(ndk) / 'toolchains/llvm/prebuilt').glob('*'))
|
||||
if len(hosts) != 1:
|
||||
raise ValueError('Expected one NDK host toolchain')
|
||||
llvm = hosts[0] / 'bin'
|
||||
original = ROOT / 'app/src/main/assets/graphics_driver/adrenotools-turnip25.1.0.tzst'
|
||||
data = subprocess.run(['zstd', '-dc', str(original)], capture_output=True, check=True).stdout
|
||||
with tempfile.TemporaryDirectory(prefix='quest-driver-') as folder:
|
||||
work = Path(folder)
|
||||
with tarfile.open(fileobj=io.BytesIO(data)) as archive:
|
||||
# Only read the known driver entry; no archive paths are extracted.
|
||||
driver = archive.extractfile('vulkan.ad07xx.so').read()
|
||||
(work / 'vulkan.ad07xx.so').write_bytes(driver)
|
||||
wrapper = work / 'libquest_turnip.so'
|
||||
subprocess.run([
|
||||
str(llvm / 'aarch64-linux-android26-clang'), '-shared', '-fPIC', '-O2',
|
||||
'-Wall', '-Wextra', '-Werror', '-fvisibility=hidden',
|
||||
'-Wl,-z,max-page-size=16384', '-Wl,-soname,libquest_turnip.so',
|
||||
'-Wl,--no-as-needed', str(work / 'vulkan.ad07xx.so'), '-Wl,--as-needed',
|
||||
str(ROOT / 'tools/quest/android_wrapper.c'), str(ROOT / 'tools/quest/kgsl_compat.c'),
|
||||
'-ldl', '-llog', '-o', str(wrapper),
|
||||
], check=True)
|
||||
subprocess.run([str(llvm / 'llvm-strip'), '--strip-unneeded', str(wrapper)], check=True)
|
||||
metadata = json.dumps({
|
||||
'libraryName': 'libquest_turnip.so',
|
||||
'name': 'Turnip Adreno 7xx - Quest GPUOBJ experimental',
|
||||
'description': 'Bundled A7xx driver with the experimental Quest 3/3S allocation adapter.',
|
||||
'driverVersion': 'Bundled Mesa + Quest GPUOBJ v1',
|
||||
}, indent=2).encode() + b'\n'
|
||||
stream = io.BytesIO()
|
||||
with tarfile.open(fileobj=stream, mode='w', format=tarfile.USTAR_FORMAT) as archive:
|
||||
for name, content in [('meta.json', metadata), ('libquest_turnip.so', wrapper.read_bytes()),
|
||||
('vulkan.ad07xx.so', driver)]:
|
||||
entry = tarfile.TarInfo(name)
|
||||
entry.size = len(content)
|
||||
entry.mode = 0o644
|
||||
archive.addfile(entry, io.BytesIO(content))
|
||||
compressed = subprocess.run(['zstd', '-q', '-19', '-c'], input=stream.getvalue(),
|
||||
capture_output=True, check=True).stdout
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
output.write_bytes(compressed)
|
||||
print(output)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
build(sys.argv[1])
|
||||
@@ -0,0 +1,78 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
* Quest experimental build, 2026-10-11.
|
||||
* Horizon permits GPUOBJ allocation, but rejects the legacy GPUMEM alloc/free calls.
|
||||
* Retry only those two calls after EACCES/EPERM. No change to device permissions.
|
||||
*/
|
||||
#include "kgsl_compat.h"
|
||||
#include <errno.h>
|
||||
#include <limits.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#ifdef __ANDROID__
|
||||
#include <android/log.h>
|
||||
#endif
|
||||
|
||||
#if __SIZEOF_POINTER__ == 8
|
||||
_Static_assert(sizeof(struct kgsl_gpumem_alloc_id) == 48, "KGSL alloc ABI");
|
||||
_Static_assert(sizeof(struct kgsl_gpuobj_alloc) == 48, "GPUOBJ alloc ABI");
|
||||
_Static_assert(sizeof(struct kgsl_gpuobj_free) == 32, "GPUOBJ free ABI");
|
||||
_Static_assert(sizeof(struct kgsl_gpuobj_info) == 48, "GPUOBJ info ABI");
|
||||
#endif
|
||||
|
||||
static void report(const char *operation, int error) {
|
||||
static unsigned successes, failures;
|
||||
unsigned *counter = error ? &failures : &successes;
|
||||
/* Log the first success and at most eight failures per process. */
|
||||
if (__atomic_fetch_add(counter, 1, __ATOMIC_RELAXED) >= (error ? 8u : 1u)) return;
|
||||
int saved = errno;
|
||||
#ifdef __ANDROID__
|
||||
__android_log_print(error ? ANDROID_LOG_ERROR : ANDROID_LOG_INFO, "QuestKGSL",
|
||||
"%s: GPUOBJ compatibility path, errno=%d", operation, error);
|
||||
#else
|
||||
fprintf(stderr, "[QuestKGSL] %s: GPUOBJ compatibility path, errno=%d\n", operation, error);
|
||||
#endif
|
||||
errno = saved;
|
||||
}
|
||||
|
||||
int quest_kgsl_ioctl(quest_ioctl_fn real, int fd, unsigned long request, void *arg) {
|
||||
int rc = real(fd, request, arg);
|
||||
if (rc != -1 || (errno != EACCES && errno != EPERM) || arg == NULL) return rc;
|
||||
|
||||
if (request == IOCTL_KGSL_GPUMEM_ALLOC_ID) {
|
||||
struct kgsl_gpumem_alloc_id *old = arg;
|
||||
struct kgsl_gpuobj_alloc alloc = {.size = old->size, .flags = old->flags};
|
||||
rc = real(fd, IOCTL_KGSL_GPUOBJ_ALLOC, &alloc);
|
||||
if (rc != 0) { report("alloc failed", errno); return rc; }
|
||||
struct kgsl_gpuobj_info info = {.id = alloc.id};
|
||||
rc = real(fd, IOCTL_KGSL_GPUOBJ_INFO, &info);
|
||||
int error = errno;
|
||||
if (rc == 0 && (info.size > SIZE_MAX || alloc.mmapsize > SIZE_MAX ||
|
||||
info.gpuaddr > ULONG_MAX || info.flags > UINT_MAX)) {
|
||||
rc = -1;
|
||||
error = EOVERFLOW;
|
||||
}
|
||||
if (rc != 0) {
|
||||
/* The caller never receives this allocation, so clean it up on an INFO failure. */
|
||||
struct kgsl_gpuobj_free release = {.id = alloc.id};
|
||||
if (real(fd, IOCTL_KGSL_GPUOBJ_FREE, &release) != 0)
|
||||
report("cleanup failed", errno);
|
||||
errno = error;
|
||||
report("info failed", error);
|
||||
return rc;
|
||||
}
|
||||
old->id = alloc.id;
|
||||
old->flags = (unsigned int)info.flags;
|
||||
old->size = info.size;
|
||||
old->mmapsize = alloc.mmapsize;
|
||||
old->gpuaddr = info.gpuaddr;
|
||||
report("allocation succeeded", 0);
|
||||
return 0;
|
||||
}
|
||||
if (request == IOCTL_KGSL_GPUMEM_FREE_ID) {
|
||||
const struct kgsl_gpumem_free_id *old = arg;
|
||||
struct kgsl_gpuobj_free release = {.id = old->id};
|
||||
rc = real(fd, IOCTL_KGSL_GPUOBJ_FREE, &release);
|
||||
if (rc != 0) report("free failed", errno);
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#ifndef DROIDDECK_QUEST_KGSL_COMPAT_H
|
||||
#define DROIDDECK_QUEST_KGSL_COMPAT_H
|
||||
|
||||
/* Use the vendored Qualcomm UAPI, including its architecture-dependent ioctl sizes. */
|
||||
#ifndef __user
|
||||
#define __user
|
||||
#endif
|
||||
#include "../../app/src/main/cpp/adrenotools/src/hook/kgsl.h"
|
||||
|
||||
typedef int (*quest_ioctl_fn)(int, unsigned long, void *);
|
||||
__attribute__((visibility("hidden")))
|
||||
int quest_kgsl_ioctl(quest_ioctl_fn real, int fd, unsigned long request, void *arg);
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,77 @@
|
||||
/* SPDX-License-Identifier: GPL-3.0-or-later */
|
||||
#include "kgsl_compat.h"
|
||||
#include <assert.h>
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static unsigned long calls[8];
|
||||
static int count, old_error, alloc_error, info_error, free_error;
|
||||
static unsigned long address;
|
||||
static int mock(int fd, unsigned long request, void *arg) {
|
||||
assert(fd == 42 && count < 8);
|
||||
calls[count++] = request;
|
||||
if (request == IOCTL_KGSL_GPUMEM_ALLOC_ID || request == IOCTL_KGSL_GPUMEM_FREE_ID) {
|
||||
if (old_error) { errno = old_error; return -1; }
|
||||
return 0;
|
||||
}
|
||||
if (request == IOCTL_KGSL_GPUOBJ_ALLOC) {
|
||||
if (alloc_error) { errno = alloc_error; return -1; }
|
||||
struct kgsl_gpuobj_alloc *p = arg;
|
||||
assert(p->size == 123 && p->flags == 0x10000 && !p->metadata && !p->metadata_len);
|
||||
p->id = 77; p->size = 4096; p->mmapsize = 8192;
|
||||
return 0;
|
||||
}
|
||||
if (request == IOCTL_KGSL_GPUOBJ_INFO) {
|
||||
struct kgsl_gpuobj_info *p = arg;
|
||||
assert(p->id == 77);
|
||||
if (info_error) { errno = info_error; return -1; }
|
||||
p->gpuaddr = address; p->flags = 0x20000; p->size = 4096;
|
||||
return 0;
|
||||
}
|
||||
if (request == IOCTL_KGSL_GPUOBJ_FREE) {
|
||||
struct kgsl_gpuobj_free *p = arg;
|
||||
assert(p->id == 77 && !p->flags && !p->priv && !p->type && !p->len);
|
||||
if (free_error) { errno = free_error; return -1; }
|
||||
return 0;
|
||||
}
|
||||
errno = EACCES;
|
||||
return -1;
|
||||
}
|
||||
static struct kgsl_gpumem_alloc_id reset(void) {
|
||||
count = old_error = alloc_error = info_error = free_error = 0;
|
||||
address = 0x100000;
|
||||
return (struct kgsl_gpumem_alloc_id){.size = 123, .flags = 0x10000};
|
||||
}
|
||||
int main(void) {
|
||||
struct kgsl_gpumem_alloc_id p = reset();
|
||||
assert(quest_kgsl_ioctl(mock, 42, IOCTL_KGSL_GPUMEM_ALLOC_ID, &p) == 0 && count == 1);
|
||||
p = reset(); old_error = EACCES;
|
||||
assert(quest_kgsl_ioctl(mock, 42, IOCTL_KGSL_GPUMEM_ALLOC_ID, &p) == 0);
|
||||
assert(count == 3 && p.id == 77 && p.size == 4096 && p.mmapsize == 8192);
|
||||
assert(p.flags == 0x20000 && p.gpuaddr == address);
|
||||
p = reset(); old_error = EPERM; address = 0;
|
||||
assert(quest_kgsl_ioctl(mock, 42, IOCTL_KGSL_GPUMEM_ALLOC_ID, &p) == 0 && p.gpuaddr == 0);
|
||||
p = reset(); old_error = ENOMEM;
|
||||
assert(quest_kgsl_ioctl(mock, 42, IOCTL_KGSL_GPUMEM_ALLOC_ID, &p) == -1 && errno == ENOMEM && count == 1);
|
||||
p = reset(); old_error = EACCES; alloc_error = ENOMEM;
|
||||
assert(quest_kgsl_ioctl(mock, 42, IOCTL_KGSL_GPUMEM_ALLOC_ID, &p) == -1 && errno == ENOMEM && count == 2);
|
||||
assert(p.id == 0 && p.size == 123);
|
||||
p = reset(); old_error = EACCES; info_error = EIO;
|
||||
assert(quest_kgsl_ioctl(mock, 42, IOCTL_KGSL_GPUMEM_ALLOC_ID, &p) == -1 && errno == EIO);
|
||||
assert(count == 4 && calls[3] == IOCTL_KGSL_GPUOBJ_FREE && p.id == 0);
|
||||
p = reset(); old_error = EACCES; info_error = EIO; free_error = ENOSPC;
|
||||
assert(quest_kgsl_ioctl(mock, 42, IOCTL_KGSL_GPUMEM_ALLOC_ID, &p) == -1 && errno == EIO && count == 4);
|
||||
p = reset(); old_error = EACCES;
|
||||
assert(quest_kgsl_ioctl(mock, 42, IOCTL_KGSL_GPUMEM_ALLOC_ID, NULL) == -1 && count == 1);
|
||||
p = reset(); old_error = EACCES;
|
||||
assert(quest_kgsl_ioctl(mock, 42, 0x1234, &p) == -1 && errno == EACCES && count == 1);
|
||||
struct kgsl_gpumem_free_id release = {.id = 77};
|
||||
reset(); old_error = EACCES;
|
||||
assert(quest_kgsl_ioctl(mock, 42, IOCTL_KGSL_GPUMEM_FREE_ID, &release) == 0 && count == 2);
|
||||
reset(); old_error = EPERM; free_error = EIO;
|
||||
assert(quest_kgsl_ioctl(mock, 42, IOCTL_KGSL_GPUMEM_FREE_ID, &release) == -1 && errno == EIO && count == 2);
|
||||
reset();
|
||||
assert(quest_kgsl_ioctl(mock, 42, IOCTL_KGSL_GPUMEM_FREE_ID, &release) == 0 && count == 1);
|
||||
puts("PASS: 12 GPUOBJ translation, passthrough, failure and cleanup scenarios");
|
||||
}
|
||||
@@ -16,7 +16,9 @@ SOURCE_PATHS = ['app/build.gradle', 'app/src/main/cpp/fakeinput_steam.cpp', 'app
|
||||
'tools/linuxfs', 'tools/proot', 'tools/aaudio-sink', 'tools/directaudio',
|
||||
'tools/gamescope/release.env', 'tools/droiddeck-esync/release.env',
|
||||
'tools/mangoapp', 'tools/msitools', 'tools/build_local.sh',
|
||||
'tools/release/runtime_inputs.py', 'app/src/main/assets/desktop-removal']
|
||||
'tools/release/runtime_inputs.py', 'app/src/main/assets/desktop-removal',
|
||||
'tools/quest', 'app/src/main/cpp/adrenotools/src/hook/kgsl.h',
|
||||
'app/src/main/assets/graphics_driver/adrenotools-turnip25.1.0.tzst']
|
||||
|
||||
|
||||
def digest(path):
|
||||
@@ -48,11 +50,14 @@ def outputs(root):
|
||||
for library in ['libproot.so', 'libproot-loader.so', 'libdirectaudiorelay.so']:
|
||||
result['lib/arm64-v8a/' + library] = 'app/src/main/jniLibs/arm64-v8a/' + library
|
||||
result['assets/pulseaudio.tzst'] = 'app/build/prepared-assets/pulseaudio.tzst'
|
||||
result['assets/graphics_driver/adrenotools-turnip25.1.0-quest1.tzst'] = \
|
||||
'app/build/prepared-assets/graphics_driver/adrenotools-turnip25.1.0-quest1.tzst'
|
||||
return result
|
||||
|
||||
|
||||
def required():
|
||||
paths = ['assets/linuxfs/' + name for name in ['libfakeinput.so', 'libblsession.so',
|
||||
paths = ['assets/graphics_driver/adrenotools-turnip25.1.0-quest1.tzst']
|
||||
paths += ['assets/linuxfs/' + name for name in ['libfakeinput.so', 'libblsession.so',
|
||||
'libblfastpath.so', 'libssbs.so', 'usr/local/bin/droiddeck-clipboard',
|
||||
'usr/local/bin/gamescope', 'usr/local/lib/droiddeck/uruntime']]
|
||||
for arch in ['x86_64', 'i386']:
|
||||
|
||||
@@ -7,3 +7,4 @@ standard com.droiddeck.launcher -
|
||||
pubg com.tencent.ig -pubg
|
||||
antutu com.antutu.benchmark.full -antutu
|
||||
ludashi com.ludashi.benchmark -ludashi
|
||||
quest com.droiddeck.quest -quest
|
||||
@@ -0,0 +1,23 @@
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
QUEST = Path(__file__).resolve().parents[1] / 'quest'
|
||||
|
||||
|
||||
@unittest.skipUnless(sys.platform == 'linux' and shutil.which('gcc'), 'needs Linux and gcc')
|
||||
class QuestKgslTest(unittest.TestCase):
|
||||
def test_allocation_passthrough_translation_and_cleanup(self):
|
||||
with tempfile.TemporaryDirectory() as folder:
|
||||
binary = str(Path(folder) / 'test-kgsl')
|
||||
subprocess.run(['gcc', '-Wall', '-Wextra', '-Werror', '-O2', '-o', binary,
|
||||
str(QUEST / 'test_kgsl_compat.c'), str(QUEST / 'kgsl_compat.c')], check=True)
|
||||
result = subprocess.run([binary], capture_output=True, text=True, check=True, timeout=10)
|
||||
self.assertIn('PASS: 12 GPUOBJ', result.stdout)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -55,6 +55,20 @@ class RuntimeInputsTest(unittest.TestCase):
|
||||
with self.assertRaisesRegex(ValueError, 'sources changed'):
|
||||
runtime.verify(self.root)
|
||||
|
||||
def test_changed_quest_source_rejects_old_outputs(self):
|
||||
source = self.root / 'tools/quest/kgsl_compat.c'
|
||||
source.parent.mkdir(parents=True)
|
||||
source.write_text('old compatibility code')
|
||||
runtime.record(self.root)
|
||||
source.write_text('new compatibility code')
|
||||
with self.assertRaisesRegex(ValueError, 'sources changed'):
|
||||
runtime.verify(self.root)
|
||||
|
||||
def test_missing_quest_driver_cannot_be_attested(self):
|
||||
(self.root / 'app/build/prepared-assets/graphics_driver/adrenotools-turnip25.1.0-quest1.tzst').unlink()
|
||||
with self.assertRaisesRegex(ValueError, 'quest1.tzst'):
|
||||
runtime.record(self.root)
|
||||
|
||||
def test_modified_prebuilt_binary_is_rejected(self):
|
||||
runtime.record(self.root)
|
||||
(self.root / 'app/src/main/jniLibs/arm64-v8a/libproot.so').write_bytes(b'old APK binary')
|
||||
|
||||
Reference in new issue
Block a user