Detect and select the system Vulkan backend on PowerVR

This commit is contained in:
wallentx committed 2026-10-11 04:01:50 -05:00
1 parent fda618618c
commit 844a1a2473
24 files changed
+823 -84

No files matched your search

@@ -1009,8 +1009,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
// the evdev codes we inject.
FileUtils.copyAsset(this, "wayland/keymap.xkb", File(runtimeDir, "keymap.xkb"))
// Turnip, not the system Adreno driver: importing the dma-bufs gamescope commits needs
// VK_EXT_image_drm_format_modifier, which the system driver does not implement.
// Adreno imports dma-bufs with Turnip. PowerVR uses its system Vulkan driver
// to import the Android hardware buffers supplied by the runtime bridge.
val turnip = TurnipDriver(this)
val driverId = if (CompositorHost.isStarted) null else turnip.install()
@@ -3,21 +3,20 @@ package com.droiddeck.launcher.core
import android.content.Context
import android.os.Build
import com.droiddeck.launcher.R
import com.droiddeck.launcher.gpu.SystemVulkanDriver
import java.io.File
/**
* Whether this device can draw a Linux session at all.
*
* The runtime draws with Turnip, an Adreno driver. On Mali, Xclipse and PowerVR the compositor
* gets no usable Vulkan device and a session comes up as sound over a black screen - a failure
* with nothing in it to read. The app cannot fix that, so it says so before the download rather
* than after it. Adreno is recognised by what only Qualcomm's stack has: the KGSL node, or the
* vendor's own Vulkan driver at its usual path.
*/
/** Whether this device has a graphics backend the runtime can use. Adreno uses Turnip;
* PowerVR uses its vendor Vulkan driver through the bundled runtime bridge. Other GPU families
* still need a supported backend before a session can display frames. */
object DeviceSupport {
fun adreno(): Boolean =
File("/sys/class/kgsl/kgsl-3d0").exists() || File("/vendor/lib64/hw/vulkan.adreno.so").exists()
fun graphicsAvailable(): Boolean = graphicsAvailable(adreno(), SystemVulkanDriver.isAvailable())
internal fun graphicsAvailable(adreno: Boolean, powerVrReady: Boolean): Boolean = adreno || powerVrReady
/** The chip as the device names it, for the card that explains the refusal. */
fun gpuName(context: Context): String {
val soc = if (Build.VERSION.SDK_INT >= 31) Build.SOC_MODEL.takeIf { it.isNotBlank() && it != Build.UNKNOWN } else null
@@ -93,7 +93,7 @@ object DriverPairs {
* the 710/720/722, and upstream Banners-Turnip for the rest. Null when nothing fits (not Adreno).
*/
fun recommendedKey(gpu: GpuInfo, pairs: List<DriverPair>): String? = when (gpu.family) {
Family.NOT_ADRENO -> null
Family.NOT_ADRENO, Family.POWERVR -> null
Family.A8XX -> if (gpu.model == 840) newestDdTurnip(pairs) ?: WN_BALANCED else WN_BALANCED
Family.A7XX_LOW -> BANNER_710
Family.A7XX -> if (gpu.oneUi8Gen2) BANNER_ONEUI else BANNER
@@ -35,6 +35,7 @@ data class GpuInfo(
A6XX("Adreno 6xx"),
/** An Adreno whose model KGSL does not give: treated as the newest family it could be. */
ADRENO_UNKNOWN("Adreno"),
POWERVR("PowerVR"),
NOT_ADRENO("Not an Adreno GPU");
/** [label] in the app's language: the Adreno families are names, the rest is words. */
@@ -56,15 +57,22 @@ data class GpuInfo(
val supportText: String
get() = when (support) {
Support.TESTED -> "Supported"
Support.UNTESTED -> if (family == Family.A7XX_LOW) "Experimental: its drivers are test builds"
else "Outside tested hardware (Adreno 650, 725 and newer): it may not run"
Support.UNSUPPORTED -> "Not supported: DroidDeck needs an Adreno (Snapdragon) GPU"
Support.UNTESTED -> when (family) {
Family.POWERVR -> "System PowerVR driver: compatibility varies by device"
Family.A7XX_LOW -> "Experimental: its drivers are test builds"
else -> "Outside tested hardware (Adreno 650, 725 and newer): it may not run"
}
Support.UNSUPPORTED -> "Not supported: no compatible graphics backend is available for this GPU"
}
/** [supportText] in the app's language. */
fun supportText(context: Context): String = context.getString(when (support) {
Support.TESTED -> R.string.gpuinfo_supported
Support.UNTESTED -> if (family == Family.A7XX_LOW) R.string.gpuinfo_experimental else R.string.gpuinfo_below_tested
Support.UNTESTED -> when (family) {
Family.POWERVR -> R.string.gpuinfo_powervr
Family.A7XX_LOW -> R.string.gpuinfo_experimental
else -> R.string.gpuinfo_below_tested
}
Support.UNSUPPORTED -> R.string.gpuinfo_unsupported
})
@@ -94,10 +102,15 @@ data class GpuInfo(
fromPlatform > 0 -> "platform"
else -> ""
}
val family = familyOf(adreno, model)
val family = familyOf(adreno, model, SystemVulkanDriver.isPowerVr())
val samsung = Build.MANUFACTURER.equals("samsung", ignoreCase = true)
return GpuInfo(
name = if (!adreno) Build.HARDWARE.ifBlank { UNNAMED } else if (model > 0) "Adreno $model" else "Adreno",
name = when {
family == Family.POWERVR -> VulkanInfo.cachedOrNull()?.get("device")?.takeIf { it.contains("PowerVR", ignoreCase = true) } ?: "PowerVR"
!adreno -> Build.HARDWARE.ifBlank { UNNAMED }
model > 0 -> "Adreno $model"
else -> "Adreno"
},
model = model, family = family, soc = soc,
oneUi8Gen2 = samsung && model == 740,
kgslName = raw.orEmpty(), modelSource = source,
@@ -122,7 +135,8 @@ data class GpuInfo(
""
}
internal fun familyOf(adreno: Boolean, model: Int): Family = when {
internal fun familyOf(adreno: Boolean, model: Int, powerVr: Boolean = false): Family = when {
!adreno && powerVr -> Family.POWERVR
!adreno -> Family.NOT_ADRENO
model == 0 -> Family.ADRENO_UNKNOWN
model >= 800 -> Family.A8XX
@@ -3,8 +3,12 @@ package com.droiddeck.launcher.gpu;
import android.content.Context;
import android.util.Log;
import com.droiddeck.launcher.R;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;
/**
* The Vulkan driver a session draws with inside the runtime: in Steam mode the native client's
@@ -17,7 +21,8 @@ import java.util.List;
* these are glibc processes, that one is bionic.
*
* <p>"" (the default) means the driver the runtime was built with, at
* {@code usr/lib/libvulkan_freedreno.so} inside it - nothing is set and nothing is touched.
* {@code usr/lib/libvulkan_freedreno.so} on Adreno, or the bundled bridge to the system
* PowerVR driver.
* Otherwise the value is an imported driver's id ({@link LinuxVulkanDriverManager}), and the
* session is handed that driver's ICD manifest in {@code BL_VK_DRIVER}: the session script points
* the Vulkan loader at it with {@code VK_DRIVER_FILES} after checking both the manifest and the
@@ -47,30 +52,44 @@ public final class LinuxVulkanDriver {
}
/**
* Label for a stored value. An id that is no longer installed says so - the launch path falls
* back to the runtime's own driver for it.
* Label for a stored value. Missing imports fall back to the runtime on Adreno;
* PowerVR requires another explicit selection or Auto before launching.
*/
public static String optionLabel(Context context, String value) {
if (value == null || value.isEmpty()) return "Runtime default (built into the runtime)";
if (value == null || value.isEmpty()) return SystemVulkanDriver.isPowerVr()
? context.getString(R.string.gpu_system_powervr) : "Runtime default (built into the runtime)";
LinuxVulkanDriverManager m = new LinuxVulkanDriverManager(context);
if (!m.isInstalled(value)) return value + " (imported, missing - uses the runtime default)";
if (!m.isInstalled(value)) return value + (SystemVulkanDriver.isPowerVr()
? " (imported, missing)" : " (imported, missing - uses the runtime default)");
String ver = m.getDriverVersion(value);
return m.getDriverName(value) + (ver.isEmpty() ? "" : " " + ver) + " (imported)";
}
/**
* The ICD manifest path to hand the session, or null to leave the runtime's own driver alone.
* An id whose import is gone resolves to null rather than failing the launch.
* A missing import uses the runtime's own driver on Adreno. PowerVR fails clearly instead:
* its system backend needs environment setup that an explicit import does not receive.
*/
public static String resolveIcdPath(Context context, String value) {
boolean powerVr = SystemVulkanDriver.isPowerVr();
return resolveIcdPath(value, powerVr,
() -> SystemVulkanDriver.icdPath(context), id -> {
String icd = new LinuxVulkanDriverManager(context).getIcdPath(id);
if (icd == null) Log.w(TAG, "imported Linux driver \"" + id + "\" is gone"
+ (powerVr ? "" : "; using the runtime's own driver"));
else Log.i(TAG, "Linux session draws with imported driver " + id + " (" + icd + ")");
return icd;
});
}
/** Resolve the selected backend before looking up an imported driver. */
static String resolveIcdPath(String value, boolean powerVr, Supplier<String> systemIcd,
Function<String, String> importedIcd) {
if (SystemVulkanDriver.usesDefault(powerVr, value)) return systemIcd.get();
if (value == null || value.isEmpty()) return null;
LinuxVulkanDriverManager m = new LinuxVulkanDriverManager(context);
String icd = m.getIcdPath(value);
if (icd == null) {
Log.w(TAG, "imported Linux driver \"" + value + "\" is gone; using the runtime's own driver");
return null;
}
Log.i(TAG, "Linux session draws with imported driver " + value + " (" + icd + ")");
String icd = importedIcd.apply(value);
if (powerVr && icd == null) throw new IllegalStateException(
"Selected Linux Vulkan driver \"" + value + "\" is unavailable. Select an installed driver or use Auto.");
return icd;
}
}
@@ -0,0 +1,147 @@
package com.droiddeck.launcher.gpu;
import android.content.Context;
import android.os.Build;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import com.droiddeck.launcher.core.FileUtils;
import com.droiddeck.launcher.core.TarZst;
import java.io.File;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.List;
import java.util.UUID;
import org.json.JSONObject;
/** Isolated glibc-to-Android Vulkan bridge. It does not replace the rootfs's Mesa libraries. */
public final class SystemVulkanDriver {
private static final String ASSET = "hybris/runtime.tzst";
private static final String HAL = "/vendor/lib64/hw/vulkan.powervr.so";
private static final String[] REQUIRED = {
"lib/libhybris-vulkan-icd.so.0", "lib/libhybris-common.so.1",
"lib/libhybris/linker/q.so", "lib/libVkLayer_hybris_compat.so",
"run-proton.py", "wsi/libVkLayer_window_system_integration.so",
"wsi/VkLayer_window_system_integration.json",
"dxvk/x32/d3d8.dll", "dxvk/x32/d3d9.dll", "dxvk/x32/d3d10core.dll",
"dxvk/x32/d3d11.dll", "dxvk/x32/dxgi.dll",
"dxvk/x64/d3d8.dll", "dxvk/x64/d3d9.dll", "dxvk/x64/d3d10core.dll",
"dxvk/x64/d3d11.dll", "dxvk/x64/dxgi.dll"
};
private SystemVulkanDriver() {}
public static boolean isPowerVr() { return new File(HAL).isFile(); }
/** HAL detection names the GPU; launching also needs the real DRM render node. */
public static boolean isAvailable() { return isPowerVr() && renderNodeAvailable(); }
public static boolean renderNodeAvailable() {
String node = "/dev/dri/renderD128";
try {
return renderNodeAvailable(OsConstants.S_ISCHR(Os.stat(node).st_mode),
Os.access(node, OsConstants.R_OK), Os.access(node, OsConstants.W_OK));
} catch (ErrnoException error) {
return false;
}
}
static boolean renderNodeAvailable(boolean characterDevice, boolean readable, boolean writable) {
return characterDevice && readable && writable;
}
public static boolean usesDefault(String choice) {
return usesDefault(isPowerVr(), choice);
}
static boolean usesDefault(boolean powerVr, String choice) {
return powerVr && (choice == null || choice.isEmpty());
}
/** APK-versioned directories stay intact while an older session still has libraries mapped. */
public static synchronized File prepare(Context context) throws IOException {
if (!isPowerVr()) throw new IOException("System PowerVR Vulkan driver is unavailable");
if (!renderNodeAvailable()) throw new IOException(
"System PowerVR requires a readable and writable character device at /dev/dri/renderD128");
File target = runtimeDirectory(context);
File base = target.getParentFile();
if (complete(target)) return target;
if (!base.isDirectory() && !base.mkdirs()) throw new IOException("Cannot create system Vulkan directory");
File staged = new File(base, ".stage-" + UUID.randomUUID());
try {
if (!TarZst.extractAsset(context, ASSET, staged)) throw new IOException("Cannot unpack system Vulkan runtime");
for (String name : REQUIRED)
if (!present(staged, name)) throw new IOException("Missing system Vulkan component: " + name);
if (!new File(staged, "run-proton.py").canExecute())
throw new IOException("System Vulkan Proton wrapper is not executable");
JSONObject library = new JSONObject();
library.put("library_path", new File(target, REQUIRED[0]).getPath());
library.put("api_version", "1.3.0");
JSONObject manifest = new JSONObject();
manifest.put("file_format_version", "1.0.0");
manifest.put("ICD", library);
Files.write(new File(staged, "icd.json").toPath(), manifest.toString().getBytes(StandardCharsets.UTF_8));
if (target.exists()) throw new IOException("Incomplete system Vulkan runtime directory: " + target.getName());
if (!staged.renameTo(target)) throw new IOException("Cannot publish system Vulkan runtime");
return target;
} catch (org.json.JSONException error) {
throw new IOException("Cannot write system Vulkan manifest", error);
} finally {
if (staged.exists()) FileUtils.delete(staged);
}
}
/** The current APK's versioned runtime path, without extracting or modifying it. */
public static File runtimeDirectory(Context context) throws IOException {
String version;
try (BufferedReader input = new BufferedReader(new InputStreamReader(
context.getAssets().open("hybris/version.txt"), StandardCharsets.US_ASCII))) {
version = input.readLine();
}
if (version == null || !version.matches("[0-9a-f]{64}")) throw new IOException("Invalid system Vulkan runtime version");
return new File(new File(context.getFilesDir(), "system_vulkan"), version);
}
private static boolean complete(File directory) {
if (!new File(directory, "icd.json").isFile()) return false;
for (String name : REQUIRED) if (!present(directory, name)) return false;
return new File(directory, "run-proton.py").canExecute();
}
private static boolean present(File directory, String name) {
File file = new File(directory, name);
return file.isFile() && file.length() > 0;
}
public static String icdPath(Context context) {
try {
return new File(prepare(context), "icd.json").getPath();
} catch (IOException error) {
throw new IllegalStateException("System Vulkan runtime is unavailable", error);
}
}
public static void addEnvironment(Context context, List<String> environment) {
File directory;
try { directory = prepare(context); }
catch (IOException error) { throw new IllegalStateException("System Vulkan runtime is unavailable", error); }
environment.add("HYBRIS_ANDROID_SDK_VERSION=" + Build.VERSION.SDK_INT);
environment.add("HYBRIS_VULKAN_HAL=" + HAL);
environment.add("HYBRIS_LINKER_DIR=" + new File(directory, "lib/libhybris/linker").getPath());
environment.add("HYBRIS_LD_LIBRARY_PATH=/vendor/lib64/egl:/vendor/lib64/hw:/vendor/lib64:/system/lib64:/system_ext/lib64");
environment.add("VK_LAYER_PATH=" + new File(directory, "lib").getPath());
environment.add("VK_INSTANCE_LAYERS=VK_LAYER_HYBRIS_compat");
// The Android driver has a render node, but no KMS primary node. SDL presents
// through the wrapper's Wayland Vulkan swapchain instead of a KMS-style path.
environment.add("BL_GAMESCOPE_BACKEND=sdl");
// The Android pvr kernel accepts Vulkan DMA-BUF imports but rejects PRIME GEM handles.
environment.add("BL_POWERVR_DRM_HANDLES=1");
// Inner Gamescope/Xwayland lacks the bridge's Android-buffer WSI protocols.
environment.add("BL_STEAM_GL_PRESENT=1");
// Only actual Proton game launches use the bundled DXVK and X11 WSI layer.
environment.add("DROIDDECK_POWERVR_RUNTIME=" + directory.getPath());
}
}
@@ -25,9 +25,10 @@ import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* The Vulkan driver the in-app compositor runs on. It has to be Turnip: the system Adreno driver
* The Vulkan driver the in-app compositor runs on. Adreno uses Turnip: the system Adreno driver
* does not implement VK_EXT_image_drm_format_modifier, so importing the dma-bufs gamescope hands
* over fails and the session renders nothing. Two builds ship in the apk, one per Adreno
* over fails and the session renders nothing. PowerVR uses the system driver and Android hardware
* buffers instead. Two Turnip builds ship in the apk, one per Adreno
* generation, and the GPU decides which is unpacked - unless the user has imported an AdrenoTools
* zip of their own and chosen it, which then wins.
*
@@ -313,6 +314,7 @@ public final class TurnipDriver {
* and falls through, so a stale preference never blocks a session.
*/
private String choose() {
SessionPrefs.ensurePowerVrAutoDrivers(context);
String chosen = SessionPrefs.androidDriver(context);
if (chosen != null && !chosen.isEmpty()) {
if (BUNDLED.contains(chosen)) {
@@ -338,6 +340,7 @@ public final class TurnipDriver {
if (forced.startsWith("a8")) return DRIVER_A8XX;
if (forced.startsWith("a7")) return DRIVER_A7XX;
}
if (SystemVulkanDriver.isPowerVr()) return null;
String model = gpuModel();
Log.i(TAG, "gpu model: " + (model == null ? "unknown" : model));
if (model != null) {
@@ -200,6 +200,13 @@ public final class LinuxRuntime {
bind(cmd, "/dev");
bind(cmd, "/proc");
bind(cmd, "/sys");
if (com.droiddeck.launcher.gpu.SystemVulkanDriver.isPowerVr()) {
// The glibc bridge loads the device's existing Android HAL and its dependencies.
// These read-only Android partitions remain subject to the app's normal permissions.
for (String path : new String[]{"/system", "/vendor", "/apex", "/system_ext", "/product", "/odm"})
if (new File(path).isDirectory()) bind(cmd, path);
if (new File("/linkerconfig/ld.config.txt").isFile()) bind(cmd, "/linkerconfig/ld.config.txt");
}
bind(cmd, "/dev/urandom:/dev/random");
bind(cmd, "/proc/self/fd:/dev/fd");
bind(cmd, "/proc/self/fd/0:/dev/stdin");
@@ -277,7 +284,11 @@ public final class LinuxRuntime {
bind(cmd, fake.getPath() + ":" + entry[1]);
}
}
bindGpuNode(context, cmd);
if (com.droiddeck.launcher.gpu.SystemVulkanDriver.isPowerVr()) {
bindPowerVrGpuNode(context, cmd);
} else {
bindGpuNode(context, cmd);
}
bindAdrenoStats(cmd, extraBinds);
bindCpuTemps(cmd, root);
if (extraBinds != null) {
@@ -288,6 +299,28 @@ public final class LinuxRuntime {
return specs;
}
/**
* Tensor's render node is accessible directly, but Android denies listing its parent
* directory. libdrm enumerates that directory even when given the device number. Expose
* the real node through a listable private directory; retain its real sysfs identity.
*/
private static void bindPowerVrGpuNode(Context context, List<String> cmd) {
if (new File("/dev/dri").list() != null) return;
String node = "/dev/dri/renderD128";
File dri = new File(context.getCacheDir(), "powervr-drm/dri");
try {
if (!SystemVulkanDriver.renderNodeAvailable()) return;
if (!dri.isDirectory() && !dri.mkdirs()) return;
new File(dri, "renderD128").createNewFile();
} catch (IOException e) {
android.util.Log.w("LinuxRuntime", "Cannot expose the PowerVR render node", e);
return;
}
bind(cmd, dri.getPath() + ":/dev/dri");
bind(cmd, node + ":" + node);
android.util.Log.i("LinuxRuntime", "PowerVR: exposing real renderD128 through a listable directory");
}
/**
* An app process may not open {@code /dev/dri} - the nodes exist but are labelled
* {@code graphics_device}, which stock policy grants surfaceflinger and not us - yet libdrm and
@@ -6,6 +6,8 @@ import androidx.annotation.StringRes
import com.droiddeck.launcher.R
import com.droiddeck.launcher.core.TextureFiltering
import com.droiddeck.launcher.gpu.ScreenEffects
import com.droiddeck.launcher.gpu.SystemVulkanDriver
import com.droiddeck.launcher.gpu.TurnipDriver
import org.json.JSONObject
/** The in-session switches: the HUD and how the on-screen controls decide to appear. */
@@ -220,6 +222,15 @@ object SessionPrefs {
p.edit().putString("gpuDriverMode", if (chosen) GPU_DRIVERS_MANUAL else GPU_DRIVERS_AUTO).apply()
}
/** Also called by direct session launches, which may never open the driver settings. */
@JvmStatic
fun ensurePowerVrAutoDrivers(context: Context) = ensurePowerVrAutoDrivers(context, SystemVulkanDriver.isPowerVr())
internal fun ensurePowerVrAutoDrivers(context: Context, powerVr: Boolean) {
if (!powerVr || gpuDriverMode(context) != GPU_DRIVERS_AUTO) return
prefs(context).edit().putString("linuxDriver", "").putString("androidDriver", TurnipDriver.AUTO).apply()
}
/** Drivers Auto downloaded: the only ones it removes when a newer pair replaces them. */
fun gpuAutoInstalled(context: Context): Set<String> =
prefs(context).getStringSet("gpuAutoInstalled", emptySet()).orEmpty()
@@ -2,6 +2,7 @@ package com.droiddeck.launcher.session
import com.droiddeck.launcher.gpu.LinuxVulkanDriver
import com.droiddeck.launcher.gpu.LinuxVulkanDriverManager
import com.droiddeck.launcher.gpu.SystemVulkanDriver
import android.app.Notification
import android.app.NotificationChannel
@@ -318,6 +319,7 @@ class SessionService : Service() {
// ── The session ─────────────────────────────────────────────────────────────────────────
private fun runSession(gen: Int) {
SessionPrefs.ensurePowerVrAutoDrivers(this)
SessionTerminal.clear()
try {
LinuxRuntime.writeAccounts(this)
@@ -331,7 +333,16 @@ class SessionService : Service() {
val sessionRoot = LinuxRuntime.sessionRoot(this).apply { mkdirs() }
val runtimeDir = File(filesDir, ".wayland-rt").apply { mkdirs() }
killStragglers()
SessionFiles.stage(this, root)
try {
if (SystemVulkanDriver.usesDefault(SessionPrefs.linuxDriver(this))) {
SystemVulkanDriver.prepare(this)
}
SessionFiles.stage(this, root)
} catch (error: Exception) {
Log.e(TAG, "Could not prepare the session graphics runtime", error)
stopSession(-1)
return
}
com.droiddeck.launcher.agent.AgentGuest.reset(this)
// The compat tool asks for an Epic game's sign-in code at every launch, the client's Play button included.
com.droiddeck.launcher.stores.StoreLaunchRequests.start(this)
@@ -609,6 +620,9 @@ class SessionService : Service() {
// import falls back to. One driver for every session: the driver's shader cache is keyed on
// its build, and with one per mode every emulator compiled its shaders twice.
val linuxDriverId = SessionPrefs.linuxDriver(this)
if (SystemVulkanDriver.usesDefault(linuxDriverId)) {
SystemVulkanDriver.addEnvironment(this, guest)
}
LinuxVulkanDriver.resolveIcdPath(this, linuxDriverId)
?.let { guest.add(LinuxVulkanDriver.ENV + "=" + it) }
// Turnip's own debug switches, for the runtime's driver and everything on it. The file in
+3 -3
View File
@@ -1110,10 +1110,10 @@
<string name="main_wadb_unavailable">La Depuración inalámbrica no está disponible</string>
<string name="main_adb_copied">Comando ADB copiado</string>
<string name="main_pick_roms">Elige la carpeta de ROMs</string>
<string name="main_non_adreno_title">La GPU no es Adreno</string>
<string name="main_non_adreno_title">GPU no compatible</string>
<!-- %1$s is the GPU's name, %2$.0f the download size in MB -->
<string name="main_non_adreno_text">Turnip es compatible con GPU Adreno. En %1$s, Steam puede mostrar una pantalla negra. Descarga: %2$.0f MB.</string>
<string name="main_non_adreno_text">No hay un motor gráfico compatible disponible para %1$s. Steam puede mostrar una pantalla negra. Descarga: %2$.0f MB.</string>
<string name="main_install_anyway">Instalar de todos modos</string>
<string name="main_remove_runtime_title">Eliminar el runtime de Linux</string>
<string name="main_remove_runtime_text">Esto elimina el runtime, el cliente de Steam que contiene y todos los juegos instalados en él.</string>
@@ -1937,7 +1937,7 @@
<string name="gpuinfo_supported">Compatible</string>
<string name="gpuinfo_experimental">Experimental: sus controladores son versiones de prueba</string>
<string name="gpuinfo_below_tested">Por debajo del hardware probado (Adreno 725 y posteriores): puede que no funcione</string>
<string name="gpuinfo_unsupported">No compatible: DroidDeck necesita una GPU Adreno (Snapdragon)</string>
<string name="gpuinfo_unsupported">No compatible: no hay un motor gráfico compatible disponible para esta GPU</string>
<!-- Driver downloads and imports -->
<string name="turnip_rate_limited">Se alcanzó el límite de solicitudes de GitHub - inténtalo más tarde</string>
+3 -3
View File
@@ -1092,10 +1092,10 @@
<string name="main_wadb_unavailable">ワイヤレスデバッグは利用できません</string>
<string name="main_adb_copied">ADBコマンドをコピーしました</string>
<string name="main_pick_roms">ROMフォルダを選択</string>
<string name="main_non_adreno_title">Adreno GPUではありません</string>
<string name="main_non_adreno_title">サポート対象外のGPU</string>
<!-- %1$s is the GPU's name, %2$.0f the download size in MB -->
<string name="main_non_adreno_text">TurnipはAdreno GPUをサポートしています。%1$sでは、Steamの画面が真っ暗になる可能性があります。ダウンロード:%2$.0f MB。</string>
<string name="main_non_adreno_text">%1$sで利用できる互換グラフィックスバックエンドがありません。Steamの画面が真っ暗になる可能性があります。ダウンロード:%2$.0f MB。</string>
<string name="main_install_anyway">インストールする</string>
<string name="main_remove_runtime_title">Linux実行環境を削除</string>
<string name="main_remove_runtime_text">実行環境、その中のSteamクライアント、そこにインストールされたすべてのゲームが削除されます。</string>
@@ -1897,7 +1897,7 @@
<string name="gpuinfo_supported">サポート対象</string>
<string name="gpuinfo_experimental">実験的:ドライバーはテストビルドです</string>
<string name="gpuinfo_below_tested">テスト済みハードウェア(Adreno 725以降)未満:動作しない可能性があります</string>
<string name="gpuinfo_unsupported">サポート対象外:DroidDeckにはAdreno(Snapdragon)GPUが必要です</string>
<string name="gpuinfo_unsupported">サポート対象外:このGPUで利用できる互換グラフィックスバックエンドがありません</string>
<!-- Driver downloads and imports -->
<string name="turnip_rate_limited">GitHubのレート制限に達しました。後でもう一度お試しください</string>
+3 -3
View File
@@ -1092,10 +1092,10 @@
<string name="main_wadb_unavailable">무선 디버깅을 사용할 수 없습니다</string>
<string name="main_adb_copied">ADB 명령이 복사되었습니다</string>
<string name="main_pick_roms">ROM 폴더 선택</string>
<string name="main_non_adreno_title">Adreno GPU가 아님</string>
<string name="main_non_adreno_title">지원되지 않는 GPU</string>
<!-- %1$s is the GPU's name, %2$.0f the download size in MB -->
<string name="main_non_adreno_text">Turnip은 Adreno GPU를 지원합니다. %1$s에서는 Steam 화면이 검게 표시될 수 있습니다. 다운로드: %2$.0f MB.</string>
<string name="main_non_adreno_text">%1$s에서 사용할 수 있는 호환 그래픽 백엔드가 없습니다. Steam 화면이 검게 표시될 수 있습니다. 다운로드: %2$.0f MB.</string>
<string name="main_install_anyway">그래도 설치</string>
<string name="main_remove_runtime_title">Linux 런타임 제거</string>
<string name="main_remove_runtime_text">런타임과 그 안의 Steam 클라이언트, 그리고 런타임에 설치된 모든 게임이 삭제됩니다.</string>
@@ -1897,7 +1897,7 @@
<string name="gpuinfo_supported">지원됨</string>
<string name="gpuinfo_experimental">실험적: 드라이버가 테스트 빌드입니다</string>
<string name="gpuinfo_below_tested">테스트된 하드웨어(Adreno 725 이상)보다 낮음: 실행되지 않을 수 있습니다</string>
<string name="gpuinfo_unsupported">지원되지 않음: DroidDeck에는 Adreno(Snapdragon) GPU가 필요합니다</string>
<string name="gpuinfo_unsupported">지원되지 않음: 이 GPU에서 사용할 수 있는 호환 그래픽 백엔드가 없습니다</string>
<!-- Driver downloads and imports -->
<string name="turnip_rate_limited">GitHub 요청 한도에 도달했습니다. 나중에 다시 시도하세요</string>
+3 -3
View File
@@ -1092,10 +1092,10 @@
<string name="main_wadb_unavailable">无线调试不可用</string>
<string name="main_adb_copied">已复制 ADB 命令</string>
<string name="main_pick_roms">选择 ROM 文件夹</string>
<string name="main_non_adreno_title">不是 Adreno GPU</string>
<string name="main_non_adreno_title">不受支持的 GPU</string>
<!-- %1$s is the GPU's name, %2$.0f the download size in MB -->
<string name="main_non_adreno_text">Turnip 支持 Adreno GPU。在 %1$s 上,Steam 可能显示黑屏。需下载:%2$.0f MB。</string>
<string name="main_non_adreno_text">没有适用于 %1$s 的兼容图形后端。Steam 可能显示黑屏。需下载:%2$.0f MB。</string>
<string name="main_install_anyway">仍要安装</string>
<string name="main_remove_runtime_title">移除 Linux 运行时</string>
<string name="main_remove_runtime_text">这会删除运行时、其中的 Steam 客户端,以及安装在其中的所有游戏。</string>
@@ -1897,7 +1897,7 @@
<string name="gpuinfo_supported">受支持</string>
<string name="gpuinfo_experimental">实验性:其驱动为测试构建</string>
<string name="gpuinfo_below_tested">低于已测试的硬件(Adreno 725 及更新):可能无法运行</string>
<string name="gpuinfo_unsupported">不受支持:DroidDeck 需要 Adreno(骁龙)GPU</string>
<string name="gpuinfo_unsupported">不受支持:没有适用于此 GPU 的兼容图形后端</string>
<!-- Driver downloads and imports -->
<string name="turnip_rate_limited">已达到 GitHub 的请求频率限制 — 请稍后重试</string>
+3 -3
View File
@@ -1092,10 +1092,10 @@
<string name="main_wadb_unavailable">無法使用無線偵錯</string>
<string name="main_adb_copied">已複製 ADB 指令</string>
<string name="main_pick_roms">選擇 ROM 資料夾</string>
<string name="main_non_adreno_title">不是 Adreno GPU</string>
<string name="main_non_adreno_title">不受支援的 GPU</string>
<!-- %1$s is the GPU's name, %2$.0f the download size in MB -->
<string name="main_non_adreno_text">Turnip 支援 Adreno GPU。在 %1$s 上,Steam 可能會出現黑畫面。下載大小:%2$.0f MB。</string>
<string name="main_non_adreno_text">沒有適用於 %1$s 的兼容圖形後端。Steam 可能會出現黑畫面。下載大小:%2$.0f MB。</string>
<string name="main_install_anyway">仍要安裝</string>
<string name="main_remove_runtime_title">移除 Linux 運行環境</string>
<string name="main_remove_runtime_text">此操作會刪除運行環境、其中的 Steam 用戶端,以及安裝在其中的所有遊戲。</string>
@@ -1897,7 +1897,7 @@
<string name="gpuinfo_supported">受支援</string>
<string name="gpuinfo_experimental">實驗性:其驅動程式為測試版本</string>
<string name="gpuinfo_below_tested">低於已測試的硬件(Adreno 725 或更新):可能無法執行</string>
<string name="gpuinfo_unsupported">不受支援:DroidDeck 需要 Adreno (Snapdragon) GPU</string>
<string name="gpuinfo_unsupported">不受支援:沒有適用於此 GPU 的兼容圖形後端</string>
<!-- Driver downloads and imports -->
<string name="turnip_rate_limited">已達 GitHub 的速率限制 - 請稍後再試</string>
+3 -3
View File
@@ -1092,10 +1092,10 @@
<string name="main_wadb_unavailable">無法使用無線偵錯</string>
<string name="main_adb_copied">已複製 ADB 指令</string>
<string name="main_pick_roms">選擇 ROM 資料夾</string>
<string name="main_non_adreno_title">不是 Adreno GPU</string>
<string name="main_non_adreno_title">不受支援的 GPU</string>
<!-- %1$s is the GPU's name, %2$.0f the download size in MB -->
<string name="main_non_adreno_text">Turnip 支援 Adreno GPU。在 %1$s 上,Steam 可能會出現黑畫面。下載大小:%2$.0f MB。</string>
<string name="main_non_adreno_text">沒有適用於 %1$s 的相容圖形後端。Steam 可能會出現黑畫面。下載大小:%2$.0f MB。</string>
<string name="main_install_anyway">仍要安裝</string>
<string name="main_remove_runtime_title">移除 Linux 執行環境</string>
<string name="main_remove_runtime_text">這會刪除執行環境、其中的 Steam 用戶端,以及安裝在其中的所有遊戲。</string>
@@ -1897,7 +1897,7 @@
<string name="gpuinfo_supported">受支援</string>
<string name="gpuinfo_experimental">實驗性:其驅動程式為測試版本</string>
<string name="gpuinfo_below_tested">低於已測試的硬體(Adreno 725 及更新機型):可能無法執行</string>
<string name="gpuinfo_unsupported">不受支援:DroidDeck 需要 Adreno(Snapdragon)GPU</string>
<string name="gpuinfo_unsupported">不受支援:沒有適用於此 GPU 的相容圖形後端</string>
<!-- Driver downloads and imports -->
<string name="turnip_rate_limited">已達 GitHub 速率限制 - 請稍後再試</string>
+9 -3
View File
@@ -1128,9 +1128,9 @@
<string name="main_wadb_unavailable">Wireless debugging is unavailable</string>
<string name="main_adb_copied">ADB command copied</string>
<string name="main_pick_roms">Choose the ROMs folder</string>
<string name="main_non_adreno_title">Not an Adreno GPU</string>
<string name="main_non_adreno_title">Unsupported GPU</string>
<!-- %1$s is the GPU's name, %2$.0f the download size in MB -->
<string name="main_non_adreno_text">Turnip supports Adreno GPUs. On %1$s, Steam may show a black screen. Download: %2$.0f MB.</string>
<string name="main_non_adreno_text">No compatible graphics backend is available for %1$s. Steam may show a black screen. Download: %2$.0f MB.</string>
<string name="main_install_anyway">Install anyway</string>
<string name="main_remove_runtime_title">Remove Linux runtime</string>
<string name="main_remove_runtime_text">This deletes the runtime, the Steam client inside it, and every game installed there.</string>
@@ -1846,10 +1846,16 @@
<!-- GPU support line -->
<string name="gpuinfo_not_adreno">Not an Adreno GPU</string>
<string name="gpuinfo_powervr">System PowerVR driver: compatibility varies by device</string>
<string name="gpu_system_powervr">System PowerVR</string>
<string name="gpu_auto_system_hint">Uses the built-in drivers for this GPU</string>
<string name="drivers_powervr_builtin">The system PowerVR driver is selected for the runtime and display.</string>
<string name="cmgr_powervr_dxvk_queued">Queued: %1$s (waiting for a different runtime driver)</string>
<string name="cmgr_powervr_dxvk_managed">DXVK is provided by the system PowerVR runtime. Other DXVK packages can be stored but are not applied with this driver.</string>
<string name="gpuinfo_supported">Supported</string>
<string name="gpuinfo_experimental">Experimental: its drivers are test builds</string>
<string name="gpuinfo_below_tested">Below tested hardware (Adreno 725 and newer): it may not run</string>
<string name="gpuinfo_unsupported">Not supported: DroidDeck needs an Adreno (Snapdragon) GPU</string>
<string name="gpuinfo_unsupported">Not supported: no compatible graphics backend is available for this GPU</string>
<!-- Driver downloads and imports -->
<string name="turnip_rate_limited">GitHub\'s rate limit was hit - try again later</string>
@@ -0,0 +1,14 @@
package com.droiddeck.launcher.core
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
class DeviceSupportTest {
@Test fun runtimeDownloadAcceptsOnlyRecognizedGraphicsBackends() {
assertTrue(DeviceSupport.graphicsAvailable(adreno = true, powerVrReady = false))
assertTrue(DeviceSupport.graphicsAvailable(adreno = false, powerVrReady = true))
// A PowerVR HAL without usable DRM access also reaches this unavailable case.
assertFalse(DeviceSupport.graphicsAvailable(adreno = false, powerVrReady = false))
}
}
@@ -42,6 +42,23 @@ class DriverPairsTest {
assertEquals(GpuInfo.Support.UNSUPPORTED, gpu(0, adreno = false).support)
}
@Test
fun powerVrUsesItsOwnFamilyWithoutClaimingAllModelsAreTested() {
assertEquals(Family.POWERVR, GpuInfo.familyOf(false, 0, powerVr = true))
assertEquals(Family.NOT_ADRENO, GpuInfo.familyOf(false, 0, powerVr = false))
assertEquals(Family.A7XX, GpuInfo.familyOf(true, 740, powerVr = true))
val powerVr = GpuInfo("PowerVR", 0, Family.POWERVR, "", false)
assertEquals(GpuInfo.Support.UNTESTED, powerVr.support)
assertNull("PowerVR never downloads an Adreno driver pair", DriverPairs.recommendedKey(powerVr, emptyList()))
val pairs = DriverPairs.from(TurnipReleases.Check(
listOf(asset("Turnip-r4.zip", "r4", false, DriverPairs.BANNER),
asset("Turnip-r4-Linux.zip", "r4", true, DriverPairs.BANNER)),
emptyList(), emptyList(), 0L,
))
assertFalse(pairs.single().suits(powerVr))
assertNull(DriverPairs.recommendedKey(powerVr, pairs))
}
@Test
fun autoPicksWinNativeBalancedOn8xxAndBannersElsewhere() {
assertEquals(DriverPairs.WN_BALANCED, DriverPairs.recommendedKey(gpu(830), emptyList()))
@@ -0,0 +1,57 @@
package com.droiddeck.launcher.gpu
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Assert.assertThrows
import org.junit.Test
class SystemVulkanSelectionTest {
@Test fun defaultPowerVrUsesTheSystemBridge() {
assertTrue(SystemVulkanDriver.usesDefault(true, ""))
assertTrue(SystemVulkanDriver.usesDefault(true, null))
}
@Test fun otherGpusAndExplicitImportsKeepTheirDriver() {
assertFalse(SystemVulkanDriver.usesDefault(false, ""))
assertFalse(SystemVulkanDriver.usesDefault(false, null))
assertFalse(SystemVulkanDriver.usesDefault(true, "custom-driver"))
}
@Test fun renderNodeRequiresARealCharacterDeviceAndBothAccessModes() {
assertTrue(SystemVulkanDriver.renderNodeAvailable(true, true, true))
assertFalse("a regular placeholder is not a DRM device", SystemVulkanDriver.renderNodeAvailable(false, true, true))
assertFalse("read access is required", SystemVulkanDriver.renderNodeAvailable(true, false, true))
assertFalse("write access is required", SystemVulkanDriver.renderNodeAvailable(true, true, false))
assertFalse("a missing node is unavailable", SystemVulkanDriver.renderNodeAvailable(false, false, false))
}
@Test fun defaultPowerVrResolvesToTheSystemIcd() {
assertEquals("system/icd.json", LinuxVulkanDriver.resolveIcdPath("", true,
{ "system/icd.json" }, { error("default must not resolve an import") }))
}
@Test fun defaultAdrenoLeavesTheRuntimeIcdUnchanged() {
assertNull(LinuxVulkanDriver.resolveIcdPath("", false,
{ error("Adreno must not prepare the system bridge") }, { error("default must not resolve an import") }))
}
@Test fun missingManualPowerVrImportFailsWithoutSwitchingBackends() {
val failure = assertThrows(IllegalStateException::class.java) {
LinuxVulkanDriver.resolveIcdPath("missing-driver", true,
{ error("manual preference must not silently switch to the system backend") }, { null })
}
assertTrue(failure.message!!.contains("missing-driver"))
assertTrue(failure.message!!.contains("use Auto"))
}
@Test fun missingAdrenoImportKeepsTheExistingRuntimeFallback() {
assertNull(LinuxVulkanDriver.resolveIcdPath("missing-driver", false,
{ error("Adreno must not prepare the system bridge") }, { null }))
}
@Test fun manualPowerVrResolvesTheChosenImport() {
assertEquals("imports/chosen/icd.json", LinuxVulkanDriver.resolveIcdPath("chosen", true,
{ error("manual must not prepare the system bridge") }, { "imports/$it/icd.json" }))
}
}
@@ -0,0 +1,44 @@
package com.droiddeck.launcher.session
import android.content.Context
import org.robolectric.RuntimeEnvironment
import com.droiddeck.launcher.gpu.TurnipDriver
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.annotation.Config
@RunWith(RobolectricTestRunner::class)
@Config(sdk = [28], manifest = Config.NONE)
class PowerVrAutoDriversTest {
private val context: Context = RuntimeEnvironment.getApplication()
@Before fun reset() {
context.getSharedPreferences("session", Context.MODE_PRIVATE).edit().clear().commit()
SessionPrefs.setLinuxDriver(context, "imported-linux")
SessionPrefs.setAndroidDriver(context, "imported-display")
}
@Test fun autoRepairsStaleAdrenoSelectionsOnPowerVr() {
SessionPrefs.setGpuDriverMode(context, SessionPrefs.GPU_DRIVERS_AUTO)
SessionPrefs.ensurePowerVrAutoDrivers(context, powerVr = true)
assertEquals("", SessionPrefs.linuxDriver(context))
assertEquals(TurnipDriver.AUTO, SessionPrefs.androidDriver(context))
}
@Test fun explicitManualChoicesRemainOnPowerVr() {
SessionPrefs.setGpuDriverMode(context, SessionPrefs.GPU_DRIVERS_MANUAL)
SessionPrefs.ensurePowerVrAutoDrivers(context, powerVr = true)
assertEquals("imported-linux", SessionPrefs.linuxDriver(context))
assertEquals("imported-display", SessionPrefs.androidDriver(context))
}
@Test fun adrenoAutoChoicesRemain() {
SessionPrefs.setGpuDriverMode(context, SessionPrefs.GPU_DRIVERS_AUTO)
SessionPrefs.ensurePowerVrAutoDrivers(context, powerVr = false)
assertEquals("imported-linux", SessionPrefs.linuxDriver(context))
assertEquals("imported-display", SessionPrefs.androidDriver(context))
}
}
+106 -27
View File
@@ -1,9 +1,10 @@
/*
* GEM handle emulation for the KGSL device presented as a DRM render node.
* GEM handle emulation for KGSL and explicitly enabled system-PowerVR sessions.
*
* Compositors and Mesa validate or convert dma-bufs with PRIME ioctls on the render node; KGSL
* has no GEM. Handles only serve as tokens to these callers, so a table of duplicated dma-buf
* descriptors stands in for the kernel's, and every other device is left to libdrm.
* descriptors stands in for the kernel's. PowerVR's Android driver can import DMA-BUFs
* through Vulkan but rejects PRIME-to-GEM conversion; its real handles still go to libdrm.
*/
#define _GNU_SOURCE
#include <dlfcn.h>
@@ -11,6 +12,7 @@
#include <fcntl.h>
#include <pthread.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
@@ -18,6 +20,10 @@
#include <unistd.h>
#define MAX_HANDLES 4096
#ifndef F_DUPFD_QUERY
#define F_DUPFD_QUERY 1027
#endif
#define PVR_HANDLE_BIT UINT32_C(0x80000000)
/* DRM_IOCTL_GEM_CLOSE from drm.h: _IOW('d', 0x09, struct drm_gem_close { u32 handle, pad; }). */
struct gem_close {
@@ -30,11 +36,14 @@ struct handle_entry {
uint32_t handle;
int dma_fd;
int drm_fd;
int owned_drm_fd; /* PowerVR: pin the open file description, not its reusable fd number. */
int powervr;
};
static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
static struct handle_entry handle_entries[MAX_HANDLES];
static uint32_t next_handle = 1;
static uint32_t next_pvr_handle = PVR_HANDLE_BIT;
static dev_t kgsl_dev;
static int kgsl_state; /* 0 unknown, 1 known, -1 absent */
@@ -43,12 +52,38 @@ static int is_kgsl(int fd) {
pthread_mutex_lock(&lock);
if (kgsl_state == 0) {
kgsl_state = stat("/dev/kgsl-3d0", &st) == 0 ? 1 : -1;
kgsl_dev = st.st_rdev;
if (kgsl_state > 0) kgsl_dev = st.st_rdev;
}
pthread_mutex_unlock(&lock);
return kgsl_state > 0 && fstat(fd, &st) == 0 && S_ISCHR(st.st_mode) && st.st_rdev == kgsl_dev;
}
/* Keep the opt-in separate from GPU detection: manual ICDs must retain native DRM behavior. */
struct bl_drm_version {
int major, minor, patch;
size_t name_len;
char *name;
size_t date_len;
char *date;
size_t desc_len;
char *desc;
};
#define BL_DRM_VERSION _IOWR('d', 0x00, struct bl_drm_version)
static int is_powervr(int fd) {
const char *enabled = getenv("BL_POWERVR_DRM_HANDLES");
if (!enabled || strcmp(enabled, "1")) return 0;
struct stat node, candidate;
if (stat("/dev/dri/renderD128", &node) || fstat(fd, &candidate) ||
!S_ISCHR(candidate.st_mode) || !S_ISCHR(node.st_mode) ||
candidate.st_rdev != node.st_rdev) return 0;
char name[16] = {0};
struct bl_drm_version version = {.name_len = sizeof(name) - 1, .name = name};
/* Bypass the preload's ioctl dispatcher to avoid recursively entering this hook. */
return syscall(SYS_ioctl, fd, BL_DRM_VERSION, &version) == 0 &&
version.name_len == 3 && !memcmp(name, "pvr", 3);
}
static void *real(const char *name) {
void *fn = dlsym(RTLD_NEXT, name);
if (!fn) {
@@ -71,10 +106,20 @@ static int same_file_description(int a, int b) {
#endif
}
static int same_owned_description(int owned, int fd) {
int saved_errno = errno;
int result = fcntl(owned, F_DUPFD_QUERY, fd);
errno = saved_errno;
if (result >= 0) return result == 1;
return same_file_description(owned, fd);
}
static int find_handle_locked(int fd, uint32_t handle) {
for (int i = 0; i < MAX_HANDLES; i++) {
if (handle_entries[i].handle == handle &&
same_file_description(handle_entries[i].drm_fd, fd)) {
(handle_entries[i].powervr
? same_owned_description(handle_entries[i].owned_drm_fd, fd)
: same_file_description(handle_entries[i].drm_fd, fd))) {
return i;
}
}
@@ -86,11 +131,15 @@ static int find_handle_locked(int fd, uint32_t handle) {
* rapidly recreated swapchains immediately reused those numbers. Keep storage slots reusable but
* make the public token monotonic so a stale handle can fail instead of aliasing a newer buffer.
*/
static uint32_t alloc_handle_locked(void) {
static uint32_t alloc_handle_locked(int powervr) {
for (int tries = 0; tries <= MAX_HANDLES; tries++) {
uint32_t candidate = next_handle++;
if (next_handle == 0) {
next_handle = 1;
uint32_t candidate;
if (powervr) {
candidate = next_pvr_handle++;
if (!(next_pvr_handle & PVR_HANDLE_BIT)) next_pvr_handle = PVR_HANDLE_BIT;
} else {
candidate = next_handle++;
if (next_handle == PVR_HANDLE_BIT) next_handle = 1;
}
if (candidate == 0) {
continue;
@@ -110,25 +159,45 @@ static uint32_t alloc_handle_locked(void) {
}
int drmPrimeFDToHandle(int fd, int prime_fd, uint32_t *handle) {
if (!is_kgsl(fd)) {
int kgsl = is_kgsl(fd);
int powervr = !kgsl && is_powervr(fd);
if (!kgsl) {
int (*fn)(int, int, uint32_t *) = (int (*)(int, int, uint32_t *)) real("drmPrimeFDToHandle");
return fn ? fn(fd, prime_fd, handle) : -ENOSYS;
if (!fn) return -ENOSYS;
int result = fn(fd, prime_fd, handle);
if (!result || !powervr || errno != EINVAL) return result;
}
if (!handle) { errno = EINVAL; return -EINVAL; }
int owner = -1;
if (powervr) {
owner = fcntl(fd, F_DUPFD_CLOEXEC, 0);
if (owner < 0) return -errno;
if (!same_owned_description(owner, fd)) {
close(owner);
errno = EOPNOTSUPP;
return -EOPNOTSUPP;
}
}
int dup_fd = fcntl(prime_fd, F_DUPFD_CLOEXEC, 0);
if (dup_fd < 0) {
return -errno;
int error = errno;
if (owner >= 0) close(owner);
errno = error;
return -error;
}
int ret = -ENOMEM;
pthread_mutex_lock(&lock);
for (int i = 0; i < MAX_HANDLES; i++) {
if (handle_entries[i].handle == 0) {
uint32_t token = alloc_handle_locked();
uint32_t token = alloc_handle_locked(powervr);
if (!token) {
break;
}
handle_entries[i].handle = token;
handle_entries[i].dma_fd = dup_fd;
handle_entries[i].drm_fd = fd;
handle_entries[i].owned_drm_fd = owner;
handle_entries[i].powervr = powervr;
*handle = token;
ret = 0;
break;
@@ -137,33 +206,42 @@ int drmPrimeFDToHandle(int fd, int prime_fd, uint32_t *handle) {
pthread_mutex_unlock(&lock);
if (ret) {
close(dup_fd);
if (owner >= 0) close(owner);
errno = -ret;
}
return ret;
}
int drmPrimeHandleToFD(int fd, uint32_t handle, uint32_t flags, int *prime_fd) {
if (!is_kgsl(fd)) {
int (*fn)(int, uint32_t, uint32_t, int *) = (int (*)(int, uint32_t, uint32_t, int *)) real("drmPrimeHandleToFD");
return fn ? fn(fd, handle, flags, prime_fd) : -ENOSYS;
}
int kgsl = is_kgsl(fd);
int ret = -EINVAL;
pthread_mutex_lock(&lock);
int index = find_handle_locked(fd, handle);
if (index >= 0) {
*prime_fd = fcntl(handle_entries[index].dma_fd, F_DUPFD_CLOEXEC, 0);
ret = *prime_fd < 0 ? -errno : 0;
if (!prime_fd || (handle_entries[index].powervr && (flags & ~(O_CLOEXEC | O_RDWR)))) {
errno = EINVAL;
} else {
int command = handle_entries[index].powervr && !(flags & O_CLOEXEC) ? F_DUPFD : F_DUPFD_CLOEXEC;
*prime_fd = fcntl(handle_entries[index].dma_fd, command, 0);
ret = *prime_fd < 0 ? -errno : 0;
}
}
pthread_mutex_unlock(&lock);
return ret;
if (index >= 0 || kgsl) return ret;
/* Unknown handles (including real PowerVR GEM handles) retain kernel semantics. */
int (*fn)(int, uint32_t, uint32_t, int *) =
(int (*)(int, uint32_t, uint32_t, int *)) real("drmPrimeHandleToFD");
return fn ? fn(fd, handle, flags, prime_fd) : -ENOSYS;
}
/* Drops only a handle belonging to this KGSL file description. */
/* Drops only an emulated handle belonging to this file description. */
static int release_handle(int fd, uint32_t handle) {
int found = 0;
pthread_mutex_lock(&lock);
int index = find_handle_locked(fd, handle);
if (index >= 0) {
close(handle_entries[index].dma_fd);
if (handle_entries[index].powervr) close(handle_entries[index].owned_drm_fd);
handle_entries[index] = (struct handle_entry){0};
found = 1;
}
@@ -172,11 +250,10 @@ static int release_handle(int fd, uint32_t handle) {
}
int drmCloseBufferHandle(int fd, uint32_t handle) {
if (!is_kgsl(fd)) {
int (*fn)(int, uint32_t) = (int (*)(int, uint32_t)) real("drmCloseBufferHandle");
return fn ? fn(fd, handle) : -ENOSYS;
}
return release_handle(fd, handle) ? 0 : -EINVAL;
if (release_handle(fd, handle)) return 0;
if (is_kgsl(fd)) { errno = EINVAL; return -EINVAL; }
int (*fn)(int, uint32_t) = (int (*)(int, uint32_t)) real("drmCloseBufferHandle");
return fn ? fn(fd, handle) : -ENOSYS;
}
/*
@@ -190,14 +267,16 @@ int drmCloseBufferHandle(int fd, uint32_t handle) {
* judder replaying old frames in No Man's Sky on an Adreno 840 even with no handle released.
*/
__attribute__((visibility("hidden"))) int bl_drm_gem_close(int fd, unsigned long request, void *arg, int *rc) {
if (request != GEM_CLOSE || !arg || !is_kgsl(fd)) {
if (request != GEM_CLOSE || !arg) {
return 0;
}
if (release_handle(fd, ((struct gem_close *) arg)->handle)) {
*rc = 0;
} else {
} else if (is_kgsl(fd)) {
errno = EINVAL;
*rc = -1;
} else {
return 0;
}
return 1;
}
+256
View File
@@ -0,0 +1,256 @@
#define _GNU_SOURCE
#include <assert.h>
#include <dlfcn.h>
#include <errno.h>
#include <fcntl.h>
#include <pthread.h>
#include <stdarg.h>
#include <stdatomic.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <unistd.h>
/* Only the device/driver boundary is simulated. Descriptors and their lifetimes are real.
* F_DUPFD_QUERY is modelled so this test also runs on older CI kernels; the device probe
* separately exercises the actual query on Android. */
#define TEST_FD_LIMIT 65536
static _Atomic unsigned identities[TEST_FD_LIMIT];
static unsigned identity_serial = 1;
static int identity_denied, kgsl_present;
static int kernel_error = EINVAL;
static int native_imports, native_exports, native_closes;
static const char *driver_name = "pvr";
static int fixture_stat(const char *path, struct stat *st);
static long fixture_syscall(long number, ...);
static int fixture_fcntl(int fd, int op, int arg);
static void *fixture_dlsym(void *object, const char *name);
static int fixture_close(int fd);
#define stat(path, st) fixture_stat(path, st)
#define syscall fixture_syscall
#define fcntl fixture_fcntl
#define dlsym fixture_dlsym
#define close fixture_close
#include "../linuxfs/preload/drm.c"
#undef stat
#undef syscall
#undef fcntl
#undef dlsym
#undef close
static int test_open(const char *path) {
int fd = open(path, O_RDWR | O_CLOEXEC);
assert(fd >= 0 && fd < TEST_FD_LIMIT);
identities[fd] = identity_serial++;
return fd;
}
static int fixture_close(int fd) {
if (fd >= 0 && fd < TEST_FD_LIMIT) identities[fd] = 0;
return close(fd);
}
static int fixture_stat(const char *path, struct stat *st) {
if (!strcmp(path, "/dev/kgsl-3d0")) {
if (kgsl_present) return stat("/dev/zero", st);
errno = ENOENT;
return -1;
}
if (!strcmp(path, "/dev/dri/renderD128")) return stat("/dev/null", st);
return stat(path, st);
}
/* The intercepted DRM calls all take an integer argument. Keep that contract
* explicit; native_import's argument-free F_GETFD uses libc after #undef above. */
static int fixture_fcntl(int fd, int op, int arg) {
if (op == F_DUPFD_QUERY) {
if (identity_denied) { errno = EINVAL; return -1; }
if (fd < 0 || arg < 0 || fd >= TEST_FD_LIMIT || arg >= TEST_FD_LIMIT ||
!identities[fd] || !identities[arg]) { errno = EBADF; return -1; }
return identities[fd] == identities[arg];
}
int ret = fcntl(fd, op, arg);
if (ret >= 0 && (op == F_DUPFD || op == F_DUPFD_CLOEXEC)) {
assert(ret < TEST_FD_LIMIT);
identities[ret] = identities[fd];
}
return ret;
}
static long fixture_syscall(long number, ...) {
va_list ap;
va_start(ap, number);
if (number == SYS_ioctl) {
(void)va_arg(ap, int);
unsigned long request = va_arg(ap, unsigned long);
struct bl_drm_version *v = va_arg(ap, void *);
assert(request == BL_DRM_VERSION);
snprintf(v->name, v->name_len, "%s", driver_name);
v->name_len = strlen(driver_name);
va_end(ap);
return 0;
}
va_end(ap);
assert(number == SYS_kcmp);
errno = EPERM;
return -1;
}
static int native_import(int fd, int dma, uint32_t *handle) {
(void)fd;
native_imports++;
if (fcntl(dma, F_GETFD) < 0) return -1;
if (!handle) { errno = EFAULT; return -1; }
if (kernel_error) { errno = kernel_error; return -1; }
*handle = 42;
return 0;
}
static int native_export(int fd, uint32_t handle, uint32_t flags, int *out) {
(void)flags;
native_exports++;
if (handle != 42 || !out) { errno = EINVAL; return -1; }
*out = fixture_fcntl(fd, F_DUPFD_CLOEXEC, 0);
return *out < 0 ? -1 : 0;
}
static int native_close(int fd, uint32_t handle) {
(void)fd;
native_closes++;
if (handle == 42) return 0;
errno = EINVAL;
return -1;
}
static void *fixture_dlsym(void *object, const char *name) {
assert(object == RTLD_NEXT);
if (!strcmp(name, "drmPrimeFDToHandle")) return native_import;
if (!strcmp(name, "drmPrimeHandleToFD")) return native_export;
if (!strcmp(name, "drmCloseBufferHandle")) return native_close;
abort();
}
static int count_fds(void) {
int count = 0;
for (int fd = 0; fd < TEST_FD_LIMIT; ++fd) count += identities[fd] != 0;
return count;
}
static void empty_table(void) {
for (int i = 0; i < MAX_HANDLES; ++i) assert(!handle_entries[i].handle);
}
int main(void) {
int drm = test_open("/dev/null"), dma = test_open("/dev/zero");
uint32_t handle = 0;
unsetenv("BL_POWERVR_DRM_HANDLES");
assert(drmPrimeFDToHandle(drm, dma, &handle) != 0);
setenv("BL_POWERVR_DRM_HANDLES", "yes", 1);
assert(drmPrimeFDToHandle(drm, dma, &handle) != 0);
setenv("BL_POWERVR_DRM_HANDLES", "1", 1);
driver_name = "other";
assert(drmPrimeFDToHandle(drm, dma, &handle) != 0);
driver_name = "pvr";
assert(drmPrimeFDToHandle(dma, dma, &handle) != 0); /* wrong character device */
kernel_error = EACCES;
assert(drmPrimeFDToHandle(drm, dma, &handle) != 0 && errno == EACCES);
kernel_error = EINVAL;
assert(drmPrimeFDToHandle(drm, -1, &handle) != 0 && errno == EBADF);
empty_table();
int baseline = count_fds();
identity_denied = 1;
assert(drmPrimeFDToHandle(drm, dma, &handle) == -EOPNOTSUPP);
assert(count_fds() == baseline);
identity_denied = 0;
assert(drmPrimeFDToHandle(drm, dma, &handle) == 0 && (handle & PVR_HANDLE_BIT));
assert(count_fds() == baseline + 2);
int alias = fixture_fcntl(drm, F_DUPFD_CLOEXEC, 0), out = -1;
int separate = test_open("/dev/null");
assert(drmPrimeHandleToFD(separate, handle, O_CLOEXEC | O_RDWR, &out) != 0);
struct gem_close close_request = {.handle = handle};
int rc = 123;
assert(bl_drm_gem_close(separate, GEM_CLOSE, &close_request, &rc) == 0 && rc == 123);
assert(drmPrimeHandleToFD(alias, handle, O_CLOEXEC | O_RDWR, &out) == 0);
assert(fcntl(out, F_GETFD) & FD_CLOEXEC);
struct stat src, dst;
assert(fstat(dma, &src) == 0 && fstat(out, &dst) == 0);
assert(src.st_dev == dst.st_dev && src.st_ino == dst.st_ino);
fixture_close(out);
assert(drmPrimeHandleToFD(alias, handle, 0, &out) == 0);
assert(!(fcntl(out, F_GETFD) & FD_CLOEXEC));
fixture_close(out);
assert(drmPrimeHandleToFD(alias, handle, 0x40, &out) == -EINVAL);
/* A closed-and-reused descriptor must not inherit the old open file's handles. */
int old_number = drm;
fixture_close(drm);
drm = test_open("/dev/null");
assert(drm == old_number);
assert(drmPrimeHandleToFD(drm, handle, O_CLOEXEC, &out) != 0);
assert(drmCloseBufferHandle(drm, handle) != 0);
assert(drmPrimeHandleToFD(alias, handle, O_CLOEXEC, &out) == 0);
fixture_close(out);
assert(bl_drm_gem_close(alias, GEM_CLOSE, &close_request, &rc) == 1 && rc == 0);
assert(drmPrimeHandleToFD(alias, handle, O_CLOEXEC, &out) != 0);
fixture_close(alias);
fixture_close(separate);
assert(count_fds() == baseline);
empty_table();
/* Real kernel handles and their direct GEM_CLOSE ioctls are never swallowed. */
kernel_error = 0;
assert(drmPrimeFDToHandle(drm, dma, &handle) == 0 && handle == 42);
empty_table();
assert(drmPrimeHandleToFD(drm, handle, O_CLOEXEC, &out) == 0);
fixture_close(out);
close_request.handle = handle;
assert(bl_drm_gem_close(drm, GEM_CLOSE, &close_request, &rc) == 0);
assert(drmCloseBufferHandle(drm, handle) == 0);
assert(native_imports && native_exports && native_closes);
kernel_error = EINVAL;
/* Slot reuse must not reuse public tokens or leak either owned descriptor. */
uint32_t previous = 0;
for (int n = 0; n < 1000; ++n) {
assert(drmPrimeFDToHandle(drm, dma, &handle) == 0 && handle != previous);
assert(drmCloseBufferHandle(drm, handle) == 0);
if (previous) assert(drmPrimeHandleToFD(drm, previous, O_CLOEXEC, &out) != 0);
previous = handle;
}
assert(count_fds() == baseline);
empty_table();
assert(drmPrimeFDToHandle(drm, dma, &handle) == 0);
pid_t child = fork();
assert(child >= 0);
if (child == 0) {
assert(drmPrimeHandleToFD(drm, handle, O_CLOEXEC, &out) == 0);
fixture_close(out);
assert(drmCloseBufferHandle(drm, handle) == 0);
_exit(0);
}
int status;
assert(waitpid(child, &status, 0) == child && WIFEXITED(status) && WEXITSTATUS(status) == 0);
assert(drmPrimeHandleToFD(drm, handle, O_CLOEXEC, &out) == 0);
fixture_close(out);
assert(drmCloseBufferHandle(drm, handle) == 0);
/* Retain the old KGSL path with the PowerVR opt-in unset. */
unsetenv("BL_POWERVR_DRM_HANDLES");
kgsl_present = 1;
kgsl_state = 0;
assert(drmPrimeFDToHandle(dma, drm, &handle) == 0 && !(handle & PVR_HANDLE_BIT));
assert(drmPrimeHandleToFD(dma, handle, O_CLOEXEC | O_RDWR, &out) == 0);
fixture_close(out);
close_request.handle = handle;
assert(bl_drm_gem_close(dma, GEM_CLOSE, &close_request, &rc) == 1 && rc == 0);
assert(drmCloseBufferHandle(dma, handle) == -EINVAL);
empty_table();
assert(count_fds() == baseline);
fixture_close(drm);
fixture_close(dma);
puts("DRM adapter scoping, ownership, reuse, fork and kernel-passthrough tests passed");
}
+26
View File
@@ -0,0 +1,26 @@
import os
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
class DrmHandleTest(unittest.TestCase):
def test_handle_lifetime_and_device_scope(self):
compiler = shutil.which(os.environ.get("CC", "cc"))
if not compiler:
self.fail("A C compiler is required for the DRM adapter regression test")
source = Path(__file__).with_name("drm_handles_test.c")
with tempfile.TemporaryDirectory(prefix="droiddeck-drm-") as tmp:
program = Path(tmp) / "drm-handles"
built = subprocess.run([compiler, "-std=gnu11", "-Wall", "-Wextra", "-Werror",
"-UNDEBUG", "-pthread", str(source), "-ldl", "-o", str(program)],
capture_output=True, text=True)
self.assertEqual(built.returncode, 0, built.stdout + built.stderr)
ran = subprocess.run([str(program)], capture_output=True, text=True, timeout=30)
self.assertEqual(ran.returncode, 0, ran.stdout + ran.stderr)
if __name__ == "__main__":
unittest.main()