mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
Detect and select the system Vulkan backend on PowerVR
This commit is contained in:
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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user