Skip to content

Android: custom Vulkan driver packages (Turnip) on Adreno - #19955

Open
Davey-Hughes wants to merge 3 commits into
libretro:masterfrom
Davey-Hughes:android-custom-vulkan-driver
Open

Davey-Hughes wants to merge 3 commits into
libretro:masterfrom
Davey-Hughes:android-custom-vulkan-driver

Conversation

@Davey-Hughes

@Davey-Hughes Davey-Hughes commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Guidelines

  1. Rebase before opening a pull request
  2. If you are sending several unrelated fixes or features, use a branch and a separate pull request for each
  3. If possible try squashing everything in a single commit. This is particularly beneficial in the case of feature merges since it allows easy bisecting when a problem arises
  4. RetroArch codebase follows C89 coding rules for portability across many old platforms check using C89_BUILD=1

Description

Sideloaded arm64 Android builds can load a Vulkan driver package, such as Mesa's Turnip, on Adreno GPUs instead of the system driver. The main use is cores the stock driver renders badly. On an Adreno 740, for example, the stock driver draws stripe garbage for paraLLEl-RDP in Mupen64Plus-Next (SM64, OoT, MK64), and T30, a Turnip build, renders all three correctly.

Related feature requests: #15472, #18143.

Commits

  1. deps: add a subset of libadrenotools. Adds the files from bylaws/libadrenotools 8fae8ce and its linkernsbypass submodule that load a driver into its own linker namespace. They are unmodified and keep their BSD-2-Clause licenses. Its README lists what was kept and what was left out. Nothing builds them yet.
  2. Android: load custom Vulkan driver packages on Adreno. Adds a new setting, video_vulkan_driver_path, that names a zip holding meta.json and one .so. An empty value means the system driver, and overrides carry it. The package is checked and extracted to internal storage once per version. This commit also adds the fallbacks, the crash state and the Safe start shortcut. The package logic is platform-free (gfx/common/vulkan_custom_driver.c), and a unit sample and a CI job cover it.
  3. Menu: Custom Vulkan Driver setting. Adds a row under Settings > Video > Output. OK browses <system>/vulkan_drivers, and Start goes back to the system driver. The sublabel says when the chosen driver crashed. A new choice takes effect at the next Vulkan start, and picking one never restarts video.

No driver is bundled. Users download a package (for example a Turnip build, in the same zip format the AdrenoTools-based emulators load) and copy it to <system>/vulkan_drivers/. If wanted, an online updater source can follow in a later PR.

Gating

  • Build: arm64-v8a with Vulkan only, and all new code is under HAVE_ADRENOTOOLS. The API 16 build passes HAVE_ADRENOTOOLS=0.
  • Play Store and Quest builds are excluded. Play's Device and Network Abuse policy forbids loading executable code (such as .so files) from outside Play. That is why the Play builds already get cores through Play Feature Delivery. The Quest build is also a Play build (PLAY_STORE_BUILD=1). On Quest, the OpenXR runtime also does swapchain interop on RetroArch's VkDevice, which nobody has tested with Turnip. Android.mk sets HAVE_ADRENOTOOLS only when neither PLAY_STORE_BUILD nor HAVE_OPENXR is set, and the Play and Quest APKs carry no adrenotools symbols.
  • Runtime: needs an Adreno GPU (/dev/kgsl-3d0) and Android 9 (API 28) or later. Elsewhere the row is hidden, and a synced-in value is ignored with an OSD message.
  • libadrenotools.so loads only when a custom driver is first opened, so its linkernsbypass constructor, which remaps dlopen's code page, never runs for users who don't pick a driver.

Fallbacks and crash state

Case Result
Bad package (meta.json, .so count, not arm64 ELF, over 64 MiB, missing) System driver, reason logged
Instance creation fails, or no GPU Retried on the system loader in the same init
Device, surface or first swapchain fails Context redone on the system loader, same process
Mesa package, but the stock driver came up (libadrenotools falls back silently) Seen through the driverID and counted as failed
Process dies in the driver's init Later starts skip that driver for that core until it is picked again
5th distinct driver or 9th path in one process System driver, "Restart RetroArch to change the Vulkan driver."

Each case shows an OSD message once per path per session. A crash after the first swapchain is not caught. For that, the user presses Start on the row or uses Safe start.

Safe start is a launcher shortcut (Android 7.1+), added before the first custom load. It starts RetroArch once on the system driver, restarting the process if needed, and leaves the saved value alone. Meanwhile the row reads "System (Safe start)".

Vendor policy (please weigh in)

Packages from any vendor load. Only Mesa Turnip was tested. The non-Mesa path was checked with a Turnip build relabelled "vendor": "Qualcomm".

How it works:

  • vendor is required in meta.json. A package counts as Mesa when it says "Mesa" (any case).
  • Mesa packages get a load check. After the instance is created, the GPU's driverID must not be Qualcomm's proprietary one. If it is, libadrenotools fell back to the stock driver without saying so. The start then counts as failed and the context is redone on the system driver.
  • Other vendors only need a visible GPU. Once per init they log that the vendor is untested and that the load can't be confirmed.

Why: Qualcomm driver repacks are widely used with other Android emulators, so refusing them outright would turn those users away. But a repack reports the same driverID as the stock driver. The Mesa check would mark every repack as failed, because the driverID can't tell a working repack from a silent fallback. We also considered comparing against the stock driver's version, saved from an earlier system-driver run. That needs a prior run and more saved state, and it misfires when a repack has the same version as the stock driver. So other vendors load unverified, and the log says so rather than claiming the custom driver is running.

Qualcomm repacks are proprietary blobs taken from other devices. If you'd rather not load them, Mesa-only is a small change to the package check.

Known issues

  • T30 + Dolphin + threaded video: with threaded video on, Dolphin crashed in 3 of 18 video reinits on T30. There were no crashes in 20 reinits on the system driver or in 10 on T30 with threaded video off. It also crashed with every core on T30 and no switching, so this is the driver, not driver switching.
  • CACHE_CONTEXT fallback: after a device or swapchain failure, a new custom instance under a cached context takes its own fallback path. Code review and unit tests cover it, but no device run did.
  • DEFLATE bound: libretro-common's streaming DEFLATE extraction checks the header's uncompressed size only after all input is used. A zip that understates it can write ~1000x its compressed size to internal storage before failing. DEFLATE64 and LZMA already stop early. The fix touches all archive extraction, so it will come as its own small PR.

Testing

  • Unit sample samples/gfx/vulkan_custom_driver: 315 checks covering the package checks, meta.json, the crash state and session limits. It passes on Linux under ASan+UBSan, and in a mingw-w64 build under wine. The new vulkan-custom-driver CI job runs it.
  • Builds, each commit: Linux configure + make, and NDK for sideloaded arm64, Play, Quest and x86_64. Only sideloaded arm64 ships the three adrenotools libraries. C89_BUILD=1 on touched objects and --disable-threads both pass. Local runs of the covering CI jobs fail only where master does.
  • Fork CI: every workflow passes except libsmb2, which fails the same way on master 92b11d9.

AYN Thor (Adreno 740, Android 13), with T30 and "Mesa Turnip driver v26.0.0 - R8". The comparison, the bad packages, pruning and Safe start through its intent extra were rerun on this branch's final commits. The switching, closing-content and Safe start UI checks ran on the build before the final review fixes.

  • Against the proof of concept: 66 cases (8 cores, 22 games, 3 drivers). Every case gave the same driver, crash and alive result as lim2222's PoC rebased on the same master, and the 44 Turnip screenshot pairs show no render differences.
  • Switching: per-core overrides flipped four cores between T30 and system for 10 cycles. That gave 80 inits, all on the expected driver, each driver mapped once, no crashes. The memory growth per cycle is smaller than in a system-only control and comes from core load/unload.
  • Closing content: all 229 closes reinitialised video on the base driver.
  • Bad packages: each fell back to the system driver:
    • a truncated header (refused);
    • a truncated body (load failure detected);
    • a core .so (no GPU);
    • a constructor that segfaults (first start dies, later starts skip it with a crash message once).
  • Other vendor: T30 relabelled "vendor": "Qualcomm" loads and runs, logging both warnings once per init.
  • Pruning: replacing a zip deletes its old extraction at the next start, and a removed zip's extraction goes too.
  • Safe start and the row: cold start, mid-game restart, no restart on a second tap, Start reset and crash-note clearing all behave as described.
  • The custom driver mode alone works, so libadrenotools' file redirect hook is left out.

Credit

Built from lim2222's proof of concept, https://github.com/lim2222/RetroArch-Turnip (branch feature/android-turnip-driver). lim2222 is co-author of commits 2 and 3.

Related Issues

[Any issues this pull request may be addressing]

Related Pull Requests

[Any other PRs from related repositories that might be needed for this pull request to work]

Reviewers

[If possible @mention all the people that should review your pull request]

Davey-Hughes and others added 3 commits October 30, 2026 20:04
The files of bylaws/libadrenotools 8fae8ce and its linkernsbypass
submodule that load a custom Vulkan driver into its own linker
namespace on Android, unmodified, with their licenses. README.md names
the commits, the files and what was left out. Nothing builds them yet.
Sideloaded arm64 builds can load a Vulkan driver package (a zip with
meta.json and one .so, such as Mesa's Turnip) on Adreno GPUs with
Android 9 or later, through libadrenotools. The new
video_vulkan_driver_path setting names the zip; empty is the system
driver, and core, content-directory and game overrides carry it. The
package is checked and extracted to internal storage once per version.

Packages from any vendor are accepted. Only Mesa ones have been
tested, so any other vendor logs that it is untested and that its load
cannot be confirmed. A Mesa package that leaves the stock Qualcomm
driver running, as the loader does when the custom library fails to
load, counts as failed.

When the custom driver fails to create an instance, a device or the
first swapchain, the context is redone on the system driver in the same
process. A driver that takes the process down during its init is turned
off for that core until it is picked again, and a "Safe start" launcher
shortcut starts RetroArch once on the system driver. Play Store, Quest
and API 16 builds do not carry any of it.

Co-authored-by: lim2222 <johnlky222@gmail.com>
Settings > Video > Output gains a Custom Vulkan Driver row on builds
and devices that can load one. OK opens a browser for driver zips in
<system>/vulkan_drivers, Start goes back to the system driver, and the
sublabel says when the chosen driver crashed during its last start and
is off for the running core. While RetroArch runs from the Safe start
shortcut, the row reads "System (Safe start)" and the sublabel names
the driver the next normal start uses. The choice applies at the next
Vulkan start; picking a driver never restarts video.

Co-authored-by: lim2222 <johnlky222@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant