diff --git a/app/src/dev/AndroidManifest.xml b/app/src/dev/AndroidManifest.xml index 5fdb55a..65b5aee 100644 --- a/app/src/dev/AndroidManifest.xml +++ b/app/src/dev/AndroidManifest.xml @@ -1,6 +1,10 @@ + openSystemDetails(rootfsName!!) + "open-desktop" -> selectDestination(UdroidDestination.DESKTOP) + "start-terminal" -> openRootfsTerminal(rootfsName!!) + "stop-terminal" -> RuntimeSupervisorService.stop(this) + "start-desktop" -> { + pendingDebugDesktop = command.copy(rootfsName = rootfsName) + openSystemDetails(rootfsName!!) + } + "stop-desktop" -> { + if (runtimeService == null) pendingDebugStopDesktop = true + else runtimeService?.stopDesktop() + } + } + } + + private fun launchPendingDebugDesktop(rootfsName: String) { + val command = pendingDebugDesktop?.takeIf { it.rootfsName == rootfsName } ?: return + pendingDebugDesktop = null + val requestedEnvironment = command.environmentId + if (requestedEnvironment != null) { + if (desktopEnvironments.none { it.id == requestedEnvironment }) { + debugCommandError("Desktop $requestedEnvironment is not installed in $rootfsName") + return + } + selectDesktopEnvironment(requestedEnvironment) + } + if (desktopEnvironments.isEmpty()) { + debugCommandError("No desktop session is installed in $rootfsName") + return + } + startSelectedDesktop() + selectDestination(UdroidDestination.DESKTOP) + } + + private fun debugCommandError(message: String) { + app.journal.append( + component = "debug", + severity = "error", + event = "adb_command_rejected", + message = message, + bootId = snapshot.bootId, + ) + } + private fun selectDesktopEnvironment(environmentId: String) { val rootfsName = selectedSystemRootfsName ?: return val environment = desktopEnvironments.firstOrNull { it.id == environmentId } ?: return diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/DesktopEnvironment.kt b/app/src/main/java/org/randomcoder/udroid/runtime/DesktopEnvironment.kt index 3f28d53..1a3d9b3 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/DesktopEnvironment.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/DesktopEnvironment.kt @@ -42,13 +42,18 @@ enum class DesktopGraphicsProfile( VIRGL("virgl"), VIRGL_ANGLE("virgl-angle"), VENUS_EXPERIMENTAL("venus-experimental"), + SPHAL_EXPERIMENTAL("sphal-experimental"), GFXSTREAM_EXPERIMENTAL("gfxstream-experimental"), ; companion object { - fun fromStorage(value: String?): DesktopGraphicsProfile = + fun fromStorage( + value: String?, + sphalEnabled: Boolean = false, + ): DesktopGraphicsProfile = entries.firstOrNull { it.storageValue == value && + (it != SPHAL_EXPERIMENTAL || sphalEnabled) && (it != GFXSTREAM_EXPERIMENTAL || GFXSTREAM_PROFILE_ENABLED) } ?: STANDARD } @@ -107,8 +112,9 @@ data class DesktopSessionSnapshot( ) class DesktopConfigurationStore(context: Context) { + private val context = context.applicationContext private val preferences = - context.applicationContext + this.context .getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) fun load( @@ -143,6 +149,7 @@ class DesktopConfigurationStore(context: Context) { graphicsProfile = DesktopGraphicsProfile.fromStorage( preferences.getString(key(rootfsName, KEY_GRAPHICS_PROFILE), null), + SphalGraphicsSupport.isEnabled(context), ), virglServerMode = VirglServerMode.fromStorage( @@ -186,6 +193,7 @@ class DesktopConfigurationStore(context: Context) { fun loadGraphicsProfile(rootfsName: String): DesktopGraphicsProfile = DesktopGraphicsProfile.fromStorage( preferences.getString(key(rootfsName, KEY_GRAPHICS_PROFILE), null), + SphalGraphicsSupport.isEnabled(context), ) fun loadVirglServerMode(rootfsName: String): VirglServerMode = diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/GraphicsProfileCompatibility.kt b/app/src/main/java/org/randomcoder/udroid/runtime/GraphicsProfileCompatibility.kt index f378dd8..48c4bc2 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/GraphicsProfileCompatibility.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/GraphicsProfileCompatibility.kt @@ -18,6 +18,7 @@ internal data class GraphicsDeviceSupport( val openGlEsVersion: Int, val vulkanVersion: Int, val hardwareBuffer: Boolean, + val vendorVulkanHal: Boolean, ) internal data class GraphicsGuestSupport( @@ -26,6 +27,7 @@ internal data class GraphicsGuestSupport( val vulkanIcd: Boolean, val virgl: Boolean, val venus: Boolean, + val sphal: Boolean, ) object GraphicsProfileCompatibilityProbe { @@ -75,6 +77,14 @@ object GraphicsProfileCompatibilityProbe { device.hardwareBuffer to "This device cannot allocate GPU hardware buffers", guest.venus to "A compatible Mesa Venus driver is not available in Linux", ), + DesktopGraphicsProfile.SPHAL_EXPERIMENTAL to + firstFailure( + device.arm64 to "SPHAL is packaged only for arm64 devices", + isVulkan11(device.vulkanVersion) to "SPHAL requires Android Vulkan 1.1", + device.hardwareBuffer to "This device cannot allocate GPU hardware buffers", + device.vendorVulkanHal to "No unambiguous Android vendor Vulkan driver was found", + guest.sphal to "Install the SPHAL guest runtime and patched Mesa in Linux", + ), DesktopGraphicsProfile.GFXSTREAM_EXPERIMENTAL to require(GFXSTREAM_PROFILE_ENABLED, "gfxstream is not enabled in this build"), ) @@ -105,6 +115,7 @@ object GraphicsProfileCompatibilityProbe { openGlEsVersion = openGlEsVersion, vulkanVersion = vulkanVersion, hardwareBuffer = hardwareBuffer, + vendorVulkanHal = SphalGraphicsSupport.findVulkanHal() != null, ) } @@ -147,6 +158,7 @@ object GraphicsProfileCompatibilityProbe { vulkanIcd = icds.isNotEmpty(), virgl = hasDriDriver("virtio_gpu_dri.so"), venus = VenusGuestRuntimeInstaller.requiresBundledMesa(rootfs) || systemVenus, + sphal = SphalGraphicsSupport.isGuestRuntimeAvailable(rootfs), ) } diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/ProotLaunchProfile.kt b/app/src/main/java/org/randomcoder/udroid/runtime/ProotLaunchProfile.kt index ac0531b..5c52336 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/ProotLaunchProfile.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/ProotLaunchProfile.kt @@ -34,6 +34,7 @@ internal class EnvironmentProotLaunchProfile private constructor( DesktopGraphicsProfile.VIRGL -> null DesktopGraphicsProfile.VIRGL_ANGLE -> null DesktopGraphicsProfile.VENUS_EXPERIMENTAL -> null + DesktopGraphicsProfile.SPHAL_EXPERIMENTAL -> null DesktopGraphicsProfile.GFXSTREAM_EXPERIMENTAL -> null } } diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt b/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt index c404399..f72be82 100644 --- a/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt +++ b/app/src/main/java/org/randomcoder/udroid/runtime/RuntimeSupervisorService.kt @@ -45,6 +45,7 @@ import org.randomcoder.udroid.virgl.VirglHostBackend import org.randomcoder.udroid.virgl.VirglHostSnapshot import org.randomcoder.udroid.virgl.VirglProotLaunchProfile import org.randomcoder.udroid.virgl.VenusGuestRuntimeInstaller +import org.randomcoder.udroid.virgl.parseZinkRequirements import org.randomcoder.udroid.x11.X11ServerController import java.io.BufferedReader import java.io.File @@ -791,6 +792,8 @@ class RuntimeSupervisorService : Service() { request.configuration.virglServerMode, rootfs, ) + DesktopGraphicsProfile.SPHAL_EXPERIMENTAL -> + sphalLaunchProfile(rootfs) DesktopGraphicsProfile.GFXSTREAM_EXPERIMENTAL -> { lateinit var host: GfxstreamHostController host = @@ -816,6 +819,28 @@ class RuntimeSupervisorService : Service() { ) } } + if (request.configuration.graphicsProfile == + DesktopGraphicsProfile.VENUS_EXPERIMENTAL + ) { + runCatching { + logZinkRequirements( + rootfs = rootfs, + socketDirectory = socketDirectory, + launchProfile = launchProfile, + bootId = runtime.bootId, + ) + }.onFailure { error -> + app.journal.append( + component = "graphics", + severity = "warning", + event = "zink_requirements_failed", + message = + error.message ?: "Could not run the Zink requirements probe", + bootId = runtime.bootId, + fields = mapOf("rootfs" to rootfs.name), + ) + } + } val launch = ProotDesktopLaunchBuilder.create( context = this, @@ -1142,11 +1167,103 @@ class RuntimeSupervisorService : Service() { DesktopGraphicsProfile.VIRGL_ANGLE, DesktopGraphicsProfile.VENUS_EXPERIMENTAL, -> virglLaunchProfile(profile, virglServerMode, rootfs) + DesktopGraphicsProfile.SPHAL_EXPERIMENTAL -> sphalLaunchProfile(rootfs) DesktopGraphicsProfile.GFXSTREAM_EXPERIMENTAL -> null else -> EnvironmentProotLaunchProfile.from(profile) } } + private fun sphalLaunchProfile(rootfs: File): ProotLaunchProfile { + check(SphalGraphicsSupport.isEnabled(this)) { + "Enable the experimental SPHAL graphics bridge in About" + } + check(SphalGraphicsSupport.isGuestRuntimeAvailable(rootfs)) { + "The SPHAL guest runtime and patched Mesa are not installed in Linux" + } + val hal = + checkNotNull(SphalGraphicsSupport.findVulkanHal()) { + "No unambiguous Android vendor Vulkan driver was found" + } + return SphalProotLaunchProfile(hal) + } + + private fun logZinkRequirements( + rootfs: File, + socketDirectory: File, + launchProfile: ProotLaunchProfile?, + bootId: String?, + ) { + val profile = launchProfile as? VirglProotLaunchProfile ?: return + if (profile.backend != VirglHostBackend.VENUS || profile.venusGuestRuntime == null) return + val checker = "${VenusGuestRuntimeInstaller.GUEST_DIRECTORY}/bin/zink_check_requirements" + val launch = + ProotApplicationLaunchBuilder.create( + context = this, + runtime = ProotRuntimeInstaller.install(this), + rootfs = rootfs, + x11SocketDirectory = socketDirectory, + application = + LinuxApplication( + id = "udroid-zink-requirements", + name = "Zink requirements", + genericName = null, + comment = null, + executable = "/usr/bin/timeout", + arguments = listOf("5s", checker), + iconName = null, + iconPath = null, + desktopFilePath = "", + desktopFileGuestPath = "", + workingDirectory = "/root", + categories = emptyList(), + terminal = true, + ), + launchProfile = profile, + ) + app.journal.appendCommand( + component = "graphics", + command = launch.command, + bootId = bootId, + fields = mapOf("probe" to "zink_check_requirements", "rootfs" to rootfs.name), + ) + val process = + ProcessBuilder(launch.command) + .directory(launch.workingDirectory) + .redirectErrorStream(true) + .apply { + environment().clear() + environment().putAll(launch.environment) + }.start() + val output = process.inputStream.bufferedReader().use { it.readText() } + val exitCode = process.waitFor() + val results = parseZinkRequirements(output) + val highest = results.lastOrNull { it.supported } + val summary = + when { + exitCode != 0 -> "Zink requirements probe failed with exit code $exitCode" + highest == null -> "Venus does not satisfy the minimum Zink profile" + else -> "Highest supported Zink profile: ${highest.label}" + } + app.journal.append( + component = "graphics", + severity = if (exitCode != 0 || highest == null) "warning" else "info", + event = "zink_requirements", + message = summary, + bootId = bootId, + fields = mapOf("report" to output.trim(), "rootfs" to rootfs.name), + ) + results.forEach { result -> + app.journal.append( + component = "graphics", + severity = if (result.supported) "debug" else "info", + event = "zink_profile", + message = "${result.label}: ${if (result.supported) "supported" else "unsupported"}", + bootId = bootId, + fields = mapOf("profile" to result.profile), + ) + } + } + private fun closeVirglHost() { synchronized(virglHostLock) { ownedVirglHosts.values.forEach(VirglHostController::close) diff --git a/app/src/main/java/org/randomcoder/udroid/runtime/SphalGraphicsSupport.kt b/app/src/main/java/org/randomcoder/udroid/runtime/SphalGraphicsSupport.kt new file mode 100644 index 0000000..f38d9b7 --- /dev/null +++ b/app/src/main/java/org/randomcoder/udroid/runtime/SphalGraphicsSupport.kt @@ -0,0 +1,81 @@ +package org.randomcoder.udroid.runtime + +import android.content.Context +import java.io.File + +object SphalGraphicsSupport { + private const val PREFERENCES_NAME = "experimental-graphics" + private const val KEY_ENABLED = "sphal-enabled" + const val GUEST_RUNNER = "/opt/proot-sphal/bin/proot-vulkan-x11-run" + const val GUEST_MESA_LIBRARY_DIRECTORY = "/opt/udroid/graphics/lib/mesa" + const val GUEST_MESA_DRI_DIRECTORY = "$GUEST_MESA_LIBRARY_DIRECTORY/dri" + + fun isEnabled(context: Context): Boolean = + context.applicationContext + .getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) + .getBoolean(KEY_ENABLED, false) + + fun setEnabled( + context: Context, + enabled: Boolean, + ) { + check( + context.applicationContext + .getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) + .edit() + .putBoolean(KEY_ENABLED, enabled) + .commit(), + ) { + "Could not save the SPHAL graphics setting" + } + } + + fun findVulkanHal(): String? = + listOf("/vendor/lib64/hw", "/odm/lib64/hw") + .asSequence() + .flatMap { directory -> + File(directory).listFiles().orEmpty().asSequence() + }.filter { file -> + file.isFile && + file.name.startsWith("vulkan.") && + file.name.endsWith(".so") && + file.name != "vulkan.default.so" + }.map(File::getAbsolutePath) + .sorted() + .singleOrNull() + + fun isGuestRuntimeAvailable(rootfs: File): Boolean = + listOf( + GUEST_RUNNER, + "$GUEST_MESA_LIBRARY_DIRECTORY/libGL.so.1", + "$GUEST_MESA_DRI_DIRECTORY/zink_dri.so", + ).all { path -> File(rootfs, path.removePrefix("/")).isFile } +} + +internal class SphalProotLaunchProfile( + private val vulkanHal: String, +) : ProotLaunchProfile { + override fun addBindings(arguments: MutableList) { + listOf("/system_ext", "/product", "/vendor", "/odm") + .filter { File(it).exists() } + .forEach { path -> + if (arguments.windowed(2).none { it == listOf("-b", path) }) { + arguments += listOf("-b", path) + } + } + } + + override fun wrapGuestCommand(command: List): List = + listOf( + "/usr/bin/env", + "MESA_LOADER_DRIVER_OVERRIDE=zink", + "GALLIUM_DRIVER=zink", + "LIBGL_KOPPER_DRI2=true", + "ZINK_FORCE_XCB_SURFACE=1", + "LD_LIBRARY_PATH=${SphalGraphicsSupport.GUEST_MESA_LIBRARY_DIRECTORY}:" + + "/usr/lib/aarch64-linux-gnu:/lib/aarch64-linux-gnu", + "LIBGL_DRIVERS_PATH=${SphalGraphicsSupport.GUEST_MESA_DRI_DIRECTORY}", + SphalGraphicsSupport.GUEST_RUNNER, + vulkanHal, + ) + command +} diff --git a/app/src/main/java/org/randomcoder/udroid/ui/AppShell.kt b/app/src/main/java/org/randomcoder/udroid/ui/AppShell.kt index 519b4f4..443cd61 100644 --- a/app/src/main/java/org/randomcoder/udroid/ui/AppShell.kt +++ b/app/src/main/java/org/randomcoder/udroid/ui/AppShell.kt @@ -73,6 +73,7 @@ import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Surface +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -117,6 +118,7 @@ import org.randomcoder.udroid.runtime.ProotMountProfileStore import org.randomcoder.udroid.runtime.RuntimePhase import org.randomcoder.udroid.runtime.RuntimeSnapshot import org.randomcoder.udroid.runtime.RuntimeSupervisorService +import org.randomcoder.udroid.runtime.SphalGraphicsSupport import org.randomcoder.udroid.runtime.VirglServerMode import org.randomcoder.udroid.update.AppUpdatePhase import org.randomcoder.udroid.update.AppUpdateState @@ -959,6 +961,7 @@ private fun ManagementPane( UdroidDestination.DEVICE -> DevicePage( capabilities = capabilities, + journalLines = journalLines, onRefresh = onRefresh, ) UdroidDestination.ABOUT -> @@ -1316,9 +1319,11 @@ private fun updateStatusText(state: AppUpdateState): String = @Composable private fun DevicePage( capabilities: List, + journalLines: List, onRefresh: () -> Unit, ) { val openDeveloperOptions = rememberDeveloperOptionsAction(onRefresh) + val zinkCapabilities = remember(journalLines) { zinkCompatibilityResults(journalLines) } LazyColumn( modifier = Modifier @@ -1348,6 +1353,34 @@ private fun DevicePage( onOpenDeveloperOptions = openDeveloperOptions, ) } + item { + UdroidSectionLabel( + text = "Venus and Zink", + modifier = Modifier.padding(top = 8.dp), + ) + } + if (zinkCapabilities.isEmpty()) { + item { + CapabilityRow( + capability = + CapabilityResult( + name = "Zink through Venus", + status = CapabilityStatus.INFO, + detail = + "Not checked yet. Start a desktop with Venus, then refresh this page.", + required = false, + ), + onOpenDeveloperOptions = openDeveloperOptions, + ) + } + } else { + items(zinkCapabilities) { capability -> + CapabilityRow( + capability = capability, + onOpenDeveloperOptions = openDeveloperOptions, + ) + } + } item { Spacer(Modifier.height(16.dp)) } } } @@ -1440,6 +1473,9 @@ private fun AboutPage( ) { val context = LocalContext.current val uriHandler = LocalUriHandler.current + var sphalEnabled by remember(context) { + mutableStateOf(SphalGraphicsSupport.isEnabled(context)) + } LazyColumn( modifier = Modifier @@ -1508,6 +1544,30 @@ private fun AboutPage( onSponsor = { uriHandler.openUri(GITHUB_SPONSOR_URL) }, ) } + item { + UdroidSectionLabel( + text = "Experimental", + modifier = Modifier.padding(top = 8.dp), + ) + } + item { + ListItem( + headlineContent = { Text("SPHAL graphics bridge") }, + supportingContent = { + Text("Show the vendor Vulkan driver option for compatible Linux systems") + }, + trailingContent = { + Switch( + checked = sphalEnabled, + onCheckedChange = { enabled -> + SphalGraphicsSupport.setEnabled(context, enabled) + sphalEnabled = enabled + }, + ) + }, + colors = ListItemDefaults.colors(containerColor = UdroidRaised), + ) + } item { UdroidToolRow( icon = Icons.Rounded.Feedback, diff --git a/app/src/main/java/org/randomcoder/udroid/ui/LinuxSystemPage.kt b/app/src/main/java/org/randomcoder/udroid/ui/LinuxSystemPage.kt index 818fc03..22c2464 100644 --- a/app/src/main/java/org/randomcoder/udroid/ui/LinuxSystemPage.kt +++ b/app/src/main/java/org/randomcoder/udroid/ui/LinuxSystemPage.kt @@ -77,6 +77,7 @@ import org.randomcoder.udroid.runtime.ProotMountProfile import org.randomcoder.udroid.runtime.ProotMountProfileStore import org.randomcoder.udroid.runtime.RuntimePhase import org.randomcoder.udroid.runtime.RuntimeSnapshot +import org.randomcoder.udroid.runtime.SphalGraphicsSupport import org.randomcoder.udroid.runtime.VirglServerMode import java.text.DateFormat import java.util.Date @@ -126,6 +127,7 @@ fun LinuxSystemPage( remember(rootfs.directory.absolutePath) { GraphicsProfileCompatibilityProbe.run(context, rootfs.directory) } + val sphalEnabled = remember(context) { SphalGraphicsSupport.isEnabled(context) } val openDeveloperOptions = rememberDeveloperOptionsAction(onRefreshCapabilities) val mountProfileStore = remember(context) { ProotMountProfileStore(context) } val environmentProfileStore = remember(context) { ProotEnvironmentProfileStore(context) } @@ -324,6 +326,7 @@ fun LinuxSystemPage( selected = configuration.graphicsProfile, virglServerMode = configuration.virglServerMode, support = graphicsProfileSupport, + sphalEnabled = sphalEnabled, runtimeRunning = runtimeOwnsSystem && snapshot.phase == RuntimePhase.RUNNING, desktopRunning = desktopRunning, @@ -1133,6 +1136,7 @@ private fun GraphicsProfileSelector( selected: DesktopGraphicsProfile, virglServerMode: VirglServerMode = VirglServerMode.AUTOMATIC, support: Map, + sphalEnabled: Boolean, runtimeRunning: Boolean, desktopRunning: Boolean, onSelected: (DesktopGraphicsProfile) -> Unit, @@ -1207,11 +1211,22 @@ private fun GraphicsProfileSelector( title = "Venus (experimental)", detail = "Accelerate Vulkan apps through Android’s driver. OpenGL stays on software " + - "rendering; X11 presentation uses Mesa’s copy path.", + "rendering; X11 presentation uses Mesa’s copy path. Zink profile results " + + "appear in Device compatibility after starting a desktop.", selected = selected, support = support, onSelected = onSelected, ) + if (sphalEnabled) { + GraphicsProfileOption( + profile = DesktopGraphicsProfile.SPHAL_EXPERIMENTAL, + title = "SPHAL Zink (experimental)", + detail = "Use an installed SPHAL bridge with Android's vendor Vulkan driver.", + selected = selected, + support = support, + onSelected = onSelected, + ) + } if ((selected == DesktopGraphicsProfile.VIRGL || selected == DesktopGraphicsProfile.VIRGL_ANGLE) && support[selected]?.available == true diff --git a/app/src/main/java/org/randomcoder/udroid/ui/ProotEnvironmentProfilesPage.kt b/app/src/main/java/org/randomcoder/udroid/ui/ProotEnvironmentProfilesPage.kt index 2ff460a..33e6bc8 100644 --- a/app/src/main/java/org/randomcoder/udroid/ui/ProotEnvironmentProfilesPage.kt +++ b/app/src/main/java/org/randomcoder/udroid/ui/ProotEnvironmentProfilesPage.kt @@ -58,6 +58,7 @@ import org.randomcoder.udroid.runtime.ProotEnvironmentProfile import org.randomcoder.udroid.runtime.ProotEnvironmentProfileStore import org.randomcoder.udroid.runtime.ProotEnvironmentProfileValidator import org.randomcoder.udroid.runtime.ProotTerminalLaunchBuilder +import org.randomcoder.udroid.runtime.SphalGraphicsSupport import java.io.File @Composable @@ -593,7 +594,18 @@ private fun automaticManagedEnvironmentValues( -> set("GALLIUM_DRIVER", "virpipe") DesktopGraphicsProfile.VENUS_EXPERIMENTAL -> { set("VN_DEBUG", "vtest") - set("MESA_VK_WSI_DEBUG", "sw") + } + DesktopGraphicsProfile.SPHAL_EXPERIMENTAL -> { + set("MESA_LOADER_DRIVER_OVERRIDE", "zink") + set("GALLIUM_DRIVER", "zink") + set("LIBGL_KOPPER_DRI2", "true") + set("ZINK_FORCE_XCB_SURFACE", "1") + set( + "LD_LIBRARY_PATH", + "${SphalGraphicsSupport.GUEST_MESA_LIBRARY_DIRECTORY}:" + + "/usr/lib/aarch64-linux-gnu:/lib/aarch64-linux-gnu", + ) + set("LIBGL_DRIVERS_PATH", SphalGraphicsSupport.GUEST_MESA_DRI_DIRECTORY) } DesktopGraphicsProfile.GFXSTREAM_EXPERIMENTAL -> Unit } diff --git a/app/src/main/java/org/randomcoder/udroid/ui/SupervisorReport.kt b/app/src/main/java/org/randomcoder/udroid/ui/SupervisorReport.kt index 23ade6a..929870d 100644 --- a/app/src/main/java/org/randomcoder/udroid/ui/SupervisorReport.kt +++ b/app/src/main/java/org/randomcoder/udroid/ui/SupervisorReport.kt @@ -5,6 +5,8 @@ import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive +import org.randomcoder.udroid.runtime.CapabilityResult +import org.randomcoder.udroid.runtime.CapabilityStatus internal data class DiagnosticLogEntry( val raw: String, @@ -14,6 +16,7 @@ internal data class DiagnosticLogEntry( val event: String, val message: String, val fields: String, + val fieldValues: Map = emptyMap(), ) internal fun formatDiagnosticLogEntry(entry: DiagnosticLogEntry): String = @@ -51,6 +54,10 @@ private fun parseDiagnosticLogEntry(line: String): DiagnosticLogEntry { val payload = runCatching { Json.parseToJsonElement(line).jsonObject }.getOrNull() ?: return DiagnosticLogEntry(line, "", "raw", "journal", "event", line, "") val fields = payload["fields"] as? JsonObject + val fieldValues = + fields + ?.mapValues { (_, value) -> (value as? JsonPrimitive)?.content ?: value.toString() } + .orEmpty() return DiagnosticLogEntry( raw = line, timestamp = payload.string("timestamp").replace('T', ' '), @@ -59,16 +66,66 @@ private fun parseDiagnosticLogEntry(line: String): DiagnosticLogEntry { event = payload.string("event").ifBlank { "event" }, message = payload.string("message").ifBlank { line }, fields = - fields - ?.toSortedMap() + fieldValues + .toSortedMap() ?.entries ?.joinToString(" ") { (key, value) -> - "$key=${(value as? JsonPrimitive)?.content ?: value}" + "$key=$value" } .orEmpty(), + fieldValues = fieldValues, ) } +internal fun zinkCompatibilityResults( + newestFirstJournalLines: List, +): List { + val entries = newestFirstJournalLines.map(::parseDiagnosticLogEntry) + val summaryIndex = + entries.indexOfFirst { + it.component == "graphics" && + it.event in setOf("zink_requirements", "zink_requirements_failed") + } + if (summaryIndex < 0) return emptyList() + + val summary = entries[summaryIndex] + val rootfs = summary.fieldValues["rootfs"] + return buildList { + add( + CapabilityResult( + name = "Zink through Venus", + status = + if (summary.severity.equals("warning", ignoreCase = true)) { + CapabilityStatus.WARNING + } else { + CapabilityStatus.PASS + }, + detail = listOfNotNull(summary.message, rootfs?.let { "Linux system: $it" }).joinToString(" · "), + required = false, + ), + ) + entries + .take(summaryIndex) + .filter { it.component == "graphics" && it.event == "zink_profile" } + .asReversed() + .forEach { entry -> + val supported = entry.message.endsWith(": supported") + add( + CapabilityResult( + name = entry.message.substringBeforeLast(": "), + status = if (supported) CapabilityStatus.PASS else CapabilityStatus.FAIL, + detail = + listOfNotNull( + entry.fieldValues["profile"], + rootfs?.let { "Linux system: $it" }, + ).joinToString(" · "), + required = false, + ), + ) + } + } +} + private fun JsonObject.string(key: String): String = this[key]?.jsonPrimitive?.content.orEmpty() internal fun buildSupervisorReport( diff --git a/app/src/main/java/org/randomcoder/udroid/virgl/VirglHostRuntime.kt b/app/src/main/java/org/randomcoder/udroid/virgl/VirglHostRuntime.kt index c0d93ed..66ab002 100644 --- a/app/src/main/java/org/randomcoder/udroid/virgl/VirglHostRuntime.kt +++ b/app/src/main/java/org/randomcoder/udroid/virgl/VirglHostRuntime.kt @@ -20,7 +20,7 @@ internal data class VenusGuestRuntime( ) internal object VirglHostRuntimeInstaller { - const val VERSION = "1.3.0-6" + const val VERSION = "1.3.0-7" private val bundle = VerifiedRuntimeAssetBundle( name = "VirGL host", @@ -92,6 +92,7 @@ internal object VenusGuestRuntimeInstaller { version = VERSION, entries = listOf( + "bin/zink_check_requirements", "lib/dri/zink_dri.so", "lib/libEGL_mesa.so.0", "lib/libGLX_mesa.so.0", @@ -102,6 +103,7 @@ internal object VenusGuestRuntimeInstaller { "share/glvnd/egl_vendor.d/50_mesa.json", "share/vulkan/icd.d/virtio_icd.aarch64.json", ), + executables = setOf("bin/zink_check_requirements"), metadataKeys = setOf("mesa_commit"), ) @@ -129,3 +131,34 @@ internal object VenusGuestRuntimeInstaller { return values["ID"] == "ubuntu" && values["VERSION_ID"] in setOf("22.04", "24.04") } } + +internal data class ZinkRequirementResult( + val profile: String, + val supported: Boolean, +) { + val label: String = + profile + .removePrefix("VP_ZINK_") + .replace(Regex("^gl(\\d)(\\d)_"), "OpenGL $1.$2 ") + .replace('_', ' ') +} + +internal fun parseZinkRequirements(output: String): List { + var profile: String? = null + return buildList { + output.lineSequence().forEach { line -> + when { + line.startsWith("Checking profile ") -> + profile = line.removePrefix("Checking profile ").trim() + line == "Supported" -> { + profile?.let { add(ZinkRequirementResult(it, true)) } + profile = null + } + line.startsWith("UNSUPPORTED") -> { + profile?.let { add(ZinkRequirementResult(it, false)) } + profile = null + } + } + } + } +} diff --git a/app/src/main/java/org/randomcoder/udroid/virgl/VirglProotLaunchProfile.kt b/app/src/main/java/org/randomcoder/udroid/virgl/VirglProotLaunchProfile.kt index ecda65c..b2e3b99 100644 --- a/app/src/main/java/org/randomcoder/udroid/virgl/VirglProotLaunchProfile.kt +++ b/app/src/main/java/org/randomcoder/udroid/virgl/VirglProotLaunchProfile.kt @@ -40,8 +40,9 @@ internal data class VirglProotLaunchProfile( "GALLIUM_DRIVER", "-u", "MESA_LOADER_DRIVER_OVERRIDE", + "-u", + "MESA_VK_WSI_DEBUG", "VN_DEBUG=vtest", - "MESA_VK_WSI_DEBUG=sw", ) + if (venusGuestRuntime != null) { listOf( diff --git a/app/src/main/java/org/randomcoder/udroid/x11/X11ServerService.kt b/app/src/main/java/org/randomcoder/udroid/x11/X11ServerService.kt index a692938..53e38e5 100644 --- a/app/src/main/java/org/randomcoder/udroid/x11/X11ServerService.kt +++ b/app/src/main/java/org/randomcoder/udroid/x11/X11ServerService.kt @@ -14,6 +14,7 @@ import android.util.Log import java.io.File import java.util.concurrent.Executors import java.util.concurrent.atomic.AtomicBoolean +import org.randomcoder.udroid.BuildConfig class X11ServerService : Service() { private val monitorExecutor = @@ -142,6 +143,7 @@ class X11ServerService : Service() { Os.setenv("TMPDIR", runtime.absolutePath, true) Os.setenv("XKB_CONFIG_ROOT", xkbRoot.canonicalPath, true) Os.setenv("HOME", filesRoot.absolutePath, true) + if (BuildConfig.DEBUG) Os.setenv("TERMUX_X11_DEBUG", "1", true) } private fun monitorSocket( diff --git a/app/src/test/java/org/randomcoder/udroid/DebugCommandStoreTest.kt b/app/src/test/java/org/randomcoder/udroid/DebugCommandStoreTest.kt new file mode 100644 index 0000000..8bc1e11 --- /dev/null +++ b/app/src/test/java/org/randomcoder/udroid/DebugCommandStoreTest.kt @@ -0,0 +1,15 @@ +package org.randomcoder.udroid + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class DebugCommandStoreTest { + @Test + fun acceptsOnlyFixedSupervisorCommands() { + assertTrue(DebugCommandStore.isSupported("start-desktop")) + assertTrue(DebugCommandStore.isSupported("stop-terminal")) + assertFalse(DebugCommandStore.isSupported("run-shell")) + assertFalse(DebugCommandStore.isSupported("")) + } +} diff --git a/app/src/test/java/org/randomcoder/udroid/runtime/DesktopEnvironmentScannerTest.kt b/app/src/test/java/org/randomcoder/udroid/runtime/DesktopEnvironmentScannerTest.kt index 62a3a9a..43e67ce 100644 --- a/app/src/test/java/org/randomcoder/udroid/runtime/DesktopEnvironmentScannerTest.kt +++ b/app/src/test/java/org/randomcoder/udroid/runtime/DesktopEnvironmentScannerTest.kt @@ -314,4 +314,38 @@ class DesktopEnvironmentScannerTest { ), ) } + + @Test + fun `SPHAL profile is available only after its experimental flag is enabled`() { + val value = DesktopGraphicsProfile.SPHAL_EXPERIMENTAL.storageValue + + assertEquals(DesktopGraphicsProfile.STANDARD, DesktopGraphicsProfile.fromStorage(value)) + assertEquals( + DesktopGraphicsProfile.SPHAL_EXPERIMENTAL, + DesktopGraphicsProfile.fromStorage(value, sphalEnabled = true), + ) + } + + @Test + fun `SPHAL profile wraps guest commands with vendor Vulkan and Zink`() { + val command = listOf("startxfce4") + + assertEquals( + listOf( + "/usr/bin/env", + "MESA_LOADER_DRIVER_OVERRIDE=zink", + "GALLIUM_DRIVER=zink", + "LIBGL_KOPPER_DRI2=true", + "ZINK_FORCE_XCB_SURFACE=1", + "LD_LIBRARY_PATH=/opt/udroid/graphics/lib/mesa:" + + "/usr/lib/aarch64-linux-gnu:/lib/aarch64-linux-gnu", + "LIBGL_DRIVERS_PATH=/opt/udroid/graphics/lib/mesa/dri", + SphalGraphicsSupport.GUEST_RUNNER, + "/vendor/lib64/hw/vulkan.mali.so", + "startxfce4", + ), + SphalProotLaunchProfile("/vendor/lib64/hw/vulkan.mali.so") + .wrapGuestCommand(command), + ) + } } diff --git a/app/src/test/java/org/randomcoder/udroid/runtime/GraphicsProfileCompatibilityTest.kt b/app/src/test/java/org/randomcoder/udroid/runtime/GraphicsProfileCompatibilityTest.kt index a0a25f8..c902966 100644 --- a/app/src/test/java/org/randomcoder/udroid/runtime/GraphicsProfileCompatibilityTest.kt +++ b/app/src/test/java/org/randomcoder/udroid/runtime/GraphicsProfileCompatibilityTest.kt @@ -13,6 +13,7 @@ class GraphicsProfileCompatibilityTest { openGlEsVersion = 0x00030000, vulkanVersion = (1 shl 22) or (1 shl 12), hardwareBuffer = true, + vendorVulkanHal = true, ) val compatibleGuest = GraphicsGuestSupport( @@ -21,12 +22,14 @@ class GraphicsProfileCompatibilityTest { vulkanIcd = true, virgl = true, venus = true, + sphal = true, ) val supported = GraphicsProfileCompatibilityProbe.classify(compatibleDevice, compatibleGuest) assertTrue(supported.getValue(DesktopGraphicsProfile.VIRGL).available) assertTrue(supported.getValue(DesktopGraphicsProfile.VIRGL_ANGLE).available) assertTrue(supported.getValue(DesktopGraphicsProfile.VENUS_EXPERIMENTAL).available) + assertTrue(supported.getValue(DesktopGraphicsProfile.SPHAL_EXPERIMENTAL).available) val noAndroidVulkan = GraphicsProfileCompatibilityProbe.classify( @@ -37,6 +40,7 @@ class GraphicsProfileCompatibilityTest { assertTrue(noAndroidVulkan.getValue(DesktopGraphicsProfile.ZINK).available) assertFalse(noAndroidVulkan.getValue(DesktopGraphicsProfile.VIRGL_ANGLE).available) assertFalse(noAndroidVulkan.getValue(DesktopGraphicsProfile.VENUS_EXPERIMENTAL).available) + assertFalse(noAndroidVulkan.getValue(DesktopGraphicsProfile.SPHAL_EXPERIMENTAL).available) val minimalGuest = GraphicsProfileCompatibilityProbe.classify( @@ -47,6 +51,7 @@ class GraphicsProfileCompatibilityTest { vulkanIcd = false, virgl = false, venus = false, + sphal = false, ), ) assertTrue(minimalGuest.getValue(DesktopGraphicsProfile.STANDARD).available) @@ -54,5 +59,6 @@ class GraphicsProfileCompatibilityTest { assertFalse(minimalGuest.getValue(DesktopGraphicsProfile.ZINK).available) assertFalse(minimalGuest.getValue(DesktopGraphicsProfile.VIRGL).available) assertFalse(minimalGuest.getValue(DesktopGraphicsProfile.VENUS_EXPERIMENTAL).available) + assertFalse(minimalGuest.getValue(DesktopGraphicsProfile.SPHAL_EXPERIMENTAL).available) } } diff --git a/app/src/test/java/org/randomcoder/udroid/runtime/SphalGraphicsSupportTest.kt b/app/src/test/java/org/randomcoder/udroid/runtime/SphalGraphicsSupportTest.kt new file mode 100644 index 0000000..86b5220 --- /dev/null +++ b/app/src/test/java/org/randomcoder/udroid/runtime/SphalGraphicsSupportTest.kt @@ -0,0 +1,34 @@ +package org.randomcoder.udroid.runtime + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import java.io.File + +class SphalGraphicsSupportTest { + @get:Rule + val temporaryFolder = TemporaryFolder() + + @Test + fun `guest runtime requires the launcher and patched Mesa`() { + val rootfs = temporaryFolder.newFolder("rootfs") + val requiredFiles = + listOf( + SphalGraphicsSupport.GUEST_RUNNER, + "${SphalGraphicsSupport.GUEST_MESA_LIBRARY_DIRECTORY}/libGL.so.1", + "${SphalGraphicsSupport.GUEST_MESA_DRI_DIRECTORY}/zink_dri.so", + ).map { path -> File(rootfs, path.removePrefix("/")) } + + assertFalse(SphalGraphicsSupport.isGuestRuntimeAvailable(rootfs)) + requiredFiles.forEach { file -> + file.parentFile?.mkdirs() + file.createNewFile() + } + assertTrue(SphalGraphicsSupport.isGuestRuntimeAvailable(rootfs)) + + requiredFiles.last().delete() + assertFalse(SphalGraphicsSupport.isGuestRuntimeAvailable(rootfs)) + } +} diff --git a/app/src/test/java/org/randomcoder/udroid/ui/SupervisorReportTest.kt b/app/src/test/java/org/randomcoder/udroid/ui/SupervisorReportTest.kt index 3e8b3d9..33c3aaf 100644 --- a/app/src/test/java/org/randomcoder/udroid/ui/SupervisorReportTest.kt +++ b/app/src/test/java/org/randomcoder/udroid/ui/SupervisorReportTest.kt @@ -44,4 +44,21 @@ class SupervisorReportTest { formatDiagnosticLogEntry(entry), ) } + + @Test + fun latestZinkCheckBecomesReadableCompatibilityRows() { + val results = + zinkCompatibilityResults( + listOf( + """{"timestamp":"2026-10-03T09:00:02Z","component":"graphics","severity":"info","event":"zink_profile","message":"OpenGL 4.6 baseline: unsupported","fields":{"profile":"VP_ZINK_gl46_baseline"}}""", + """{"timestamp":"2026-10-03T09:00:01Z","component":"graphics","severity":"debug","event":"zink_profile","message":"OpenGL 2.1 baseline: supported","fields":{"profile":"VP_ZINK_gl21_baseline"}}""", + """{"timestamp":"2026-10-03T09:00:00Z","component":"graphics","severity":"info","event":"zink_requirements","message":"Highest supported Zink profile: OpenGL 2.1 baseline","fields":{"rootfs":"proot-ubuntu-jammy"}}""", + ), + ) + + assertEquals("Zink through Venus", results[0].name) + assertEquals("OpenGL 2.1 baseline", results[1].name) + assertEquals("OpenGL 4.6 baseline", results[2].name) + assertEquals("VP_ZINK_gl46_baseline · Linux system: proot-ubuntu-jammy", results[2].detail) + } } diff --git a/app/src/test/java/org/randomcoder/udroid/virgl/VirglHostLaunchTest.kt b/app/src/test/java/org/randomcoder/udroid/virgl/VirglHostLaunchTest.kt index 8970cc8..218e63c 100644 --- a/app/src/test/java/org/randomcoder/udroid/virgl/VirglHostLaunchTest.kt +++ b/app/src/test/java/org/randomcoder/udroid/virgl/VirglHostLaunchTest.kt @@ -166,7 +166,7 @@ class VirglHostLaunchTest { } @Test - fun `selects Venus vtest with the X11 copy presentation path`() { + fun `selects Venus vtest with native WSI presentation`() { val socket = Files.createTempFile("udroid-venus", ".sock").toFile() val runtime = Files.createTempDirectory("udroid-venus-runtime").toFile() val profile = @@ -197,8 +197,9 @@ class VirglHostLaunchTest { "GALLIUM_DRIVER", "-u", "MESA_LOADER_DRIVER_OVERRIDE", + "-u", + "MESA_VK_WSI_DEBUG", "VN_DEBUG=vtest", - "MESA_VK_WSI_DEBUG=sw", "LD_LIBRARY_PATH=/opt/udroid/venus-mesa/lib:" + "/usr/lib/aarch64-linux-gnu:/lib/aarch64-linux-gnu", "LIBGL_DRIVERS_PATH=/opt/udroid/venus-mesa/lib/dri", @@ -227,4 +228,29 @@ class VirglHostLaunchTest { osRelease.writeText("ID=ubuntu\nVERSION_ID=\"20.04\"\n") assertFalse(VenusGuestRuntimeInstaller.requiresBundledMesa(rootfs)) } + + @Test + fun `parses verbose Zink profile results`() { + val results = + parseZinkRequirements( + """ + Checking profile VP_ZINK_gl21_baseline + Checking device Virtio-GPU Venus + Supported + + Checking profile VP_ZINK_gl46_baseline + Checking device Virtio-GPU Venus + UNSUPPORTED physical device + """.trimIndent(), + ) + + assertEquals( + listOf( + ZinkRequirementResult("VP_ZINK_gl21_baseline", true), + ZinkRequirementResult("VP_ZINK_gl46_baseline", false), + ), + results, + ) + assertEquals("OpenGL 2.1 baseline", results.first().label) + } } diff --git a/docs/DESKTOP_INPUT_AND_SESSION_ROADMAP.md b/docs/DESKTOP_INPUT_AND_SESSION_ROADMAP.md index 35a44f3..221a59f 100644 --- a/docs/DESKTOP_INPUT_AND_SESSION_ROADMAP.md +++ b/docs/DESKTOP_INPUT_AND_SESSION_ROADMAP.md @@ -539,3 +539,58 @@ not prohibited by the architecture. Normal UI may call these Linux workspaces. Advanced UI may expose display numbers, PIDs, sockets, leases, and renderer state. + +## Native Wayland session (planned, opt-in) + +Wayland is a separate session backend, not Wayland-over-Lorie. Keep the existing +Lorie/X11 session unchanged as the fallback. The Android app owns the Wayland +compositor and its Android surface; Linux clients run in the selected PRoot and +connect through a private Wayland socket. A guest desktop compositor such as +KWin is nested as a Wayland client, so this is rootless and does not pretend +that PRoot owns DRM/KMS. + +Use an Android-hosted wlroots compositor as the reference architecture. +WLDroid at `9560e767bb3e31f3d9b72d280e051c47cda35cf1` is a useful black-box +reference, not a safe integration dependency yet. Its current code advertises +fabricated DRM device feedback, assumes a private AHardwareBuffer native-handle +layout for DMA-BUF export, submits buffers without acquire fences, and drives +frames from a fixed 16 ms timer. Do not carry these assumptions into uDroid. +Audit every dependency license before considering any reuse. Do not port the +GPL-3.0 wlroots-android-bridge code into uDroid. The existing experimental +AHardwareBuffer/SurfaceControl presenter is a candidate output component, not +proof that the complete Wayland protocol, client-buffer import, or KWin path is +ready. + +### First checkpoint and current status + +The pinned source audit is complete, but its reference build is not. The local +macOS build reached the cross-build and stopped because the host Bison is 2.3, +below libxkbcommon's required 3.6. Meson was installed only in a temporary +Python environment; no APK or compositor library was produced, and nothing +was installed on a device. + +Do not run stock WLDroid with KWin: it advertises fabricated DRM device +feedback and its AHardwareBuffer output omits acquire fences. A standalone APK +also cannot connect to uDroid's app-private rootfs/socket because it has a +different Android UID. These are architectural blockers, not just build +friction. + +1. Keep the current X11 path unchanged. Do not import WLDroid's Proot, launcher, + VirGL, or Android shims into uDroid. +2. If continuing with this reference, first make a disposable development + variant that completely disables its linux-dmabuf global/feedback and uses + a software/SHM client-buffer path plus CPU-copy presentation. Do not expose + fake DRM identity or the current no-fence AHardwareBuffer path. +3. Embed that compositor in a uDroid dev build so the selected distro connects + through a same-UID private Wayland socket; a separate APK cannot test this + boundary. +4. Launch `weston-simple-shm`; verify pixels, resize, touch/pointer/keyboard + input, detach/reattach, and clean stop/restart before trying EGL or KWin. +5. Add EGL and guest KWin only after the SHM/lifecycle gate passes. Keep + `linux-dmabuf` disabled until device feedback is truthful and the complete + buffer import plus acquire/release ordering is validated. + +The first checkpoint passes only if X11 remains unaffected, the SHM client is +visibly correct, input reaches it, and the session survives lifecycle cycles. +GPU acceleration, Plasma support, and performance qualification are later +gates; the Wayland option stays hidden/disabled until each is demonstrated. diff --git a/docs/PERFORMANCE.md b/docs/PERFORMANCE.md index ebf55a9..18ba40a 100644 --- a/docs/PERFORMANCE.md +++ b/docs/PERFORMANCE.md @@ -62,6 +62,9 @@ Keep expensive runtime scenarios small and reproducible first. Add focused Macrobenchmarks or microbenchmarks for a suspected cost, inspect traces, then validate the resulting change in the complete user journey. +For Venus, Zink, Lorie, and SurfaceFlinger profiling, use the reproducible +[Venus Perfetto capture](graphics/VENUS_PERFETTO_PROFILING.md). + References: - [Compose performance](https://developer.android.com/develop/ui/compose/performance) diff --git a/docs/graphics/SPHAL_ZINK_PROFILE.md b/docs/graphics/SPHAL_ZINK_PROFILE.md new file mode 100644 index 0000000..cd567b3 --- /dev/null +++ b/docs/graphics/SPHAL_ZINK_PROFILE.md @@ -0,0 +1,62 @@ +# SPHAL Zink profile + +The hidden `SPHAL Zink (experimental)` profile runs guest Zink on the Android +vendor Vulkan HAL and presents through uDroid's embedded Termux:X11 renderer. +It is disabled by default because the guest runtime is not yet installed by the +app and vendor HAL compatibility is device-specific. + +## Runtime path + +```text +Linux OpenGL application + -> patched Mesa Zink in /opt/udroid/graphics + -> /opt/proot-sphal/bin/proot-vulkan-x11-run + -> Android vendor Vulkan HAL + -> AHardwareBuffer/private DRI3 transport + -> embedded Termux:X11 GPU Present copy + -> Android Surface +``` + +The app exposes the profile only after **About > Experimental > SPHAL graphics +bridge** is enabled. Compatibility requires AArch64, Android Vulkan 1.1, +AHardwareBuffer support, exactly one non-default Vulkan HAL under +`/vendor/lib64/hw` or `/odm/lib64/hw`, and these guest files: + +- `/opt/proot-sphal/bin/proot-vulkan-x11-run` +- `/opt/udroid/graphics/lib/mesa/libGL.so.1` +- `/opt/udroid/graphics/lib/mesa/dri/zink_dri.so` + +The launch profile binds the Android vendor trees, selects Zink and Kopper's +XCB surface, and places the patched Mesa libraries before the distribution +libraries. Terminal and desktop launches use the same profile so applications +started from either path inherit the same driver selection. + +## Display-server contract + +SPHAL is selected at runtime, while the embedded display server is compiled +once. Therefore the AHardwareBuffer/DRI3 contract is present in every build, +not conditionally patched when SPHAL is selected. See the +[Termux:X11 patch inventory](../../patches/termux-x11/README.md). + +The essential behavior is: + +1. Import producer AHardwareBuffers with explicit RGBA/BGRA content semantics. +2. Preserve GPU-backed redirected pixmaps and export their existing storage + through DRI3. +3. Accept both DMA-BUF and AHardwareBuffer sources in the GPU Present path. +4. Keep the CPU fallback synchronized, but never force a CPU map of GPU-only + storage. +5. Revert the old `0010` GPU-copy disable patch; otherwise composited windows + can remain black even though the vendor driver is running. + +On the Pixel 6a qualification run, repeated standard/experimental source +transitions remained idempotent, XFCE rendered with normal colors, and Lorie +reported `8/8 present copies offloaded to GPU`. This proves the intended +transport was used for that run; it is not broad device qualification. + +## Current boundary + +uDroid currently consumes a manually installed `proot-sphal` guest runtime. +The standalone Termux/PRoot distribution and its paired Termux:X11 artifact are +maintained separately and must be updated in their own repository before this +profile can be offered as a complete end-user install path. diff --git a/docs/graphics/VENUS_PERFETTO_PROFILING.md b/docs/graphics/VENUS_PERFETTO_PROFILING.md new file mode 100644 index 0000000..28feb90 --- /dev/null +++ b/docs/graphics/VENUS_PERFETTO_PROFILING.md @@ -0,0 +1,113 @@ +# Venus and Zink Perfetto profiling + +Use this trace when a Venus or Venus/Zink desktop feels slow, misses frames, +or stalls while the renderer still appears healthy. It separates guest work, +Lorie presentation, and Android composition instead of treating one FPS number +as the cause. + +## Capture + +1. Select **Venus (experimental)**, start the desktop, and open **Display**. +2. Start a continuously rendered workload. A static desktop or cursor-only + movement is not a useful graphics baseline. +3. Record the 15-second trace while reproducing the slowdown: + +```sh +adb shell perfetto --txt -c - \ + -o /data/misc/perfetto-traces/udroid-venus.perfetto-trace \ + < tools/profiling/venus-perfetto.pbtxt +adb pull /data/misc/perfetto-traces/udroid-venus.perfetto-trace . +``` + +Open the trace in [Perfetto UI](https://ui.perfetto.dev/). Keep the workload, +resolution, compositor setting, graphics profile, device temperature, and app +build identical when comparing two traces. + +The config records the uDroid process, CPU scheduling and frequency, Mali job +submission/completion, GPU frequency and memory, SurfaceFlinger frames and +layers, and FrameTimeline. It intentionally does not request +`gpu.renderstages`: the Pixel 6a used for this checkpoint does not publish that +data source. Check another device with: + +```sh +adb shell perfetto --query --long +``` + +Unavailable vendor ftrace events may be omitted by that device. Keep the +portable scheduling, SurfaceFlinger, and uDroid trace sources enabled. + +## Read the trace + +Find these tracks in order: + +1. The guest workload and `virgl_render_server` threads: look for runnable but + unscheduled gaps or a saturated CPU. +2. `org.randomcoder.udroid.dev`: expand the Lorie renderer thread and inspect + `uDroid/Lorie draw`, `root fence wait`, `eglSwapBuffers`, and + `post-swap fence wait` slices. +3. GPU frequency and Mali job tracks: confirm whether the GPU is busy while a + Lorie wait is open. +4. The uDroid `SurfaceView` under SurfaceFlinger: compare buffer arrival with + expected and actual presentation. + +A stable 16.67 ms interval is one frame at 60 Hz, not evidence of 16.67 ms of +rendering. Investigate intervals that repeatedly become 33.3 ms or longer, +long fence slices, scheduling gaps, or buffers that arrive after their desired +presentation time. + +FrameTimeline does not attribute every `SurfaceView` producer correctly on +every Android release. Treat it as Android composition evidence and correlate +it with the Lorie slices and SurfaceFlinger layer rather than using it alone. + +Use **Query (SQL)** for a reproducible summary: + +```sql +SELECT name, + count(*) AS frames, + round(avg(dur) / 1000000.0, 3) AS avg_ms, + round(max(dur) / 1000000.0, 3) AS max_ms +FROM slice +WHERE name LIKE 'uDroid/Lorie%' +GROUP BY name +ORDER BY name; +``` + +```sql +SELECT present_type, on_time_finish, jank_type, count(*) AS frames +FROM actual_frame_timeline_slice +GROUP BY present_type, on_time_finish, jank_type +ORDER BY frames DESC; +``` + +```sql +SELECT min(value) AS min_khz, + round(avg(value), 1) AS avg_khz, + max(value) AS max_khz +FROM counter +JOIN counter_track ON counter_track.id = counter.track_id +WHERE counter_track.type = 'gpu_frequency'; +``` + +## Pixel 6a checkpoint + +On Android 17 with uDroid `0.1.4+dev`, a 15-second Venus/Zink `glxgears` +capture produced 898 Lorie frames and 899 on-time SurfaceFlinger presents with +no reported jank. Lorie draw averaged 4.37 ms; the root-fence wait averaged +1.39 ms, `eglSwapBuffers` 0.79 ms, and the post-swap wait 0.73 ms. GPU +frequency averaged 171 MHz and peaked at 434 MHz. + +That run held the 60 Hz presentation budget. It also emitted per-frame +`writeReleaseFence ... Broken pipe` messages, so treat that message as a +separate Android buffer-release-channel compatibility problem, not proof of a +slow frame by itself. + +## Comparison gate + +Use at least three matched captures. A change is an improvement only if it +reduces p95/p99 missed-frame intervals or CPU/GPU work without introducing +corruption, transparent frames, resize failures, or teardown hangs. Do not +promote a Zink environment option from a single FPS result. + +References: [Perfetto FrameTimeline](https://perfetto.dev/docs/data-sources/frametimeline), +[GPU tracing](https://perfetto.dev/docs/data-sources/gpu), and +[ATrace](https://perfetto.dev/docs/data-sources/atrace). diff --git a/docs/graphics/gfxstream-surface-presenter.md b/docs/graphics/gfxstream-surface-presenter.md index 11c57af..fbe0aeb 100644 --- a/docs/graphics/gfxstream-surface-presenter.md +++ b/docs/graphics/gfxstream-surface-presenter.md @@ -251,12 +251,14 @@ transport now separates the physical buffer format from its content semantics: - `1255`: RGBA AHardwareBuffer carrying BGRA-compatible content; the X11 renderer applies its channel swizzle. -- `1257`: RGBA AHardwareBuffer carrying native RGBA content; the X11 renderer - samples it without the swizzle. - -Before `1257`, direct Vulkan rendered the LunarG texture with visibly swapped -brown channels. The paired client/server change restores its cyan/teal colors -without regressing GLX. The server publishes the versioned root-window +- `1257`: RGBA AHardwareBuffer whose sampled bytes match the X11 root-buffer + contract; the GPU copy does not swizzle it, and the final root draw performs + the single required Android presentation swizzle. + +Treating `1257` as already Android-display-ready added a second channel swap; +SuperTuxKart exposed it as blue UI and kart colors. Matching the imported +buffer to the X11 root contract restores normal red, green, and yellow colors +without disabling GPU Present. The server publishes the versioned root-window property `_UDROID_X11_BUFFER_TRANSPORT`; the verified value is `1, 11, 1255, 0, 1257, 0`, advertising AHardwareBuffer socket transport, RGBA content semantics, and GPU-copy support. It deliberately does not advertise diff --git a/patches/termux-x11/0000-xserver-patch-semantic-idempotence.patch b/patches/termux-x11/0000-xserver-patch-semantic-idempotence.patch index a33ce47..570d7dc 100644 --- a/patches/termux-x11/0000-xserver-patch-semantic-idempotence.patch +++ b/patches/termux-x11/0000-xserver-patch-semantic-idempotence.patch @@ -1,3 +1,9 @@ +termux-x11: make nested Xserver patching idempotent + +Later uDroid patches modify files already touched by xserver.patch, making a +reverse dry-run unreliable. Record the base patch semantically before applying +the rest of the stack. + --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -20,6 +20,17 @@ find_package(Python3 REQUIRED) diff --git a/patches/termux-x11/0001-android-xlocale-include-order.patch b/patches/termux-x11/0001-android-xlocale-include-order.patch index 8292ea0..5bc4132 100644 --- a/patches/termux-x11/0001-android-xlocale-include-order.patch +++ b/patches/termux-x11/0001-android-xlocale-include-order.patch @@ -1,3 +1,8 @@ +libX11: use Bionic's xlocale compatibility header + +Including locale.h directly selects the glibc-oriented declarations. Include +Android's compatibility header when building the embedded libX11 instead. + --- a/include/X11/Xlocale.h +++ b/include/X11/Xlocale.h @@ -32,6 +32,10 @@ diff --git a/patches/termux-x11/0002-udroid-native-server-entrypoint.patch b/patches/termux-x11/0002-udroid-native-server-entrypoint.patch index 89c38d3..b906fb1 100644 --- a/patches/termux-x11/0002-udroid-native-server-entrypoint.patch +++ b/patches/termux-x11/0002-udroid-native-server-entrypoint.patch @@ -1,3 +1,8 @@ +lorie: expose the X server entry point to uDroid + +uDroid embeds libXlorie without Termux:X11's CmdEntryPoint class. Export its JNI +name while keeping both applications on the same startup implementation. + --- a/cmdentrypoint.c +++ b/cmdentrypoint.c @@ -69,8 +69,7 @@ static Bool detectTracer(void) diff --git a/patches/termux-x11/0003-udroid-renderer-bridge.patch b/patches/termux-x11/0003-udroid-renderer-bridge.patch index 34d006a..28412fd 100644 --- a/patches/termux-x11/0003-udroid-renderer-bridge.patch +++ b/patches/termux-x11/0003-udroid-renderer-bridge.patch @@ -1,3 +1,8 @@ +lorie: add the uDroid renderer bridge + +The embedded display uses uDroid JNI names rather than Termux:X11's classes. +Route both consumers through the shared renderer and X socket helpers. + --- a/cmdentrypoint.c +++ b/cmdentrypoint.c @@ -511,8 +511,7 @@ void DDXNotifyFocusChanged(void) { diff --git a/patches/termux-x11/0004-udroid-batched-native-touch.patch b/patches/termux-x11/0004-udroid-batched-native-touch.patch index 81287f3..b74bb68 100644 --- a/patches/termux-x11/0004-udroid-batched-native-touch.patch +++ b/patches/termux-x11/0004-udroid-batched-native-touch.patch @@ -1,3 +1,9 @@ +lorie: batch Android multi-touch frames + +Send every contact from one Android sample as a single ordered X11 update. +Publishing contacts independently exposes partial state and can reorder or +strand touches under load. + --- a/lorie.h +++ b/lorie.h @@ -100,6 +100,7 @@ typedef enum { diff --git a/patches/termux-x11/0005-dmabuf-cpu-read-sync.patch b/patches/termux-x11/0005-dmabuf-cpu-read-sync.patch index 49ef860..fb7626d 100644 --- a/patches/termux-x11/0005-dmabuf-cpu-read-sync.patch +++ b/patches/termux-x11/0005-dmabuf-cpu-read-sync.patch @@ -1,3 +1,10 @@ +lorie: synchronize DMA-BUF reads before GLES upload + +A GPU producer may leave cache lines invisible to the CPU. Bracket mapped reads +with DMA_BUF_IOCTL_SYNC and retain the shared-memory path for ordinary file +descriptors. This follows the Linux DMA-BUF userspace contract and the work in +termux/termux-x11#1148. + --- a/buffer.c +++ b/buffer.c @@ -7,12 +7,17 @@ @@ -18,12 +25,11 @@ #include #include #include -@@ -33,12 +38,80 @@ struct LorieBuffer { +@@ -33,12 +38,57 @@ struct LorieBuffer { int fd; size_t size; off_t offset; + int8_t dmaBufSyncSupport; -+ uint32_t debugUploadCount; GLuint id; EGLImage image; @@ -73,49 +79,25 @@ + buffer->fd, strerror(errno)); + } +} -+ -+static void debugMappedBuffer(LorieBuffer *buffer) { -+ if (!getenv("TERMUX_X11_DEBUG") || (buffer->debugUploadCount++ % 120) != 0) -+ return; -+ -+ const uint32_t *words = buffer->desc.data; -+ size_t wordCount = buffer->size / sizeof(*words); -+ size_t step = wordCount > 256 ? wordCount / 256 : 1; -+ uint32_t hash = 2166136261u; -+ uint32_t nonzero = 0; -+ for (size_t index = 0; index < wordCount; index += step) { -+ uint32_t value = words[index]; -+ nonzero += value != 0; -+ hash = (hash ^ value) * 16777619u; -+ } -+ -+ dprintf(2, -+ "LorieBuffer: fd %d DMA-BUF=%s sampled_nonzero=%u hash=%08x size=%zu\n", -+ buffer->fd, -+ buffer->dmaBufSyncSupport == DMA_BUF_SYNC_SUPPORTED ? "yes" : "no", -+ nonzero, hash, buffer->size); -+} + __attribute__((unused)) static int memfd_create(const char *name, unsigned int flags) { #ifndef __NR_memfd_create -@@ -356,6 +429,8 @@ __LIBC_HIDDEN__ void LorieBuffer_recvHandleFromUnixSocket(int socketFd, LorieBuf +@@ -356,6 +406,7 @@ __LIBC_HIDDEN__ void LorieBuffer_recvHandleFromUnixSocket(int socketFd, LorieBuf read(socketFd, &buffer, sizeof(buffer)); buffer.image = NULL; // Only for process-local use + buffer.dmaBufSyncSupport = DMA_BUF_SYNC_UNKNOWN; -+ buffer.debugUploadCount = 0; if (buffer.desc.type == LORIEBUFFER_FD) { size_t size = buffer.desc.stride * buffer.desc.height * sizeof(uint32_t); buffer.fd = ancil_recv_fd(socketFd); -@@ -421,8 +496,13 @@ __LIBC_HIDDEN__ void LorieBuffer_bindTexture(LorieBuffer *buffer) { +@@ -421,8 +472,12 @@ __LIBC_HIDDEN__ void LorieBuffer_bindTexture(LorieBuffer *buffer) { return; glBindTexture(GL_TEXTURE_2D, buffer->id); - if (buffer->desc.type == LORIEBUFFER_FD) + if (buffer->desc.type == LORIEBUFFER_FD) { + bool dmaBufRead = dmaBufCpuReadBegin(buffer); -+ debugMappedBuffer(buffer); glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, buffer->desc.stride, buffer->desc.height, buffer->desc.format == AHARDWAREBUFFER_FORMAT_B8G8R8A8_UNORM ? GL_BGRA_EXT : GL_RGBA, GL_UNSIGNED_BYTE, buffer->desc.data); + if (dmaBufRead) + dmaBufCpuReadEnd(buffer); diff --git a/patches/termux-x11/0006-ahardwarebuffer-external-texture-sampling.patch b/patches/termux-x11/0006-ahardwarebuffer-external-texture-sampling.patch index 9e3dce5..2c24555 100644 --- a/patches/termux-x11/0006-ahardwarebuffer-external-texture-sampling.patch +++ b/patches/termux-x11/0006-ahardwarebuffer-external-texture-sampling.patch @@ -1,8 +1,14 @@ +lorie: sample AHardwareBuffers as external textures + +Android BufferQueue images use external-image storage. Keep the existing 2D +view for framebuffer use, but sample supported AHardwareBuffers through +GL_TEXTURE_EXTERNAL_OES to avoid incomplete or stale images on Mali. + --- a/buffer.c +++ b/buffer.c -@@ -42,6 +42,7 @@ struct LorieBuffer { - uint32_t debugUploadCount; - +@@ -41,6 +41,7 @@ struct LorieBuffer { + int8_t dmaBufSyncSupport; + GLuint id; + GLuint externalId; EGLImage image; @@ -17,14 +23,13 @@ if (eglGetCurrentDisplay() && buffer->image) eglDestroyImageKHR(eglGetCurrentDisplay(), buffer->image); -@@ -428,6 +431,8 @@ __LIBC_HIDDEN__ void LorieBuffer_recvHandleFromUnixSocket(int socketFd, LorieBuf +@@ -404,5 +407,7 @@ __LIBC_HIDDEN__ void LorieBuffer_recvHandleFromUnixSocket(int socketFd, LorieBuf read(socketFd, &buffer, sizeof(buffer)); buffer.image = NULL; // Only for process-local use + buffer.id = 0; + buffer.externalId = 0; buffer.dmaBufSyncSupport = DMA_BUF_SYNC_UNKNOWN; - buffer.debugUploadCount = 0; if (buffer.desc.type == LORIEBUFFER_FD) { @@ -489,6 +494,22 @@ __LIBC_HIDDEN__ void LorieBuffer_attachToGL(LorieBuffer* buffer) { // The image will be updated in redraw call because of `drawRequested` flag, so we are not uploading pixels diff --git a/patches/termux-x11/0007-ahardwarebuffer-content-semantics.patch b/patches/termux-x11/0007-ahardwarebuffer-content-semantics.patch index 30ed8d0..7bcef2d 100644 --- a/patches/termux-x11/0007-ahardwarebuffer-content-semantics.patch +++ b/patches/termux-x11/0007-ahardwarebuffer-content-semantics.patch @@ -1,3 +1,9 @@ +lorie: define AHardwareBuffer content and ownership semantics + +Give the private DRI3 modifiers explicit BGRA and RGBA meanings and bound the +socket handshake. This prevents swapped colors and releases received buffers +when transport fails. + --- a/InitOutput.c +++ b/InitOutput.c @@ -14,6 +14,8 @@ @@ -86,7 +92,7 @@ + check(!(priv->buffer = LorieBuffer_wrapAHardwareBuffer(received_buffer)), "DRI3: LorieBuffer_wrapAHardwareBuffer failed."); + received_buffer = NULL; + if (modifier == AHARDWAREBUFFER_RGBA_SOCKET_FD) -+ LorieBuffer_setNeedsBgraSwizzle(priv->buffer, false); ++ LorieBuffer_setNeedsBgraSwizzle(priv->buffer, true); screen->ModifyPixmapHeader(pixmap, desc.width, desc.height, 0, 0, desc.stride * 4, NULL); if (lorieServerDebugEnabled) diff --git a/patches/termux-x11/0008-advertise-buffer-transport-protocol.patch b/patches/termux-x11/0008-advertise-buffer-transport-protocol.patch index 2d77d54..3303eff 100644 --- a/patches/termux-x11/0008-advertise-buffer-transport-protocol.patch +++ b/patches/termux-x11/0008-advertise-buffer-transport-protocol.patch @@ -1,3 +1,9 @@ +lorie: advertise the private buffer transport contract + +Publish a versioned root-window property so guest producers can discover AHB +socket transport, channel semantics, and GPU Present support instead of +guessing. The property describes capabilities without enabling them. + --- a/InitOutput.c +++ b/InitOutput.c @@ -40,6 +40,9 @@ @@ -58,3 +64,83 @@ return TRUE; } /* end lorieScreenInit */ +@@ -1184,32 +1219,56 @@ + return NULL; + } +- +-static int lorieGetFormats(__unused ScreenPtr screen, CARD32 *num_formats, CARD32 **formats) { +- static CARD32 format = DRM_FORMAT_ARGB8888; +- *num_formats = 1; +- *formats = &format; +- return TRUE; +-} +- +-static int lorieGetModifiers(__unused ScreenPtr screen, uint32_t format, uint32_t *num_modifiers, uint64_t **modifiers) { +- static uint64_t modifier = DRM_FORMAT_MOD_LINEAR; +- +- if (format != DRM_FORMAT_ARGB8888 && format != DRM_FORMAT_XRGB8888) { +- *num_modifiers = 0; +- *modifiers = NULL; +- return TRUE; +- } +- +- *num_modifiers = 1; +- *modifiers = &modifier; +- return TRUE; +-} +- ++ ++static int lorieGetFormats(__unused ScreenPtr screen, CARD32 *num_formats, CARD32 **formats) { ++ static CARD32 format = DRM_FORMAT_ARGB8888; ++ *num_formats = 1; ++ *formats = &format; ++ return TRUE; ++} ++ ++static uint64_t lorieSupportedModifiers[] = { ++ 1255, /* AHardwareBuffer BGRA socket transport */ ++ 1257, /* AHardwareBuffer RGBA socket transport */ ++ DRM_FORMAT_MOD_LINEAR, ++}; ++ ++static int lorieGetModifiers(__unused ScreenPtr screen, uint32_t format, uint32_t *num_modifiers, uint64_t **modifiers) { ++ if (format != DRM_FORMAT_ARGB8888 && format != DRM_FORMAT_XRGB8888) { ++ *num_modifiers = 0; ++ *modifiers = NULL; ++ return TRUE; ++ } ++ ++ *num_modifiers = sizeof(lorieSupportedModifiers) / sizeof(lorieSupportedModifiers[0]); ++ *modifiers = lorieSupportedModifiers; ++ return TRUE; ++} ++ ++static int lorieGetDrawableModifiers(__unused DrawablePtr draw, uint32_t format, ++ uint32_t *num_modifiers, uint64_t **modifiers) { ++ uint64_t *copy; ++ ++ if (format != DRM_FORMAT_ARGB8888 && format != DRM_FORMAT_XRGB8888) { ++ *num_modifiers = 0; ++ *modifiers = NULL; ++ return TRUE; ++ } ++ ++ copy = malloc(sizeof(lorieSupportedModifiers)); ++ if (!copy) ++ return FALSE; ++ ++ memcpy(copy, lorieSupportedModifiers, sizeof(lorieSupportedModifiers)); ++ *num_modifiers = sizeof(lorieSupportedModifiers) / sizeof(lorieSupportedModifiers[0]); ++ *modifiers = copy; ++ return TRUE; ++} ++ + static dri3_screen_info_rec lorieDri3Info = { + .version = 2, + .fds_from_pixmap = FalseNoop, + .pixmap_from_fds = loriePixmapFromFds, + .get_formats = lorieGetFormats, + .get_modifiers = lorieGetModifiers, +- .get_drawable_modifiers = FalseNoop ++ .get_drawable_modifiers = lorieGetDrawableModifiers + }; diff --git a/patches/termux-x11/0009-defer-gpu-only-present-while-detached.patch b/patches/termux-x11/0009-defer-gpu-only-present-while-detached.patch index 7e5ccd3..063296b 100644 --- a/patches/termux-x11/0009-defer-gpu-only-present-while-detached.patch +++ b/patches/termux-x11/0009-defer-gpu-only-present-while-detached.patch @@ -1,3 +1,9 @@ +present: defer GPU-only buffers while the renderer is detached + +GPU-only AHardwareBuffers cannot fall back to EXA when the Android renderer is +temporarily unavailable. Retry them on a later fake vblank instead of entering +an invalid CPU mapping path. + --- a/lorie/InitOutput.c +++ b/lorie/InitOutput.c @@ -988,6 +988,19 @@ Bool lorieGpuCopyIsDone(uint64_t serial) { diff --git a/patches/termux-x11/0010-disable-gpu-present-copy.patch b/patches/termux-x11/0010-disable-gpu-present-copy.patch deleted file mode 100644 index 4a5d4bc..0000000 --- a/patches/termux-x11/0010-disable-gpu-present-copy.patch +++ /dev/null @@ -1,12 +0,0 @@ ---- a/InitOutput.c -+++ b/InitOutput.c -@@ -848,6 +848,10 @@ Bool lorieTryScheduleGpuCopy(PixmapPtr pixmap, PixmapPtr dst, RegionPtr update, - uint64_t *out_serial, void **out_dst_buffer) { -+ /* uDroid standard profile: use Xorg's copy path until GPU Present copies are reliable. */ -+ gpuCopyAttempts++; -+ return FALSE; -+ - LorieBuffer *srcBuffer, *dstBuffer; - LoriePixmapPriv *priv; - const LorieBuffer_Desc *desc, *dstDesc; - LorieGpuCopyEntry *entry; diff --git a/patches/termux-x11/0011-lorie-perfetto-slices.patch b/patches/termux-x11/0011-lorie-perfetto-slices.patch new file mode 100644 index 0000000..b3aad6a --- /dev/null +++ b/patches/termux-x11/0011-lorie-perfetto-slices.patch @@ -0,0 +1,78 @@ +lorie: trace draw, fence and swap latency with Perfetto + +Frame counters cannot identify whether rendering, fence waits, or swapping +caused a stall. Add inactive-by-default ATrace slices rather than per-frame log +output. + +--- a/renderer.c ++++ b/renderer.c +@@ -16,6 +16,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -30,6 +31,18 @@ + #define log(...) __android_log_print(ANDROID_LOG_DEBUG, "gles-renderer", __VA_ARGS__) + #define loge(...) __android_log_print(ANDROID_LOG_ERROR, "gles-renderer", __VA_ARGS__) + ++static inline bool traceBegin(const char *name) { ++ bool enabled = ATrace_isEnabled(); ++ if (enabled) ++ ATrace_beginSection(name); ++ return enabled; ++} ++ ++static inline void traceEnd(bool enabled) { ++ if (enabled) ++ ATrace_endSection(); ++} ++ + static GLuint createProgram(const char* p_vertex_source, const char* p_fragment_source); + + static int printEglError(char* msg, int line) { +@@ -890,6 +903,7 @@ void rendererRedrawLocked(bool* waitingForBuffers) { + sourceLeft, sourceTop, logicalSourceWidth, logicalSourceHeight); + + // We should signal X server to not use root window while we actively copy it ++ bool frameTrace = traceBegin("uDroid/Lorie draw"); + lorie_mutex_lock(&state->lock, &state->lockingPid); + // Share this draw's flush+fence below instead of a separate round trip per frame. + uint64_t gpuCopySerial = rendererApplyPendingGpuCopiesLocked(); +@@ -920,7 +934,9 @@ void rendererRedrawLocked(bool* waitingForBuffers) { + glFlush(); + + // Wait until root window drawing is finished before giving control back to X server ++ bool rootFenceTrace = traceBegin("uDroid/Lorie root fence wait"); + eglClientWaitSyncKHR(egl_display, fence, 0, EGL_FOREVER); ++ traceEnd(rootFenceTrace); + eglDestroySyncKHR(egl_display, fence); + if (gpuCopySerial) { + state->gpuCopyQueue.completedSerial = gpuCopySerial; +@@ -929,8 +945,10 @@ void rendererRedrawLocked(bool* waitingForBuffers) { + state->waitForNextFrame = true; + lorie_mutex_unlock(&state->lock, &state->lockingPid); + ++ bool swapTrace = traceBegin("uDroid/Lorie eglSwapBuffers"); + if (eglSwapBuffers(egl_display, sfc) != EGL_TRUE) + printEglError("Failed to swap buffers", __LINE__); ++ traceEnd(swapTrace); + + // Perform a little drawing operation to make sure the next buffer is ready on the next invocation of drawing + glEnable(GL_SCISSOR_TEST); +@@ -939,10 +957,13 @@ void rendererRedrawLocked(bool* waitingForBuffers) { + glClear(GL_COLOR_BUFFER_BIT); + glDisable(GL_SCISSOR_TEST); + fence = eglCreateSyncKHR(egl_display, EGL_SYNC_FENCE_KHR, NULL); ++ bool postSwapFenceTrace = traceBegin("uDroid/Lorie post-swap fence wait"); + eglClientWaitSyncKHR(egl_display, fence, EGL_SYNC_FLUSH_COMMANDS_BIT_KHR, EGL_FOREVER); ++ traceEnd(postSwapFenceTrace); + eglDestroySyncKHR(egl_display, fence); + + state->renderedFrames++; ++ traceEnd(frameTrace); + } + + static inline __always_inline bool rendererShouldWait(bool *waitingForBuffers) { diff --git a/patches/termux-x11/0015-export-pixmaps-through-dri3.patch b/patches/termux-x11/0015-export-pixmaps-through-dri3.patch new file mode 100644 index 0000000..030b887 --- /dev/null +++ b/patches/termux-x11/0015-export-pixmaps-through-dri3.patch @@ -0,0 +1,215 @@ +lorie: export redirected pixmaps through DRI3 + +Implement fds_from_pixmap with aligned, CPU-mappable DMA-heap storage so +compositors can share redirected pixmaps with Mesa instead of falling back to +XGetImage readback. + +diff --git a/InitOutput.c b/InitOutput.c +index 2c7453f..eb1889f 100644 +--- a/InitOutput.c ++++ b/InitOutput.c +@@ -1299,6 +1299,27 @@ static int lorieGetFormats(__unused ScreenPtr screen, CARD32 *num_formats, CARD3 + return TRUE; + } + ++static int lorieFdsFromPixmap(__unused ScreenPtr screen, PixmapPtr pixmap, ++ int *fds, uint32_t *strides, ++ uint32_t *offsets, uint64_t *modifier) { ++ LorieBuffer *buffer = lorieEnsureGpuSampleable(pixmap, LORIEBUFFER_FD); ++ uint32_t size; ++ if (!buffer) ++ return 0; ++ ++ const LorieBuffer_Desc *desc = LorieBuffer_description(buffer); ++ fds[0] = LorieBuffer_exportDmaBuf(buffer, &offsets[0], &size); ++ if (fds[0] < 0) ++ return 0; ++ ++ strides[0] = desc->stride * 4; ++ *modifier = DRM_FORMAT_MOD_LINEAR; ++ if (lorieServerDebugEnabled) ++ log(INFO, "DRI3: exported DMA-BUF pixmap %ux%u stride %u size %u", ++ pixmap->drawable.width, pixmap->drawable.height, strides[0], size); ++ return 1; ++} ++ + static uint64_t lorieSupportedModifiers[] = { + 1255, /* AHardwareBuffer BGRA socket transport */ + 1257, /* AHardwareBuffer RGBA socket transport */ +@@ -1348,7 +1370,7 @@ static int lorieGetDrawableModifiers(__unused DrawablePtr draw, uint32_t format, + + static dri3_screen_info_rec lorieDri3Info = { + .version = 2, +- .fds_from_pixmap = FalseNoop, ++ .fds_from_pixmap = lorieFdsFromPixmap, + .pixmap_from_fds = loriePixmapFromFds, + .get_formats = lorieGetFormats, + .get_modifiers = lorieGetModifiers, +diff --git a/buffer.c b/buffer.c +index f063c79..8ee07db 100644 +--- a/buffer.c ++++ b/buffer.c +@@ -12,6 +12,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -142,6 +143,49 @@ static inline size_t alignToPage(size_t size) { + return (size + page_size - 1) & ~(page_size - 1); + } + ++static inline int32_t alignGpuSampledStride(int32_t width) { ++ const int32_t pixelsPerAlignment = 64 / sizeof(uint32_t); ++ return (width + pixelsPerAlignment - 1) & ~(pixelsPerAlignment - 1); ++} ++ ++static int createDmaHeapRegion(size_t size) { ++ static const char *const heapPaths[] = { ++ "/dev/dma_heap/system-uncached", ++ "/dev/dma_heap/system", ++ }; ++ struct dma_heap_allocation_data allocation = { ++ .len = alignToPage(size), ++ .fd_flags = O_RDWR | O_CLOEXEC, ++ }; ++ ++ for (size_t i = 0; i < sizeof(heapPaths) / sizeof(heapPaths[0]); i++) { ++ int heap = open(heapPaths[i], O_RDONLY | O_CLOEXEC); ++ if (heap < 0) ++ continue; ++ ++ int result; ++ do { ++ result = ioctl(heap, DMA_HEAP_IOCTL_ALLOC, &allocation); ++ } while (result < 0 && errno == EINTR); ++ int savedErrno = errno; ++ close(heap); ++ errno = savedErrno; ++ if (result == 0) ++ return (int) allocation.fd; ++ } ++ ++ return -1; ++} ++ ++static int syncDmaBuf(int fd, uint64_t flags) { ++ struct dma_buf_sync sync = { .flags = flags }; ++ int result; ++ do { ++ result = ioctl(fd, DMA_BUF_IOCTL_SYNC, &sync); ++ } while (result < 0 && errno == EINTR); ++ return result; ++} ++ + #pragma clang diagnostic push + #pragma ide diagnostic ignored "UnreachableCallsOfFunction" + int LorieBuffer_createRegion(char const* name, size_t size) { +@@ -266,12 +308,16 @@ static LorieBuffer* allocate(int32_t width, int32_t stride, int32_t height, int8 + + __LIBC_HIDDEN__ LorieBuffer* LorieBuffer_allocate(int32_t width, int32_t height, int8_t format, int8_t type) { + int fd = -1; ++ int32_t stride = width; + size_t size = 0; + AHardwareBuffer *ahardwarebuffer = NULL; + + if (type == LORIEBUFFER_FD) { +- size = alignToPage(width * height * sizeof(uint32_t)); +- fd = LorieBuffer_createRegion("LorieBuffer", size); ++ stride = alignGpuSampledStride(width); ++ size = alignToPage(stride * height * sizeof(uint32_t)); ++ fd = createDmaHeapRegion(size); ++ if (fd < 0) ++ fd = LorieBuffer_createRegion("LorieBuffer", size); + if (fd < 0) + return NULL; + } else if (type == LORIEBUFFER_AHARDWAREBUFFER) { +@@ -281,7 +327,7 @@ __LIBC_HIDDEN__ LorieBuffer* LorieBuffer_allocate(int32_t width, int32_t height, + dprintf(2, "FATAL: failed to allocate AHardwareBuffer (width %d height %d format %d): error %d\n", width, height, format, err); + } + +- return allocate(width, width, height, format, type, ahardwarebuffer, fd, size, 0, true); ++ return allocate(width, stride, height, format, type, ahardwarebuffer, fd, size, 0, true); + } + + __LIBC_HIDDEN__ LorieBuffer* LorieBuffer_wrapFileDescriptor(int32_t width, int32_t stride, int32_t height, int8_t format, int fd, off_t offset) { +@@ -299,8 +345,12 @@ __LIBC_HIDDEN__ void LorieBuffer_convert(LorieBuffer* buffer, int8_t type, int8_ + return; + + if (type == LORIEBUFFER_FD) { +- size_t size = alignToPage(buffer->desc.stride * buffer->desc.height * sizeof(uint32_t)); +- int fd = LorieBuffer_createRegion("LorieBuffer", size); ++ const int32_t sourceStride = buffer->desc.stride; ++ const int32_t destinationStride = alignGpuSampledStride(buffer->desc.width); ++ size_t size = alignToPage(destinationStride * buffer->desc.height * sizeof(uint32_t)); ++ int fd = createDmaHeapRegion(size); ++ if (fd < 0) ++ fd = LorieBuffer_createRegion("LorieBuffer", size); + if (fd < 0) + return; + +@@ -311,10 +361,18 @@ __LIBC_HIDDEN__ void LorieBuffer_convert(LorieBuffer* buffer, int8_t type, int8_ + return; + } + +- pixman_blt(buffer->desc.data, data, buffer->desc.stride, buffer->desc.stride, 32, 32, 0, 0, 0, 0, buffer->desc.width, buffer->desc.height); ++ bool isDmaBuf = syncDmaBuf(fd, DMA_BUF_SYNC_START | DMA_BUF_SYNC_WRITE) == 0; ++ pixman_blt(buffer->desc.data, data, sourceStride, destinationStride, 32, 32, ++ 0, 0, 0, 0, buffer->desc.width, buffer->desc.height); ++ if (isDmaBuf && syncDmaBuf(fd, DMA_BUF_SYNC_END | DMA_BUF_SYNC_WRITE) < 0) { ++ munmap(data, size); ++ close(fd); ++ return; ++ } + + buffer->desc.type = type; + buffer->desc.format = format; ++ buffer->desc.stride = destinationStride; + buffer->fd = fd; + buffer->size = size; + buffer->offset = 0; +@@ -383,6 +441,27 @@ __LIBC_HIDDEN__ const LorieBuffer_Desc* LorieBuffer_description(LorieBuffer* buf + return buffer ? &buffer->desc : &none; + } + ++__LIBC_HIDDEN__ int LorieBuffer_exportDmaBuf(LorieBuffer* buffer, ++ uint32_t* offset, ++ uint32_t* size) { ++ if (!buffer || !offset || !size || buffer->desc.type != LORIEBUFFER_FD || ++ buffer->fd < 0 || buffer->offset < 0 || buffer->offset > UINT32_MAX || ++ buffer->size > UINT32_MAX) ++ return -1; ++ ++ if (syncDmaBuf(buffer->fd, DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ) < 0) ++ return -1; ++ if (syncDmaBuf(buffer->fd, DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ) < 0) ++ return -1; ++ ++ int fd = fcntl(buffer->fd, F_DUPFD_CLOEXEC, 0); ++ if (fd >= 0) { ++ *offset = (uint32_t) buffer->offset; ++ *size = (uint32_t) buffer->size; ++ } ++ return fd; ++} ++ + __LIBC_HIDDEN__ int LorieBuffer_lock(LorieBuffer* buffer, void** out) { + int ret = 0; + if (!buffer) +diff --git a/buffer.h b/buffer.h +index 751004b..98ac68b 100644 +--- a/buffer.h ++++ b/buffer.h +@@ -112,6 +112,10 @@ STATIC_INLINE void LorieBuffer_release(LorieBuffer* _Nullable buffer) { + */ + const LorieBuffer_Desc* _Nonnull LorieBuffer_description(LorieBuffer* _Nullable buffer); + ++int LorieBuffer_exportDmaBuf(LorieBuffer* _Nullable buffer, ++ uint32_t* _Nonnull offset, ++ uint32_t* _Nonnull size); ++ + /** + * Lock the AHardwareBuffer for direct CPU access. + * See AHardwareBuffer_lock() description for details diff --git a/patches/termux-x11/0016-accept-fd-gpu-copy-sources.patch b/patches/termux-x11/0016-accept-fd-gpu-copy-sources.patch new file mode 100644 index 0000000..f144226 --- /dev/null +++ b/patches/termux-x11/0016-accept-fd-gpu-copy-sources.patch @@ -0,0 +1,32 @@ +lorie: accept DMA-BUF pixmaps as GPU-copy sources + +The renderer accepts both DMA-BUF and AHardwareBuffer storage. Avoid converting +an already usable Venus or Zink pixmap and forcing Present through the CPU +fallback. + +--- a/InitOutput.c ++++ b/InitOutput.c +@@ -135,6 +135,19 @@ static LorieBuffer *lorieEnsureGpuSampleable(PixmapPtr pixmap, int8_t type) { + return desc->type == type ? priv->buffer : NULL; + } + ++static LorieBuffer *lorieGpuCopySource(PixmapPtr pixmap) { ++ LoriePixmapPriv *priv = LORIE_PIXMAP_PRIV_FROM_PIXMAP(pixmap); ++ const LorieBuffer_Desc *desc; ++ if (!priv || !priv->buffer || priv->mem) ++ return NULL; ++ ++ desc = LorieBuffer_description(priv->buffer); ++ if (desc->type == LORIEBUFFER_FD || desc->type == LORIEBUFFER_AHARDWAREBUFFER) ++ return priv->buffer; ++ ++ return lorieEnsureGpuSampleable(pixmap, LORIEBUFFER_AHARDWAREBUFFER); ++} ++ + static Bool lorieServerDebugEnabled = FALSE; + + void OsVendorInit(void) { +@@ -921,2 +934,2 @@ Bool lorieTryScheduleGpuCopy(PixmapPtr pixmap, PixmapPtr dst, RegionPtr update, +- if (!(srcBuffer = lorieEnsureGpuSampleable(pixmap, LORIEBUFFER_AHARDWAREBUFFER)) || ++ if (!(srcBuffer = lorieGpuCopySource(pixmap)) || + !(dstBuffer = lorieEnsureGpuSampleable(dst, LORIEBUFFER_AHARDWAREBUFFER))) { diff --git a/patches/termux-x11/0017-export-ahardwarebuffer-pixmaps-through-dri3.patch b/patches/termux-x11/0017-export-ahardwarebuffer-pixmaps-through-dri3.patch new file mode 100644 index 0000000..f7555bf --- /dev/null +++ b/patches/termux-x11/0017-export-ahardwarebuffer-pixmaps-through-dri3.patch @@ -0,0 +1,176 @@ +lorie: export existing AHardwareBuffers through DRI3 + +Redirected compositor pixmaps may already have GPU-native storage. Duplicate +the underlying DMA-BUF while preserving offset and size instead of replacing +the buffer and forcing a readback. + +--- a/InitOutput.c ++++ b/InitOutput.c +@@ -1333,12 +1333,28 @@ + static int lorieFdsFromPixmap(__unused ScreenPtr screen, PixmapPtr pixmap, + int *fds, uint32_t *strides, + uint32_t *offsets, uint64_t *modifier) { +- LorieBuffer *buffer = lorieEnsureGpuSampleable(pixmap, LORIEBUFFER_FD); ++ LoriePixmapPriv *priv = LORIE_PIXMAP_PRIV_FROM_PIXMAP(pixmap); ++ LorieBuffer *buffer; + uint32_t size; +- if (!buffer) ++ if (!priv || !priv->buffer || priv->mem) + return 0; + ++ /* ++ * Redirected compositor pixmaps are already AHB-backed. Export that same ++ * storage through DRI3; rejecting it forces Mesa into an XGetImage readback. ++ */ ++ buffer = priv->buffer; + const LorieBuffer_Desc *desc = LorieBuffer_description(buffer); ++ if (desc->type == LORIEBUFFER_REGULAR) { ++ buffer = lorieEnsureGpuSampleable(pixmap, LORIEBUFFER_FD); ++ if (!buffer) ++ return 0; ++ desc = LorieBuffer_description(buffer); ++ } ++ if (desc->type != LORIEBUFFER_FD && ++ desc->type != LORIEBUFFER_AHARDWAREBUFFER) ++ return 0; ++ + fds[0] = LorieBuffer_exportDmaBuf(buffer, &offsets[0], &size); + if (fds[0] < 0) + return 0; +--- a/buffer.c ++++ b/buffer.c +@@ -17,6 +17,7 @@ + #include + #include + #include ++#include + #include + #include + #include +@@ -441,12 +441,115 @@ + static const LorieBuffer_Desc none = {0}; + return buffer ? &buffer->desc : &none; + } ++ ++/* ++ * AHardwareBuffer exposes no public direct fd getter. Round-trip the native ++ * handle through the NDK socket API, then accept only a CPU-readable DMA-BUF. ++ */ ++static int exportAHardwareBufferDmaBuf(AHardwareBuffer *buffer, ++ uint32_t minimumSize, ++ uint32_t *size) { ++ enum { MAX_HANDLE_FDS = 16 }; ++ int sockets[2] = {-1, -1}; ++ int receivedFds[MAX_HANDLE_FDS]; ++ size_t receivedFdCount = 0; ++ int selectedFd = -1; ++ ssize_t receivedBytes; ++ char payload[4096]; ++ char control[CMSG_SPACE(sizeof(int) * MAX_HANDLE_FDS)]; ++ struct iovec iov = { ++ .iov_base = payload, ++ .iov_len = sizeof(payload), ++ }; ++ struct msghdr message = { ++ .msg_iov = &iov, ++ .msg_iovlen = 1, ++ .msg_control = control, ++ .msg_controllen = sizeof(control), ++ }; ++ ++ if (!buffer || socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sockets) != 0) ++ return -1; ++ ++ if (AHardwareBuffer_sendHandleToUnixSocket(buffer, sockets[0]) != 0) ++ goto out; ++ close(sockets[0]); ++ sockets[0] = -1; + ++ receivedBytes = recvmsg(sockets[1], &message, MSG_CMSG_CLOEXEC); ++ if (receivedBytes <= 0) ++ goto out; ++ ++ for (struct cmsghdr *header = CMSG_FIRSTHDR(&message); ++ header && receivedFdCount < MAX_HANDLE_FDS; ++ header = CMSG_NXTHDR(&message, header)) { ++ if (header->cmsg_level != SOL_SOCKET || header->cmsg_type != SCM_RIGHTS) ++ continue; ++ ++ size_t count = (header->cmsg_len - CMSG_LEN(0)) / sizeof(int); ++ int *fds = (int *)CMSG_DATA(header); ++ for (size_t i = 0; i < count && receivedFdCount < MAX_HANDLE_FDS; i++) ++ receivedFds[receivedFdCount++] = fds[i]; ++ } ++ ++ /* recvmsg may still install the descriptors that fit before reporting a ++ * truncated control message. Close those descriptors through out rather ++ * than attempting to interpret an incomplete native handle. */ ++ if (message.msg_flags & MSG_CTRUNC) ++ goto out; ++ ++ for (size_t i = 0; i < receivedFdCount; i++) { ++ struct stat info = {0}; ++ int fd = receivedFds[i]; ++ ++ if (fstat(fd, &info) != 0 || info.st_size < minimumSize || ++ info.st_size > UINT32_MAX) ++ continue; ++ if (syncDmaBuf(fd, DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ) != 0) ++ continue; ++ if (syncDmaBuf(fd, DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ) != 0) ++ continue; ++ ++ selectedFd = fd; ++ *size = (uint32_t)info.st_size; ++ receivedFds[i] = -1; ++ break; ++ } ++ ++out: ++ for (size_t i = 0; i < receivedFdCount; i++) ++ if (receivedFds[i] >= 0) ++ close(receivedFds[i]); ++ if (sockets[0] >= 0) ++ close(sockets[0]); ++ if (sockets[1] >= 0) ++ close(sockets[1]); ++ return selectedFd; ++} ++ + __LIBC_HIDDEN__ int LorieBuffer_exportDmaBuf(LorieBuffer* buffer, + uint32_t* offset, + uint32_t* size) { +- if (!buffer || !offset || !size || buffer->desc.type != LORIEBUFFER_FD || +- buffer->fd < 0 || buffer->offset < 0 || buffer->offset > UINT32_MAX || ++ int fd; ++ ++ if (!buffer || !offset || !size) ++ return -1; ++ ++ if (buffer->desc.type == LORIEBUFFER_AHARDWAREBUFFER) { ++ uint64_t minimumSize = (uint64_t)buffer->desc.stride * ++ buffer->desc.height * sizeof(uint32_t); ++ if (minimumSize > UINT32_MAX) ++ return -1; ++ ++ fd = exportAHardwareBufferDmaBuf(buffer->desc.buffer, ++ (uint32_t)minimumSize, size); ++ if (fd >= 0) ++ *offset = 0; ++ return fd; ++ } ++ ++ if (buffer->desc.type != LORIEBUFFER_FD || buffer->fd < 0 || ++ buffer->offset < 0 || buffer->offset > UINT32_MAX || + buffer->size > UINT32_MAX) + return -1; + +@@ -455,7 +554,7 @@ + if (syncDmaBuf(buffer->fd, DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ) < 0) + return -1; + +- int fd = fcntl(buffer->fd, F_DUPFD_CLOEXEC, 0); ++ fd = fcntl(buffer->fd, F_DUPFD_CLOEXEC, 0); + if (fd >= 0) { + *offset = (uint32_t) buffer->offset; + *size = (uint32_t) buffer->size; diff --git a/patches/termux-x11/0018-present-damage-ordering.patch b/patches/termux-x11/0018-present-damage-ordering.patch new file mode 100644 index 0000000..8a4274c --- /dev/null +++ b/patches/termux-x11/0018-present-damage-ordering.patch @@ -0,0 +1,198 @@ +present: order damage and ownership around async GPU copies + +Keep damage pending until the queued copy completes, drain earlier GPU work +before a CPU fallback, and translate damage into screen coordinates. Increase +the bounded damage list to 64 rectangles and use acquire/release ordering for +completion state. + +Adapted from termux/termux-x11#1148 commits 0995bdc, e32828e, bac0460, +4eb76e3, and 1346e1e. + +--- a/lorie/lorie.h ++++ b/lorie/lorie.h +@@ -177,7 +177,7 @@ + + typedef struct { int16_t x1, y1, x2, y2; } LorieGpuCopyRect; + +-#define LORIE_GPU_COPY_MAX_RECTS 16 ++#define LORIE_GPU_COPY_MAX_RECTS 64 + #define LORIE_GPU_COPY_QUEUE_CAPACITY 8 + + typedef struct { +--- a/lorie/InitOutput.c ++++ b/lorie/InitOutput.c +@@ -152,7 +152,7 @@ + + typedef struct { + LorieBuffer *buffer; +- bool flipped, wasLocked, imported; ++ bool flipped, wasLocked, imported, gpuOnly; + void *locked; + void *mem; + } LoriePixmapPriv; +@@ -1025,20 +1025,21 @@ + } + + Bool lorieGpuCopyIsDone(uint64_t serial) { +- return pvfb->state->gpuCopyQueue.completedSerial >= serial; ++ return __atomic_load_n(&pvfb->state->gpuCopyQueue.completedSerial, __ATOMIC_ACQUIRE) >= serial; + } + +-Bool loriePixmapRequiresGpuCopy(PixmapPtr pixmap) { +- LoriePixmapPriv *priv = pixmap ? LORIE_PIXMAP_PRIV_FROM_PIXMAP(pixmap) : NULL; +- const LorieBuffer_Desc *desc = priv ? LorieBuffer_description(priv->buffer) : NULL; ++void lorieGpuCopyFlush(void) { ++ uint64_t serial = pvfb->gpuCopySerialCounter; ++ for (int i = 0; i < 500 && !lorieGpuCopyIsDone(serial); i++) { ++ if (!lorieConnectionAlive()) ++ return; ++ usleep(200); ++ } ++} + +- /* A DRI3 AHardwareBuffer may be allocated without CPU read/write usage. In +- * particular, gfxstream scanout images are valid GPU textures but Android +- * gralloc rejects AHardwareBuffer_lock() for them. Never let a temporarily +- * detached renderer turn that expected lifecycle state into a CPU-copy +- * fallback; Present will wait for the renderer to reconnect instead. */ +- return priv && priv->imported && desc && +- desc->type == LORIEBUFFER_AHARDWAREBUFFER; ++Bool loriePixmapIsGpuOnly(PixmapPtr pixmap) { ++ LoriePixmapPriv *priv = pixmap ? LORIE_PIXMAP_PRIV_FROM_PIXMAP(pixmap) : NULL; ++ return priv && priv->gpuOnly; + } + + void lorieGpuCopyAck(PixmapPtr pixmap, void *dst_buffer) { +@@ -1258,9 +1259,13 @@ + if (modifier == AHARDWAREBUFFER_RGBA_SOCKET_FD) + LorieBuffer_setNeedsBgraSwizzle(priv->buffer, true); + ++ void *probe = NULL; ++ priv->gpuOnly = LorieBuffer_lock(priv->buffer, &probe) != 0; ++ LorieBuffer_unlock(priv->buffer); ++ + screen->ModifyPixmapHeader(pixmap, desc.width, desc.height, 0, 0, desc.stride * 4, NULL); + if (lorieServerDebugEnabled) +- log(INFO, "DRI3: imported AHardwareBuffer, modifier %llu, %ux%u stride %u", (unsigned long long) modifier, desc.width, desc.height, desc.stride); ++ log(INFO, "DRI3: imported AHardwareBuffer, modifier %llu, %ux%u stride %u%s", (unsigned long long) modifier, desc.width, desc.height, desc.stride, priv->gpuOnly ? ", not lockable" : ""); + } + + return pixmap; +--- a/xserver/present/present_priv.h ++++ b/xserver/present/present_priv.h +@@ -97,6 +97,7 @@ + Bool gpu_copy_pending; + uint64_t gpu_copy_serial; + void *gpu_copy_dst_buffer; /* opaque LorieBuffer*, held for lorieGpuCopyAck */ ++ RegionPtr gpu_copy_damage; /* reported as damaged once the GPU copy has finished */ + }; + + /* lorie: hooks implemented in lorie/InitOutput.c, used by present_execute_copy() to offload the +@@ -106,7 +107,9 @@ + uint64_t *out_serial, void **out_dst_buffer); + extern Bool lorieGpuCopyIsDone(uint64_t serial); + extern void lorieGpuCopyAck(PixmapPtr pixmap, void *dst_buffer); +-extern Bool loriePixmapRequiresGpuCopy(PixmapPtr pixmap); ++/* Wait for queued GPU copies before a later CPU copy can overwrite their destination. */ ++extern void lorieGpuCopyFlush(void); ++extern Bool loriePixmapIsGpuOnly(PixmapPtr pixmap); + /* lorie: implemented in lorie/cmdentrypoint.c. Avoids waiting forever on a GPU copy that a dead renderer will never finish. */ + extern bool lorieConnectionAlive(void); + +--- a/xserver/present/present_execute.c ++++ b/xserver/present/present_execute.c +@@ -96,16 +96,28 @@ + if (vblank->gpu_copy_pending) { + lorieGpuCopyAck(vblank->pixmap, vblank->gpu_copy_dst_buffer); + vblank->gpu_copy_pending = FALSE; ++ if (vblank->gpu_copy_damage) { ++ DamageDamageRegion(&window->drawable, vblank->gpu_copy_damage); ++ RegionDestroy(vblank->gpu_copy_damage); ++ vblank->gpu_copy_damage = NULL; ++ } + } else if (lorieTryScheduleGpuCopy(vblank->pixmap, gpuCopyDst, vblank->update, gpuCopyXOff, gpuCopyYOff, + &vblank->gpu_copy_serial, &vblank->gpu_copy_dst_buffer)) { + /* lorie: our GPU blit writes the destination pixmap directly, bypassing the normal GC + * ops that Damage tracking hooks into, so report it manually - same drawable and region + * present_scmd.c's flip success path already uses, so this also reaches Composite's +- * per-window damage for redirected windows, not just lorie's own root damage. */ +- RegionPtr damage = vblank->update ? vblank->update : &window->clipList; +- if (vblank->update) +- RegionIntersect(damage, damage, &window->clipList); +- DamageDamageRegion(&window->drawable, damage); ++ * per-window damage for redirected windows, not just lorie's own root damage. ++ * vblank->update is in pixmap coordinates, damage and clipList are in screen coordinates. */ ++ RegionPtr damage = RegionDuplicate(vblank->update ? vblank->update : &window->clipList); ++ if (damage) { ++ if (vblank->update) { ++ RegionTranslate(damage, vblank->x_off + window->drawable.x, vblank->y_off + window->drawable.y); ++ RegionIntersect(damage, damage, &window->clipList); ++ } ++ DamageDamageRegion(&window->drawable, damage); ++ /* A compositor may read the destination before the GPU copy completes. */ ++ vblank->gpu_copy_damage = damage; ++ } + + /* lorie: copy offloaded to the renderer's GPU context. The region was already + * consumed (copied into the shared command queue), so free it like +@@ -122,17 +134,25 @@ + /* Failed to requeue for polling (e.g. OOM) - release our extra ref and treat the + * presumably still in-flight GPU copy as done; same best-effort fallback as above. */ + lorieGpuCopyAck(vblank->pixmap, vblank->gpu_copy_dst_buffer); +- } else if (loriePixmapRequiresGpuCopy(vblank->pixmap)) { ++ if (vblank->gpu_copy_damage) { ++ RegionDestroy(vblank->gpu_copy_damage); ++ vblank->gpu_copy_damage = NULL; ++ } ++ } else if (loriePixmapIsGpuOnly(vblank->pixmap)) { + /* Imported AHardwareBuffers are not guaranteed to be CPU-mappable. If + * the Android renderer is detached or its queue is temporarily full, + * preserve the Present request until a later fake-vblank instead of + * entering EXA's CPU path and fatally failing PrepareAccess. */ + if (Success == screen_priv->queue_vblank(screen, window, vblank->crtc, + vblank->event_id, crtc_msc + 1)) { + vblank->queued = TRUE; + return; + } ++ present_vblank_scrap(vblank); ++ screen_priv->flush(window); ++ return; + } else { ++ lorieGpuCopyFlush(); + present_copy_region(&window->drawable, vblank->pixmap, vblank->update, vblank->x_off, vblank->y_off); + + /* present_copy_region sticks the region into a scratch GC, +--- a/xserver/present/present_vblank.c ++++ b/xserver/present/present_vblank.c +@@ -188,6 +188,15 @@ + vblank->pixmap->drawable.id, vblank->window->drawable.id, + vblank->crtc)); + ++ if (vblank->gpu_copy_pending) { ++ lorieGpuCopyAck(vblank->pixmap, vblank->gpu_copy_dst_buffer); ++ vblank->gpu_copy_pending = FALSE; ++ if (vblank->gpu_copy_damage) { ++ RegionDestroy(vblank->gpu_copy_damage); ++ vblank->gpu_copy_damage = NULL; ++ } ++ } ++ + present_pixmap_idle(vblank->pixmap, vblank->window, vblank->serial, vblank->idle_fence); + present_fence_destroy(vblank->idle_fence); + dixDestroyPixmap(vblank->pixmap, vblank->pixmap->drawable.id); +@@ -200,6 +209,15 @@ + void + present_vblank_destroy(present_vblank_ptr vblank) + { ++ if (vblank->gpu_copy_pending) { ++ lorieGpuCopyAck(vblank->pixmap, vblank->gpu_copy_dst_buffer); ++ vblank->gpu_copy_pending = FALSE; ++ if (vblank->gpu_copy_damage) { ++ RegionDestroy(vblank->gpu_copy_damage); ++ vblank->gpu_copy_damage = NULL; ++ } ++ } ++ + /* Remove vblank from window and screen lists */ + xorg_list_del(&vblank->window_list); + /* Also make sure vblank is removed from event queue (wnmd) */ diff --git a/patches/termux-x11/0019-dmabuf-exa-cpu-access-sync.patch b/patches/termux-x11/0019-dmabuf-exa-cpu-access-sync.patch new file mode 100644 index 0000000..48e5e78 --- /dev/null +++ b/patches/termux-x11/0019-dmabuf-exa-cpu-access-sync.patch @@ -0,0 +1,90 @@ +exa: synchronize CPU access to DMA-BUF pixmaps + +DRI3 clients may render with the GPU while Xorg reads or writes the same mapped +storage. Emit matching DMA_BUF_SYNC_START and END ioctls around the outermost +EXA access to prevent partially updated rows. + +Based on termux/termux-x11@d85d65c7809c53c1707cb771fa8e4e645cc9f3ea. + +--- a/buffer.h ++++ b/buffer.h +@@ -115,6 +115,8 @@ const LorieBuffer_Desc* _Nonnull LorieBuffer_description(LorieBuffer* _Nullable + int LorieBuffer_exportDmaBuf(LorieBuffer* _Nullable buffer, + uint32_t* _Nonnull offset, + uint32_t* _Nonnull size); ++bool LorieBuffer_cpuAccessBegin(LorieBuffer* _Nullable buffer, bool write); ++void LorieBuffer_cpuAccessEnd(LorieBuffer* _Nullable buffer, bool write); + + /** + * Lock the AHardwareBuffer for direct CPU access. +--- a/buffer.c ++++ b/buffer.c +@@ -548,6 +548,30 @@ __LIBC_HIDDEN__ int LorieBuffer_exportDmaBuf(LorieBuffer* buffer, + return fd; + } + ++__LIBC_HIDDEN__ bool LorieBuffer_cpuAccessBegin(LorieBuffer* buffer, bool write) { ++ if (!buffer || buffer->desc.type != LORIEBUFFER_FD || ++ buffer->dmaBufSyncSupport == DMA_BUF_SYNC_UNSUPPORTED) ++ return false; ++ ++ if (syncDmaBuf(buffer->fd, DMA_BUF_SYNC_START | DMA_BUF_SYNC_READ | ++ (write ? DMA_BUF_SYNC_WRITE : 0)) == 0) { ++ buffer->dmaBufSyncSupport = DMA_BUF_SYNC_SUPPORTED; ++ return true; ++ } ++ ++ int error = errno; ++ if (buffer->dmaBufSyncSupport == DMA_BUF_SYNC_UNKNOWN && ++ (error == ENOTTY || error == EINVAL || error == ENOSYS)) { ++ buffer->dmaBufSyncSupport = DMA_BUF_SYNC_UNSUPPORTED; ++ } ++ return false; ++} ++ ++__LIBC_HIDDEN__ void LorieBuffer_cpuAccessEnd(LorieBuffer* buffer, bool write) { ++ syncDmaBuf(buffer->fd, DMA_BUF_SYNC_END | DMA_BUF_SYNC_READ | ++ (write ? DMA_BUF_SYNC_WRITE : 0)); ++} ++ + __LIBC_HIDDEN__ int LorieBuffer_lock(LorieBuffer* buffer, void** out) { + int ret = 0; + if (!buffer) +--- a/InitOutput.c ++++ b/InitOutput.c +@@ -112,6 +112,8 @@ static pthread_cond_t* volatile rendererCond = &rendererCondPlaceholder; + typedef struct { + LorieBuffer *buffer; + bool flipped, wasLocked, imported, gpuOnly; ++ uint8_t dmaBufAccessDepth; ++ bool dmaBufAccessWrite; + void *locked; + void *mem; + } LoriePixmapPriv; +@@ -1170,6 +1172,14 @@ Bool loriePrepareAccess(PixmapPtr pPix, int index) { + } else + priv->wasLocked = TRUE; + ++ if (priv->buffer && !priv->mem) { ++ bool write = index == EXA_PREPARE_DEST || index == EXA_PREPARE_AUX_DEST; ++ if (priv->dmaBufAccessDepth || LorieBuffer_cpuAccessBegin(priv->buffer, write)) { ++ priv->dmaBufAccessDepth++; ++ priv->dmaBufAccessWrite |= write; ++ } ++ } ++ + pPix->devPrivate.ptr = priv->locked ?: priv->mem; + return TRUE; + } +@@ -1176,6 +1186,11 @@ void lorieFinishAccess(PixmapPtr pPix, int index) { + if (lorieNeedsGpuLock(pPix, priv, index)) + lorie_mutex_unlock(&pvfb->state->lock, &pvfb->state->lockingPid); + ++ if (priv->dmaBufAccessDepth && !--priv->dmaBufAccessDepth) { ++ LorieBuffer_cpuAccessEnd(priv->buffer, priv->dmaBufAccessWrite); ++ priv->dmaBufAccessWrite = false; ++ } ++ + if (!priv->wasLocked) { + LorieBuffer_unlock(priv->buffer); + priv->locked = NULL; diff --git a/patches/termux-x11/0020-renderer-route-capability-probe.patch b/patches/termux-x11/0020-renderer-route-capability-probe.patch new file mode 100644 index 0000000..8a63ba7 --- /dev/null +++ b/patches/termux-x11/0020-renderer-route-capability-probe.patch @@ -0,0 +1,39 @@ +lorie: report renderer import capabilities at startup + +Emit one structured record with the EGL and GLES extensions needed to identify +the DMA-BUF, AHardwareBuffer, or CPU-upload route. The probe does not alter +route selection. + +--- a/renderer.c ++++ b/renderer.c +@@ -298,6 +298,30 @@ int rendererInitThread(void* cookie) { + eglMakeCurrent(egl_display, sfc, sfc, ctx); + eglSwapInterval(egl_display, 0); + ++ const char *eglVendor = eglQueryString(egl_display, EGL_VENDOR); ++ const char *eglVersion = eglQueryString(egl_display, EGL_VERSION); ++ const char *eglExtensions = eglQueryString(egl_display, EGL_EXTENSIONS); ++ const char *glExtensions = (const char *) glGetString(GL_EXTENSIONS); ++ bool dmaBufImport = eglExtensions && strstr(eglExtensions, "EGL_EXT_image_dma_buf_import"); ++ bool dmaBufModifiers = eglExtensions && strstr(eglExtensions, "EGL_EXT_image_dma_buf_import_modifiers"); ++ bool nativeBuffer = eglExtensions && strstr(eglExtensions, "EGL_ANDROID_image_native_buffer"); ++ bool nativeClientBuffer = eglExtensions && strstr(eglExtensions, "EGL_ANDROID_get_native_client_buffer"); ++ bool nativeFence = eglExtensions && strstr(eglExtensions, "EGL_ANDROID_native_fence_sync"); ++ bool waitSync = eglExtensions && strstr(eglExtensions, "EGL_KHR_wait_sync"); ++ bool imageBase = eglExtensions && strstr(eglExtensions, "EGL_KHR_image_base"); ++ bool externalImage = glExtensions && strstr(glExtensions, "GL_OES_EGL_image_external"); ++ bool ahardwareBufferImport = nativeBuffer && nativeClientBuffer && imageBase && externalImage; ++ const char *route = dmaBufImport && imageBase ? "egl-dmabuf" : ++ ahardwareBufferImport ? "ahardwarebuffer" : "cpu-upload"; ++ const char *fallback = dmaBufImport && imageBase ? "none" : ++ ahardwareBufferImport ? "direct DMA-BUF import unavailable" : ++ "no GPU-native import route"; ++ ++ log("Xlorie: G0 graphics contract vendor=\"%s\" version=\"%s\" dma_buf_import=%d dma_buf_modifiers=%d native_buffer=%d native_client_buffer=%d native_fence=%d wait_sync=%d image_base=%d external_image=%d eligible_route=%s fallback=\"%s\"", ++ eglVendor ?: "unknown", eglVersion ?: "unknown", dmaBufImport, dmaBufModifiers, ++ nativeBuffer, nativeClientBuffer, nativeFence, waitSync, imageBase, externalImage, ++ route, fallback); ++ + g_texture_program = createProgram(vertexShaderSrc, fragmentShaderSrc); + if (!g_texture_program) + log("Xlorie: GLESv2: Unable to create shader program.\n"); diff --git a/patches/termux-x11/0021-preserve-logical-ahb-pixmap-size.patch b/patches/termux-x11/0021-preserve-logical-ahb-pixmap-size.patch new file mode 100644 index 0000000..00c402e --- /dev/null +++ b/patches/termux-x11/0021-preserve-logical-ahb-pixmap-size.patch @@ -0,0 +1,32 @@ +lorie: preserve logical pixmap dimensions for padded AHBs + +Android allocators may enlarge the physical allocation or stride. Validate that +storage covers the requested image, but keep the DRI3 dimensions as the logical +X pixmap size so padding is never exposed as content. + +--- a/InitOutput.c ++++ b/InitOutput.c +@@ -1268,6 +1268,9 @@ static PixmapPtr loriePixmapFromFds(ScreenPtr screen, CARD8 num_fds, const int + && desc.format != AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM + && desc.format != AHARDWAREBUFFER_FORMAT_B8G8R8A8_UNORM, + "DRI3: AHARDWAREBUFFER_SOCKET_FD: wrong format of AHardwareBuffer. Must be one of: AHARDWAREBUFFER_FORMAT_R8G8B8X8_UNORM, AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM, AHARDWAREBUFFER_FORMAT_B8G8R8A8_UNORM (stands for 5)."); ++ check(desc.width < width || desc.height < height, ++ "DRI3: AHardwareBuffer %ux%u is smaller than pixmap %ux%u", ++ desc.width, desc.height, width, height); + check(!(priv->buffer = LorieBuffer_wrapAHardwareBuffer(received_buffer)), "DRI3: LorieBuffer_wrapAHardwareBuffer failed."); + received_buffer = NULL; + if (modifier == AHARDWAREBUFFER_RGBA_SOCKET_FD) +@@ -1278,9 +1281,11 @@ static PixmapPtr loriePixmapFromFds(ScreenPtr screen, CARD8 num_fds, const int + priv->gpuOnly = LorieBuffer_lock(priv->buffer, &probe) != 0; + LorieBuffer_unlock(priv->buffer); + +- screen->ModifyPixmapHeader(pixmap, desc.width, desc.height, 0, 0, desc.stride * 4, NULL); ++ /* The AHB may contain physical padding hidden from the X pixmap. */ ++ screen->ModifyPixmapHeader(pixmap, width, height, 0, 0, desc.stride * 4, NULL); + if (lorieServerDebugEnabled) +- log(INFO, "DRI3: imported AHardwareBuffer, modifier %llu, %ux%u stride %u%s", (unsigned long long) modifier, desc.width, desc.height, desc.stride, priv->gpuOnly ? ", not lockable" : ""); ++ log(INFO, "DRI3: imported AHardwareBuffer, modifier %llu, logical %ux%u physical %ux%u stride %u%s", ++ (unsigned long long) modifier, width, height, desc.width, desc.height, desc.stride, priv->gpuOnly ? ", not lockable" : ""); + } + + return pixmap; diff --git a/patches/termux-x11/0022-disable-incoherent-ahb-gpu-present.patch b/patches/termux-x11/0022-disable-incoherent-ahb-gpu-present.patch new file mode 100644 index 0000000..570b674 --- /dev/null +++ b/patches/termux-x11/0022-disable-incoherent-ahb-gpu-present.patch @@ -0,0 +1,141 @@ +lorie: reject incoherent AHB GPU Present paths + +Probe whether CPU and GLES observe the same AHardwareBuffer contents before +enabling GPU Present. Fall back to the synchronized CPU path when they do not, +without maintaining a vendor or device blacklist. + +Based on termux/termux-x11@501f72f8e1183e62925951551bf631586145c1e6 +from termux/termux-x11#1148. + +diff --git a/InitOutput.c b/InitOutput.c +--- a/InitOutput.c ++++ b/InitOutput.c +@@ -857,7 +857,7 @@ void InitOutput(ScreenInfo * screen_info, int argc, char **argv) { + screen_info->bitmapBitOrder = BITMAP_BIT_ORDER; + screen_info->numPixmapFormats = ARRAY_SIZE(depths); + +- rendererTestCapabilities(&pvfb->root.legacyDrawing); ++ rendererTestCapabilities(&pvfb->root.legacyDrawing, &pvfb->gpuPresentDisabled); + xorgGlxCreateVendor(); + lorieInitClipboard(); + +diff --git a/lorie.h b/lorie.h +--- a/lorie.h ++++ b/lorie.h +@@ -42,7 +42,7 @@ int rendererGetWakeupCondFd(void); + + __unused void rendererInit(JNIEnv* env); + __unused void rendererSetFiltering(JNIEnv* env, jobject self, jint filtering); +-__unused void rendererTestCapabilities(int* legacy_drawing); ++__unused void rendererTestCapabilities(int* legacy_drawing, int* gpu_present_disabled); + __unused void rendererSetWindow(JNIEnv *env, jobject thiz, jobject sfc); + __unused void rendererSetViewport(JNIEnv *env, jclass clazz, int x, int y, int w, int h, int ew, int eh); + __unused void rendererSetZoom(JNIEnv *env, jclass clazz, int percent); +diff --git a/renderer.c b/renderer.c +--- a/renderer.c ++++ b/renderer.c +@@ -415,7 +415,81 @@ void rendererSetFiltering(JNIEnv* env, jobject self, jint f) { + filtering = f; + } + +-void rendererTestCapabilities(int* legacy_drawing) { ++// Present GPU offload writes this buffer while Xorg keeps its CPU mapping locked. ++// Verify both views refer to coherent memory instead of guessing from the driver name. ++static bool cpuAndGpuShareAhbMemory(EGLDisplay display) { ++ const EGLint imageAttributes[] = {EGL_IMAGE_PRESERVED_KHR, EGL_TRUE, EGL_NONE}; ++ AHardwareBuffer_Desc desc = { ++ .width = 64, ++ .height = 64, ++ .layers = 1, ++ .format = AHARDWAREBUFFER_FORMAT_R8G8B8X8_UNORM, ++ .usage = AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | AHARDWAREBUFFER_USAGE_GPU_FRAMEBUFFER | ++ AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN | AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN ++ }; ++ AHardwareBuffer *buffer = NULL; ++ EGLImageKHR image = EGL_NO_IMAGE_KHR; ++ GLuint texture = 0, fbo = 0; ++ volatile uint32_t *pixels = NULL; ++ bool result = true; ++ ++ if (AHardwareBuffer_allocate(&desc, &buffer) != 0 || !buffer) ++ return true; ++ ++ EGLClientBuffer clientBuffer = eglGetNativeClientBufferANDROID(buffer); ++ if (clientBuffer) ++ image = eglCreateImageKHR(display, EGL_NO_CONTEXT, EGL_NATIVE_BUFFER_ANDROID, clientBuffer, imageAttributes); ++ if (image == EGL_NO_IMAGE_KHR) ++ goto out; ++ ++ glGenTextures(1, &texture); ++ glBindTexture(GL_TEXTURE_2D, texture); ++ glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, image); ++ glGenFramebuffers(1, &fbo); ++ glBindFramebuffer(GL_FRAMEBUFFER, fbo); ++ glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, texture, 0); ++ if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) ++ goto out; ++ ++ if (AHardwareBuffer_lock(buffer, AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN | AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN, ++ -1, NULL, (void **) &pixels) != 0 || !pixels) ++ goto out; ++ pixels[0] = 0; ++ ++ glClearColor(0x11 / 255.f, 0x22 / 255.f, 0x33 / 255.f, 1.f); ++ glClear(GL_COLOR_BUFFER_BIT); ++ glFinish(); ++ ++ if ((pixels[0] & 0xffffff) != 0x332211) { ++ loge("CPU mapping does not see GPU writes (got %08x)", pixels[0]); ++ AHardwareBuffer_unlock(buffer, NULL); ++ result = false; ++ goto out; ++ } ++ ++ AHardwareBuffer_unlock(buffer, NULL); ++ { ++ uint8_t pixel[4] = {0, 0, 0, 0}; ++ glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel); ++ if (pixel[0] != 0x11 || pixel[1] != 0x22 || pixel[2] != 0x33) { ++ loge("CPU unlock overwrote GPU writes (got %02x%02x%02x)", pixel[0], pixel[1], pixel[2]); ++ result = false; ++ } ++ } ++ ++out: ++ glBindFramebuffer(GL_FRAMEBUFFER, 0); ++ if (fbo) ++ glDeleteFramebuffers(1, &fbo); ++ if (texture) ++ glDeleteTextures(1, &texture); ++ if (image != EGL_NO_IMAGE_KHR) ++ eglDestroyImageKHR(display, image); ++ AHardwareBuffer_release(buffer); ++ return result; ++} ++ ++void rendererTestCapabilities(int* legacy_drawing, int* gpu_present_disabled) { + // Some devices do not support sampling from HAL_PIXEL_FORMAT_BGRA_8888, here we are checking it. + const EGLint imageAttributes[] = {EGL_IMAGE_PRESERVED_KHR, EGL_TRUE, EGL_NONE}; + EGLint numConfigs; +@@ -428,7 +502,8 @@ void rendererTestCapabilities(int* legacy_drawing) { + .width = 64, + .height = 64, + .layers = 1, +- .usage = AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN | AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN, ++ .usage = AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE | AHARDWAREBUFFER_USAGE_GPU_FRAMEBUFFER | ++ AHARDWAREBUFFER_USAGE_CPU_WRITE_OFTEN | AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN, + .format = AHARDWAREBUFFER_FORMAT_R8G8B8X8_UNORM + }; + +@@ -503,6 +578,11 @@ void rendererTestCapabilities(int* legacy_drawing) { + if (eglMakeCurrent(egl_display, checksfc, checksfc, testctx) != EGL_TRUE) + return vprintEglError("check eglMakeCurrent failed", __LINE__); + ++ if (!*gpu_present_disabled && !cpuAndGpuShareAhbMemory(egl_display)) { ++ loge("CPU and GPU views of AHardwareBuffer are not coherent on this device, disabling Present GPU offload"); ++ *gpu_present_disabled = 1; ++ } ++ + glActiveTexture(GL_TEXTURE0); checkGlError(); + glGenTextures(1, &texture); checkGlError(); + bindTexture(texture); diff --git a/patches/termux-x11/0023-transfer-ahb-present-ownership.patch b/patches/termux-x11/0023-transfer-ahb-present-ownership.patch new file mode 100644 index 0000000..8f0c0dd --- /dev/null +++ b/patches/termux-x11/0023-transfer-ahb-present-ownership.patch @@ -0,0 +1,78 @@ +lorie: transfer AHardwareBuffer ownership around GPU Present + +AHardwareBuffer does not permit GPU writes while a CPU mapping is locked. +The previous capability probe exercised that invalid overlap, so conforming +devices disabled every GPU Present copy even though the renderer already +fences completed work. + +End the CPU mapping before the probe writes through GLES, then reacquire it +after glFinish to verify the result. Likewise release an idle destination +mapping before queuing a Present copy. EXA PrepareAccess reacquires the mapping +for later CPU work under the existing renderer state lock. + +diff --git a/InitOutput.c b/InitOutput.c +--- a/InitOutput.c ++++ b/InitOutput.c +@@ -963,11 +963,16 @@ Bool lorieTryScheduleGpuCopy(PixmapPtr pixmap, PixmapPtr dst, RegionPtr update, + desc = LorieBuffer_description(srcBuffer); + dstDesc = LorieBuffer_description(dstBuffer); + +- /* AHardwareBuffer_lock() does not permit a concurrent GPU write. */ ++ /* Transfer an idle CPU mapping back to the GPU before queuing the write. */ + if (dstDesc->type == LORIEBUFFER_AHARDWAREBUFFER && dstPriv && dstPriv->locked) { +- gpuCopyAttempts++; +- gpuCopyDisabled++; +- return FALSE; ++ if (LorieBuffer_unlock(dstPriv->buffer) != 0) { ++ gpuCopyAttempts++; ++ gpuCopyDisabled++; ++ return FALSE; ++ } ++ dstPriv->locked = NULL; ++ dstPriv->wasLocked = false; ++ dst->devPrivate.ptr = NULL; + } + + if (update) { +diff --git a/renderer.c b/renderer.c +--- a/renderer.c ++++ b/renderer.c +@@ -457,19 +457,25 @@ static bool cpuAndGpuShareAhbMemory(EGLDisplay display) { + -1, NULL, (void **) &pixels) != 0 || !pixels) + goto out; + pixels[0] = 0; ++ AHardwareBuffer_unlock(buffer, NULL); ++ pixels = NULL; + + glClearColor(0x11 / 255.f, 0x22 / 255.f, 0x33 / 255.f, 1.f); + glClear(GL_COLOR_BUFFER_BIT); + glFinish(); + +- if ((pixels[0] & 0xffffff) != 0x332211) { +- loge("CPU mapping does not see GPU writes (got %08x)", pixels[0]); +- AHardwareBuffer_unlock(buffer, NULL); ++ if (AHardwareBuffer_lock(buffer, AHARDWAREBUFFER_USAGE_CPU_READ_OFTEN, ++ -1, NULL, (void **) &pixels) != 0 || !pixels) ++ goto out; ++ uint32_t gpuPixel = pixels[0]; ++ AHardwareBuffer_unlock(buffer, NULL); ++ pixels = NULL; ++ if ((gpuPixel & 0xffffff) != 0x332211) { ++ loge("CPU mapping does not see GPU writes (got %08x)", gpuPixel); + result = false; + goto out; + } + +- AHardwareBuffer_unlock(buffer, NULL); + { + uint8_t pixel[4] = {0, 0, 0, 0}; + glReadPixels(0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel); +@@ -482,6 +489,8 @@ static bool cpuAndGpuShareAhbMemory(EGLDisplay display) { + } + + out: ++ if (pixels) ++ AHardwareBuffer_unlock(buffer, NULL); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + if (fbo) + glDeleteFramebuffers(1, &fbo); diff --git a/patches/termux-x11/README.md b/patches/termux-x11/README.md new file mode 100644 index 0000000..9650c07 --- /dev/null +++ b/patches/termux-x11/README.md @@ -0,0 +1,73 @@ +# Embedded Termux:X11 patch stack + +`tools/prepare-termux-x11-source.sh` applies the production series to pinned +Termux:X11 commit `0cb0203`. Order is part of the ABI: guest producers, Xorg, +Lorie, and the Android renderer must agree on buffer type, channel order, +ownership, damage, and synchronization. + +Each patch has a kernel-style subject and a short explanation of the problem it +solves. Production patches live here; noisy probes live in `diagnostics/`; +unsafe or incomplete work lives in `experiments/` and is never applied +automatically. + +## Patch inventory + +The table names the patches; `tools/prepare-termux-x11-source.sh` is the +authoritative application order because some later-numbered patches depend on +Xserver changes applied outside `lorie/`. + +| Patch | Purpose | Applied by default | +| --- | --- | --- | +| `0000` | Makes upstream nested Xserver patching idempotent when later uDroid patches extend the same files. | Yes | +| `0001` | Restores Android's `xlocale.h` include order for the embedded libX11 build. | Yes | +| `0002` | Replaces Termux:X11's JNI server entry point with uDroid's native bridge. | Yes | +| `0003` | Connects the embedded renderer and X server through uDroid's JNI bridge. | Yes | +| `0004` | Batches touch frames so one Android gesture reaches X11 as one ordered update. | Yes | +| `0005` | Synchronizes DMA-BUF CPU reads used by GLES upload fallback. | Yes | +| `0006` | Samples imported AHardwareBuffers as external textures instead of assuming ordinary 2D texture storage. | Yes | +| `0007` | Defines private RGBA/BGRA content semantics, bounds socket handshakes, and preserves AHardwareBuffer ownership on failure. | Yes | +| `0008` | Publishes the private buffer-transport contract on the X11 root window. | Yes | +| `0009` | Defers GPU-only Present work while the Android renderer is detached. | Yes | +| `0011` | Adds low-overhead Perfetto slices around draw, fence, and swap operations. | Yes | +| `0015` | Exports CPU-mappable redirected pixmaps through DRI3 using aligned DMA-heap storage. | Yes | +| `0016` | Accepts existing DMA-BUF and AHardwareBuffer pixmaps as GPU-copy sources instead of forcing a CPU fallback. | Yes | +| `0017` | Exports existing AHardwareBuffer pixmaps through DRI3 without replacing their storage. | Yes | +| `0018` | Orders Present damage, ownership, GPU copies, and CPU fallback. | Yes | +| `0019` | Synchronizes nested EXA CPU reads/writes to DMA-BUF pixmaps. | Yes | +| `0020` | Reports the active EGL/GLES import contract, eligible route, and fallback reason once at renderer startup. | Yes | +| `0021` | Keeps padded AHardwareBuffer dimensions out of the logical X pixmap size and rejects undersized imports. | Yes | +| `0022` | Disables Present GPU copies when CPU and GPU views of the same AHardwareBuffer are incoherent. | Yes | +| `0023` | Transfers AHardwareBuffer ownership between EXA CPU access and fenced GPU Present copies. | Yes | + +## Non-production patches + +| Patch | Purpose | How it is used | +| --- | --- | --- | +| `diagnostics/0012` | Logs raw-FD duplication, mmap, dimensions, and errno. | Applied with `UDROID_X11_DIAGNOSTICS=1`. | +| `experiments/0013` | Fixes an upstream width/height typo in direct flip. | Never applied: it exposes an unqualified direct-flip path whose ownership/fence contract is incomplete. | + +The diagnostic profile adds observability only; it does not change the selected +buffer transport. Production builds must not set `UDROID_X11_DIAGNOSTICS`. +Use the upstream `-disable-gpu-present` server option for CPU/GPU Present A/B +tests; no source patch is needed. Perfetto slices and X11 Present tools cover +lifecycle timing without permanent per-frame logging. + +## Validation + +Run source preparation twice for each profile. The second run verifies semantic +idempotence rather than only a clean first apply: + +```sh +./tools/prepare-termux-x11-source.sh +./tools/prepare-termux-x11-source.sh +UDROID_X11_DIAGNOSTICS=1 ./tools/prepare-termux-x11-source.sh +UDROID_X11_DIAGNOSTICS=1 ./tools/prepare-termux-x11-source.sh +./tools/prepare-termux-x11-source.sh +``` + +The final standard run also verifies diagnostic patches can be removed cleanly. +Before release, replay both profiles from a fresh pinned submodule checkout; +semantic markers prove idempotence but cannot detect stale patch context. +Then rebuild the APK. Device qualification must include a composited desktop, +normal channel colors, repeated window movement, typed repaint, and Present +offload counters; a visible first frame alone is insufficient. diff --git a/patches/termux-x11/diagnostics/0012-lorie-raw-fd-import-diagnostics.patch b/patches/termux-x11/diagnostics/0012-lorie-raw-fd-import-diagnostics.patch new file mode 100644 index 0000000..eeb96c5 --- /dev/null +++ b/patches/termux-x11/diagnostics/0012-lorie-raw-fd-import-diagnostics.patch @@ -0,0 +1,75 @@ +lorie: add raw-FD import diagnostics + +Log descriptor size, offset, stride, ownership, and errno around duplication and +mmap failures. This is intentionally limited to the opt-in diagnostics profile +because imports may generate substantial output. + +--- a/buffer.c ++++ b/buffer.c +@@ -175,9 +175,11 @@ + AHardwareBuffer_Desc desc = {0}; + static uint64_t id = 0; + bool acceptable = (format == AHARDWAREBUFFER_FORMAT_B8G8R8A8_UNORM || format == AHARDWAREBUFFER_FORMAT_R8G8B8X8_UNORM) && width > 0 && height > 0; ++ int ownedFd = takeFd ? fd : dup(fd); ++ int duplicateError = !takeFd && ownedFd < 0 ? errno : 0; + LorieBuffer b = { .desc = { .width = width, .stride = stride, .height = height, .format = format, .type = type, + .needsBgraSwizzle = format != AHARDWAREBUFFER_FORMAT_B8G8R8A8_UNORM, .buffer = buf, .id = id++ }, +- .fd = takeFd ? fd : dup(fd), .size = size, .offset = offset }; ++ .fd = ownedFd, .size = size, .offset = offset }; + + if (type != LORIEBUFFER_AHARDWAREBUFFER && !acceptable) + return NULL; +@@ -194,11 +197,27 @@ + break; + case LORIEBUFFER_FD: +- if (b.fd < 0) ++ if (b.fd < 0) { ++ int error = duplicateError ? duplicateError : EBADF; ++ struct stat info = {0}; ++ off_t fdSize = fstat(fd, &info) == 0 ? info.st_size : -1; ++ dprintf(2, "LorieBuffer: raw-fd import stage=%s source_fd=%d owned_fd=%d fd_size=%lld width=%d height=%d stride=%d offset=%lld map_size=%zu errno=%d (%s)\n", ++ takeFd ? "validate" : "dup", fd, b.fd, (long long) fdSize, ++ width, height, stride, (long long) offset, size, ++ error, strerror(error)); ++ errno = error; + return NULL; ++ } + + b.desc.data = mmap(NULL, b.size, PROT_READ|PROT_WRITE, MAP_SHARED, b.fd, b.offset); + if (b.desc.data == NULL || b.desc.data == MAP_FAILED) { ++ int error = errno; ++ struct stat info = {0}; ++ off_t fdSize = fstat(b.fd, &info) == 0 ? info.st_size : -1; ++ dprintf(2, "LorieBuffer: raw-fd import stage=mmap source_fd=%d owned_fd=%d fd_size=%lld width=%d height=%d stride=%d offset=%lld map_size=%zu errno=%d (%s)\n", ++ fd, b.fd, (long long) fdSize, width, height, stride, ++ (long long) offset, size, error, strerror(error)); + close(b.fd); ++ errno = error; + return NULL; + } + break; +--- a/InitOutput.c ++++ b/InitOutput.c +@@ -1206,10 +1206,20 @@ + priv->imported = true; + + if (modifier == DRM_FORMAT_MOD_INVALID || modifier == DRM_FORMAT_MOD_LINEAR || modifier == RAW_MMAPPABLE_FD) { +- check(!(priv->buffer = LorieBuffer_wrapFileDescriptor(width, strides[0]/4, height, AHARDWAREBUFFER_FORMAT_B8G8R8A8_UNORM, fds[0], offsets[0])), "DRI3: LorieBuffer_wrapAHardwareBuffer failed."); ++ struct stat fdInfo = {0}; ++ int fdStatResult = fstat(fds[0], &fdInfo); ++ int fdStatError = fdStatResult == 0 ? 0 : errno; ++ off_t fdSize = fdStatResult == 0 ? fdInfo.st_size : -1; ++ if (lorieServerDebugEnabled) ++ log(INFO, "DRI3: raw-fd import request modifier=%llu fd=%d fd_size=%lld dimensions=%ux%u stride=%lu offset=%lu fstat_errno=%d", ++ (unsigned long long) modifier, fds[0], (long long) fdSize, ++ width, height, (unsigned long) strides[0], (unsigned long) offsets[0], fdStatError); ++ check(!(priv->buffer = LorieBuffer_wrapFileDescriptor(width, strides[0]/4, height, AHARDWAREBUFFER_FORMAT_B8G8R8A8_UNORM, fds[0], offsets[0])), "DRI3: raw-fd import failed modifier=%llu fd=%d fd_size=%lld dimensions=%ux%u stride=%lu offset=%lu errno=%d (%s)", (unsigned long long) modifier, fds[0], (long long) fdSize, width, height, (unsigned long) strides[0], (unsigned long) offsets[0], errno, strerror(errno)); + screen->ModifyPixmapHeader(pixmap, width, height, 0, 0, strides[0], NULL); + if (lorieServerDebugEnabled) +- log(INFO, "DRI3: imported raw fd, modifier %llu, %ux%u stride %u", (unsigned long long) modifier, width, height, strides[0]); ++ log(INFO, "DRI3: raw-fd import succeeded modifier=%llu fd=%d fd_size=%lld dimensions=%ux%u stride=%lu offset=%lu", ++ (unsigned long long) modifier, fds[0], (long long) fdSize, ++ width, height, (unsigned long) strides[0], (unsigned long) offsets[0]); + return pixmap; + } + diff --git a/patches/termux-x11/experiments/0013-fix-present-flip-height-check.patch b/patches/termux-x11/experiments/0013-fix-present-flip-height-check.patch new file mode 100644 index 0000000..5e218ee --- /dev/null +++ b/patches/termux-x11/experiments/0013-fix-present-flip-height-check.patch @@ -0,0 +1,19 @@ +present: correct the direct-flip height comparison + +The upstream predicate compares root width against both pixmap dimensions and +rejects valid non-square fullscreen buffers. Keep this patch experimental: the +fix also makes direct flip reachable before uDroid has established its required +ownership and fence contract. + +--- a/InitOutput.c ++++ b/InitOutput.c +@@ -1038,7 +1038,8 @@ + + Bool loriePresentFlip(__unused RRCrtcPtr crtc, __unused uint64_t event_id, __unused uint64_t target_msc, PixmapPtr pixmap, __unused Bool sync_flip) { + LoriePixmapPriv* priv = (LoriePixmapPriv*) exaGetPixmapDriverPrivate(pixmap); +- if (!priv || !priv->buffer || priv->mem || pvfb->root.width != pixmap->drawable.width || pvfb->root.width != pixmap->drawable.height) ++ /* Compare each pixmap axis with the matching root axis. */ ++ if (!priv || !priv->buffer || priv->mem || pvfb->root.width != pixmap->drawable.width || pvfb->root.height != pixmap->drawable.height) + return FALSE; + + const LorieBuffer_Desc *desc = LorieBuffer_description(priv->buffer); diff --git a/tools/experiments/README.md b/tools/experiments/README.md new file mode 100644 index 0000000..2683409 --- /dev/null +++ b/tools/experiments/README.md @@ -0,0 +1,19 @@ +# X11 diagnostic probes + +These small programs isolate one X11 or process-lifecycle contract at a time. +They are developer diagnostics, not application runtime dependencies. + +| Probe | Purpose | +|---|---| +| `glx-oml-sample.c` | Read GLX OML UST, MSC, and SBC values for presentation timing. | +| `ppoll-trace.c` | Preload trace for blocking `poll` and `ppoll` calls. | +| `signal-backtrace.c` | Install an opt-in signal handler that prints a native backtrace. | +| `x11-input-probe.c` | Send simple pointer and launcher-key input through Xlib. | +| `x11-interaction-probe.c` | Check display geometry and XTest input injection. | +| `xdamage-trace.c` | Preload trace for XDamage API calls. | +| `xdamage-watch.c` | Count damage events for one X11 window over a bounded interval. | +| `xkb-reply-probe.c` | Distinguish delayed XKB replies, protocol errors, and timeouts. | + +Build a probe inside the target Linux system with its matching development +libraries. Keep preload traces and signal handlers out of normal sessions: they +change timing and are intended only for short, reproducible captures. diff --git a/tools/experiments/glx-oml-sample.c b/tools/experiments/glx-oml-sample.c new file mode 100644 index 0000000..0be8709 --- /dev/null +++ b/tools/experiments/glx-oml-sample.c @@ -0,0 +1,45 @@ +#include +#include +#include +#include + +int main(void) { + Display *display = XOpenDisplay(NULL); + PFNGLXGETSYNCVALUESOMLPROC get_sync_values; + XVisualInfo *visual; + XSetWindowAttributes attributes; + Window window; + GLXContext context; + int visual_attributes[] = {GLX_RGBA, GLX_DOUBLEBUFFER, None}; + + if (!display) return 1; + visual = glXChooseVisual(display, DefaultScreen(display), visual_attributes); + if (!visual) return 2; + attributes.colormap = XCreateColormap(display, + RootWindow(display, visual->screen), + visual->visual, AllocNone); + window = XCreateWindow(display, RootWindow(display, visual->screen), + 0, 0, 64, 64, 0, visual->depth, InputOutput, + visual->visual, CWColormap, &attributes); + context = glXCreateContext(display, visual, NULL, True); + if (!window || !context || !glXMakeCurrent(display, window, context)) return 3; + get_sync_values = (PFNGLXGETSYNCVALUESOMLPROC) + glXGetProcAddressARB((const GLubyte *) "glXGetSyncValuesOML"); + if (!get_sync_values) return 4; + + for (int i = 0; i < 12; i++) { + int64_t ust = 0, msc = 0, sbc = 0; + Bool ok = get_sync_values(display, window, &ust, &msc, &sbc); + printf("ok=%d ust=%" PRId64 " msc=%" PRId64 " sbc=%" PRId64 "\n", + ok, ust, msc, sbc); + fflush(stdout); + usleep(250000); + } + + glXMakeCurrent(display, None, NULL); + glXDestroyContext(display, context); + XDestroyWindow(display, window); + XFree(visual); + XCloseDisplay(display); + return 0; +} diff --git a/tools/experiments/ppoll-trace.c b/tools/experiments/ppoll-trace.c new file mode 100644 index 0000000..281fff9 --- /dev/null +++ b/tools/experiments/ppoll-trace.c @@ -0,0 +1,79 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include +#include +#include +#include + +static int (*real_poll)(struct pollfd *, nfds_t, int); +static int (*real_ppoll)(struct pollfd *, nfds_t, const struct timespec *, + const sigset_t *); +static pthread_t main_thread; +static __thread int tracing; +static __thread unsigned int traced_waits; + +__attribute__((constructor)) static void +remember_main_thread(void) +{ + main_thread = pthread_self(); +} + +static int +is_gnome_shell(void) +{ + char executable[256]; + ssize_t length = readlink("/proc/self/exe", executable, sizeof(executable) - 1); + + if (length < 0) + return 0; + executable[length] = '\0'; + + const char *name = strrchr(executable, '/'); + return strcmp(name ? name + 1 : executable, "gnome-shell") == 0; +} + +static void +trace_wait(const char *name, const struct pollfd *fds, nfds_t count, int indefinite) +{ + if (tracing || !indefinite || count != 1 || traced_waits >= 512 || + !pthread_equal(pthread_self(), main_thread) || !is_gnome_shell()) + return; + + tracing = 1; + traced_waits++; + + void *frames[48]; + int frame_count; + + dprintf(STDERR_FILENO, + "UDROID_POLL_TRACE %s seq=%u pid=%d fd=%d events=0x%x\n", + name, traced_waits, getpid(), fds[0].fd, fds[0].events); + frame_count = backtrace(frames, 48); + backtrace_symbols_fd(frames, frame_count, STDERR_FILENO); + dprintf(STDERR_FILENO, "UDROID_POLL_TRACE end\n"); + + tracing = 0; +} + +int +poll(struct pollfd *fds, nfds_t count, int timeout) +{ + if (!real_poll) + real_poll = dlsym(RTLD_NEXT, "poll"); + trace_wait("poll", fds, count, timeout < 0); + return real_poll(fds, count, timeout); +} + +int +ppoll(struct pollfd *fds, nfds_t count, const struct timespec *timeout, + const sigset_t *sigmask) +{ + if (!real_ppoll) + real_ppoll = dlsym(RTLD_NEXT, "ppoll"); + trace_wait("ppoll", fds, count, timeout == NULL); + return real_ppoll(fds, count, timeout, sigmask); +} diff --git a/tools/experiments/signal-backtrace.c b/tools/experiments/signal-backtrace.c new file mode 100644 index 0000000..f683df3 --- /dev/null +++ b/tools/experiments/signal-backtrace.c @@ -0,0 +1,51 @@ +#define _GNU_SOURCE + +#include +#include +#include +#include +#include + +#define TRACE_SIGNAL (SIGRTMIN + 7) + +static void +write_backtrace(int signal_number) +{ + void *frames[96]; + int count = backtrace(frames, 96); + static const char start[] = "UDROID_SIGNAL_BACKTRACE start\n"; + static const char end[] = "UDROID_SIGNAL_BACKTRACE end\n"; + + (void)signal_number; + ssize_t unused = write(STDERR_FILENO, start, sizeof(start) - 1); + backtrace_symbols_fd(frames, count, STDERR_FILENO); + unused = write(STDERR_FILENO, end, sizeof(end) - 1); + (void)unused; +} + +__attribute__((constructor)) static void +install_backtrace_signal(void) +{ + char executable[256]; + ssize_t length = readlink("/proc/self/exe", executable, + sizeof(executable) - 1); + if (length < 0) + return; + + executable[length] = '\0'; + const char *name = strrchr(executable, '/'); + name = name ? name + 1 : executable; + if (strcmp(name, "gnome-shell") != 0) + return; + + /* Prime the unwinder before a diagnostic signal interrupts the process. */ + void *frame; + backtrace(&frame, 1); + + struct sigaction action = { + .sa_handler = write_backtrace, + .sa_flags = SA_RESTART, + }; + sigemptyset(&action.sa_mask); + sigaction(TRACE_SIGNAL, &action, NULL); +} diff --git a/tools/experiments/x11-input-probe.c b/tools/experiments/x11-input-probe.c new file mode 100644 index 0000000..34880b5 --- /dev/null +++ b/tools/experiments/x11-input-probe.c @@ -0,0 +1,46 @@ +#include +#include +#include +#include +#include + +extern Bool XTestFakeKeyEvent(Display *, unsigned int, Bool, unsigned long); +extern Bool XTestFakeMotionEvent(Display *, int, int, int, unsigned long); +extern Bool XTestFakeButtonEvent(Display *, unsigned int, Bool, unsigned long); + +int +main(int argc, char **argv) +{ + Display *display = XOpenDisplay(NULL); + + if (!display) { + fputs("could not open DISPLAY\n", stderr); + return 1; + } + + if (argc == 4 && strcmp(argv[1], "move") == 0) { + if (!XTestFakeMotionEvent(display, DefaultScreen(display), atoi(argv[2]), atoi(argv[3]), 0)) + return 2; + } else if (argc == 2 && strcmp(argv[1], "click") == 0) { + if (!XTestFakeButtonEvent(display, 1, True, 0) || + !XTestFakeButtonEvent(display, 1, False, 0)) + return 2; + } else if (argc == 2 && strcmp(argv[1], "show-apps") == 0) { + KeyCode super = XKeysymToKeycode(display, XK_Super_L); + KeyCode a = XKeysymToKeycode(display, XK_a); + + if (!super || !a || + !XTestFakeKeyEvent(display, super, True, 0) || + !XTestFakeKeyEvent(display, a, True, 0) || + !XTestFakeKeyEvent(display, a, False, 0) || + !XTestFakeKeyEvent(display, super, False, 0)) + return 2; + } else { + fprintf(stderr, "usage: %s move X Y | click | show-apps\n", argv[0]); + return 2; + } + + XSync(display, False); + XCloseDisplay(display); + return 0; +} diff --git a/tools/experiments/x11-interaction-probe.c b/tools/experiments/x11-interaction-probe.c new file mode 100644 index 0000000..3cb609f --- /dev/null +++ b/tools/experiments/x11-interaction-probe.c @@ -0,0 +1,64 @@ +#include +#include +#include + +#include +#include +#include +#include + +static int +parse_coordinate(const char *value, int *coordinate) +{ + char *end; + errno = 0; + long parsed = strtol(value, &end, 10); + if (errno || *value == '\0' || *end != '\0' || parsed < 0 || parsed > 32767) + return 0; + + *coordinate = parsed; + return 1; +} + +int +main(int argc, char **argv) +{ + Display *display = XOpenDisplay(NULL); + if (!display) { + fprintf(stderr, "could not open DISPLAY\n"); + return 1; + } + + int screen = DefaultScreen(display); + if (argc == 2 && strcmp(argv[1], "--size") == 0) { + printf("%dx%d\n", DisplayWidth(display, screen), DisplayHeight(display, screen)); + } else if (argc == 2 && strcmp(argv[1], "--show-apps") == 0) { + KeyCode super = XKeysymToKeycode(display, XK_Super_L); + KeyCode a = XKeysymToKeycode(display, XK_A); + if (!super || !a || + !XTestFakeKeyEvent(display, super, True, CurrentTime) || + !XTestFakeKeyEvent(display, a, True, CurrentTime) || + !XTestFakeKeyEvent(display, a, False, CurrentTime) || + !XTestFakeKeyEvent(display, super, False, CurrentTime)) { + fprintf(stderr, "could not inject Super+A\n"); + XCloseDisplay(display); + return 1; + } + } else if (argc == 4 && strcmp(argv[1], "--move") == 0) { + int x, y; + if (!parse_coordinate(argv[2], &x) || !parse_coordinate(argv[3], &y) || + !XTestFakeMotionEvent(display, screen, x, y, CurrentTime)) { + fprintf(stderr, "could not inject pointer move\n"); + XCloseDisplay(display); + return 1; + } + } else { + fprintf(stderr, "usage: %s --size | --show-apps | --move X Y\n", argv[0]); + XCloseDisplay(display); + return 2; + } + + XSync(display, False); + XCloseDisplay(display); + return 0; +} diff --git a/tools/experiments/xdamage-trace.c b/tools/experiments/xdamage-trace.c new file mode 100644 index 0000000..c008c54 --- /dev/null +++ b/tools/experiments/xdamage-trace.c @@ -0,0 +1,54 @@ +#define _GNU_SOURCE +#include +#include + +#include +#include +#include + +static FILE *trace_file(void) { + static FILE *file; + if (!file) + file = fopen("/tmp/udroid-xdamage-trace.log", "a"); + return file; +} + +static void trace(const char *operation, unsigned long drawable, + unsigned long damage, int level) { + FILE *file = trace_file(); + struct timespec now; + if (!file) + return; + clock_gettime(CLOCK_MONOTONIC, &now); + fprintf(file, "%lld.%09ld %s drawable=0x%lx damage=0x%lx level=%d\n", + (long long)now.tv_sec, now.tv_nsec, operation, drawable, damage, + level); + fflush(file); +} + +Damage XDamageCreate(Display *display, Drawable drawable, int level) { + static Damage (*next)(Display *, Drawable, int); + Damage damage; + if (!next) + next = dlsym(RTLD_NEXT, "XDamageCreate"); + damage = next(display, drawable, level); + trace("create", drawable, damage, level); + return damage; +} + +void XDamageSubtract(Display *display, Damage damage, XserverRegion repair, + XserverRegion parts) { + static void (*next)(Display *, Damage, XserverRegion, XserverRegion); + if (!next) + next = dlsym(RTLD_NEXT, "XDamageSubtract"); + trace("subtract", repair, damage, 0); + next(display, damage, repair, parts); +} + +void XDamageDestroy(Display *display, Damage damage) { + static void (*next)(Display *, Damage); + if (!next) + next = dlsym(RTLD_NEXT, "XDamageDestroy"); + trace("destroy", 0, damage, 0); + next(display, damage); +} diff --git a/tools/experiments/xdamage-watch.c b/tools/experiments/xdamage-watch.c new file mode 100644 index 0000000..ad9e01d --- /dev/null +++ b/tools/experiments/xdamage-watch.c @@ -0,0 +1,55 @@ +#include +#include + +#include +#include +#include +#include +#include + +int main(int argc, char **argv) { + Display *display; + Window window; + Damage damage; + XEvent event; + int damage_event; + int damage_error; + unsigned long count = 0; + time_t deadline; + + if (argc != 3) { + fprintf(stderr, "usage: %s WINDOW_ID SECONDS\n", argv[0]); + return 2; + } + + errno = 0; + window = (Window)strtoul(argv[1], NULL, 0); + deadline = time(NULL) + strtol(argv[2], NULL, 10); + if (errno || !window || deadline <= time(NULL)) + return 2; + + display = XOpenDisplay(NULL); + if (!display || !XDamageQueryExtension(display, &damage_event, &damage_error)) + return 3; + + damage = XDamageCreate(display, window, XDamageReportRawRectangles); + XSync(display, False); + while (time(NULL) < deadline) { + while (XPending(display)) { + XNextEvent(display, &event); + if (event.type == damage_event + XDamageNotify) { + XDamageNotifyEvent *notify = (XDamageNotifyEvent *)&event; + printf("damage=%lu drawable=0x%lx area=%dx%d+%d+%d\n", + ++count, notify->drawable, notify->area.width, + notify->area.height, notify->area.x, notify->area.y); + } + } + struct timespec pause = {.tv_nsec = 10000000}; + nanosleep(&pause, NULL); + } + + printf("total=%lu\n", count); + XDamageDestroy(display, damage); + XCloseDisplay(display); + return 0; +} diff --git a/tools/experiments/xkb-reply-probe.c b/tools/experiments/xkb-reply-probe.c new file mode 100644 index 0000000..c8650cc --- /dev/null +++ b/tools/experiments/xkb-reply-probe.c @@ -0,0 +1,133 @@ +#define _POSIX_C_SOURCE 200809L + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +struct request { + const char *name; + unsigned int sequence; + int complete; +}; + +static int64_t +milliseconds(void) +{ + struct timespec now; + + clock_gettime(CLOCK_MONOTONIC, &now); + return now.tv_sec * 1000 + now.tv_nsec / 1000000; +} + +int +main(void) +{ + xcb_connection_t *connection = xcb_connect(NULL, NULL); + if (xcb_connection_has_error(connection)) { + fputs("connect failed\n", stderr); + return 1; + } + + if (!xkb_x11_setup_xkb_extension(connection, + XKB_X11_MIN_MAJOR_XKB_VERSION, + XKB_X11_MIN_MINOR_XKB_VERSION, + XKB_X11_SETUP_XKB_EXTENSION_NO_FLAGS, + NULL, NULL, NULL, NULL)) { + fputs("XKB setup failed\n", stderr); + return 1; + } + + int32_t device = xkb_x11_get_core_keyboard_device_id(connection); + if (device < 0) { + fputs("core keyboard lookup failed\n", stderr); + return 1; + } + printf("device=%d\n", device); + + const xcb_xkb_map_part_t map_parts = + XCB_XKB_MAP_PART_KEY_TYPES | + XCB_XKB_MAP_PART_KEY_SYMS | + XCB_XKB_MAP_PART_MODIFIER_MAP | + XCB_XKB_MAP_PART_EXPLICIT_COMPONENTS | + XCB_XKB_MAP_PART_KEY_ACTIONS | + XCB_XKB_MAP_PART_VIRTUAL_MODS | + XCB_XKB_MAP_PART_VIRTUAL_MOD_MAP; + const xcb_xkb_name_detail_t names = + XCB_XKB_NAME_DETAIL_KEYCODES | + XCB_XKB_NAME_DETAIL_SYMBOLS | + XCB_XKB_NAME_DETAIL_TYPES | + XCB_XKB_NAME_DETAIL_COMPAT | + XCB_XKB_NAME_DETAIL_KEY_TYPE_NAMES | + XCB_XKB_NAME_DETAIL_KT_LEVEL_NAMES | + XCB_XKB_NAME_DETAIL_INDICATOR_NAMES | + XCB_XKB_NAME_DETAIL_KEY_NAMES | + XCB_XKB_NAME_DETAIL_KEY_ALIASES | + XCB_XKB_NAME_DETAIL_VIRTUAL_MOD_NAMES | + XCB_XKB_NAME_DETAIL_GROUP_NAMES; + + xcb_xkb_get_map_cookie_t map = + xcb_xkb_get_map(connection, device, map_parts, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); + xcb_xkb_get_indicator_map_cookie_t indicators = + xcb_xkb_get_indicator_map(connection, device, UINT32_MAX); + xcb_xkb_get_compat_map_cookie_t compat = + xcb_xkb_get_compat_map(connection, device, 0, 1, 0, 0); + xcb_xkb_get_names_cookie_t key_names = + xcb_xkb_get_names(connection, device, names); + xcb_xkb_get_controls_cookie_t controls = + xcb_xkb_get_controls(connection, device); + + struct request requests[] = { + { "GetMap", map.sequence, 0 }, + { "GetIndicatorMap", indicators.sequence, 0 }, + { "GetCompatMap", compat.sequence, 0 }, + { "GetNames", key_names.sequence, 0 }, + { "GetControls", controls.sequence, 0 }, + }; + const size_t request_count = sizeof(requests) / sizeof(requests[0]); + size_t completed = 0; + int64_t deadline = milliseconds() + 3000; + + xcb_flush(connection); + for (size_t i = 0; i < request_count; i++) + printf("sent %s sequence=%u\n", requests[i].name, requests[i].sequence); + fflush(stdout); + + while (completed < request_count && milliseconds() < deadline) { + for (size_t i = 0; i < request_count; i++) { + void *reply = NULL; + xcb_generic_error_t *error = NULL; + + if (requests[i].complete || + !xcb_poll_for_reply(connection, requests[i].sequence, + &reply, &error)) + continue; + + requests[i].complete = 1; + completed++; + printf("reply %s sequence=%u status=%s error=%u\n", + requests[i].name, requests[i].sequence, + reply ? "ok" : "error", error ? error->error_code : 0); + free(reply); + free(error); + } + + struct pollfd fd = { xcb_get_file_descriptor(connection), POLLIN, 0 }; + poll(&fd, 1, 20); + } + + for (size_t i = 0; i < request_count; i++) { + if (!requests[i].complete) + printf("timeout %s sequence=%u\n", + requests[i].name, requests[i].sequence); + } + + xcb_disconnect(connection); + return completed == request_count ? 0 : 2; +} diff --git a/tools/prepare-termux-x11-source.sh b/tools/prepare-termux-x11-source.sh index e7777bb..cb0da09 100755 --- a/tools/prepare-termux-x11-source.sh +++ b/tools/prepare-termux-x11-source.sh @@ -81,58 +81,88 @@ apply_once \ "$repo_root/patches/termux-x11/0004-udroid-batched-native-touch.patch" \ 'EVENT_TOUCH_FRAME' -if [[ "${UDROID_EXPERIMENTAL_GFXSTREAM_X11:-0}" == "1" ]]; then - source_profile="experimental gfxstream" - revert_if_applied \ - "$cpp_root/lorie" \ - "$repo_root/patches/termux-x11/0010-disable-gpu-present-copy.patch" \ - "uDroid standard profile: use Xorg's copy path" - apply_once \ - "$cpp_root/lorie" \ - "$repo_root/patches/termux-x11/0005-dmabuf-cpu-read-sync.patch" \ - 'DMA_BUF_IOCTL_SYNC' - apply_once \ - "$cpp_root/lorie" \ - "$repo_root/patches/termux-x11/0006-ahardwarebuffer-external-texture-sampling.patch" \ - 'GL_TEXTURE_EXTERNAL_OES' - apply_once \ - "$cpp_root/lorie" \ - "$repo_root/patches/termux-x11/0007-ahardwarebuffer-content-semantics.patch" \ - 'AHARDWAREBUFFER_RGBA_SOCKET_FD' +# SPHAL is a runtime graphics profile, so its AHardwareBuffer and Present-copy +# contract must be present in every embedded X11 build. +apply_once \ + "$cpp_root/lorie" \ + "$repo_root/patches/termux-x11/0005-dmabuf-cpu-read-sync.patch" \ + 'DMA_BUF_IOCTL_SYNC' +apply_once \ + "$cpp_root/lorie" \ + "$repo_root/patches/termux-x11/0006-ahardwarebuffer-external-texture-sampling.patch" \ + 'GL_TEXTURE_EXTERNAL_OES' +apply_once \ + "$cpp_root/lorie" \ + "$repo_root/patches/termux-x11/0007-ahardwarebuffer-content-semantics.patch" \ + 'AHARDWAREBUFFER_RGBA_SOCKET_FD' +apply_once \ + "$cpp_root/lorie" \ + "$repo_root/patches/termux-x11/0008-advertise-buffer-transport-protocol.patch" \ + 'UDROID_X11_BUFFER_TRANSPORT_ATOM' +apply_once \ + "$cpp_root" \ + "$repo_root/patches/termux-x11/0009-defer-gpu-only-present-while-detached.patch" \ + 'Bool loriePixmapIsGpuOnly(PixmapPtr pixmap) {' +revert_if_applied \ + "$cpp_root/lorie" \ + "$repo_root/patches/termux-x11/experiments/0013-fix-present-flip-height-check.patch" \ + 'pvfb->root.height != pixmap->drawable.height' + +if [[ "${UDROID_X11_DIAGNOSTICS:-0}" == "1" ]]; then + source_profile="diagnostics" apply_once \ "$cpp_root/lorie" \ - "$repo_root/patches/termux-x11/0008-advertise-buffer-transport-protocol.patch" \ - 'UDROID_X11_BUFFER_TRANSPORT_ATOM' - apply_once \ - "$cpp_root" \ - "$repo_root/patches/termux-x11/0009-defer-gpu-only-present-while-detached.patch" \ - 'Bool loriePixmapRequiresGpuCopy(PixmapPtr pixmap) {' + "$repo_root/patches/termux-x11/diagnostics/0012-lorie-raw-fd-import-diagnostics.patch" \ + 'raw-fd import stage=mmap' else source_profile="standard" - revert_if_applied \ - "$cpp_root" \ - "$repo_root/patches/termux-x11/0009-defer-gpu-only-present-while-detached.patch" \ - 'Bool loriePixmapRequiresGpuCopy(PixmapPtr pixmap) {' - revert_if_applied \ - "$cpp_root/lorie" \ - "$repo_root/patches/termux-x11/0008-advertise-buffer-transport-protocol.patch" \ - 'UDROID_X11_BUFFER_TRANSPORT_ATOM' - revert_if_applied \ - "$cpp_root/lorie" \ - "$repo_root/patches/termux-x11/0007-ahardwarebuffer-content-semantics.patch" \ - 'AHARDWAREBUFFER_RGBA_SOCKET_FD' revert_if_applied \ "$cpp_root/lorie" \ - "$repo_root/patches/termux-x11/0006-ahardwarebuffer-external-texture-sampling.patch" \ - 'GL_TEXTURE_EXTERNAL_OES' - revert_if_applied \ - "$cpp_root/lorie" \ - "$repo_root/patches/termux-x11/0005-dmabuf-cpu-read-sync.patch" \ - 'DMA_BUF_IOCTL_SYNC' - apply_once \ - "$cpp_root/lorie" \ - "$repo_root/patches/termux-x11/0010-disable-gpu-present-copy.patch" \ - "uDroid standard profile: use Xorg's copy path" + "$repo_root/patches/termux-x11/diagnostics/0012-lorie-raw-fd-import-diagnostics.patch" \ + 'raw-fd import stage=mmap' fi +# Compositors import redirected window pixmaps through DRI3 in every profile. +apply_once \ + "$cpp_root/lorie" \ + "$repo_root/patches/termux-x11/0015-export-pixmaps-through-dri3.patch" \ + 'DRI3: exported DMA-BUF pixmap' +apply_once \ + "$cpp_root/lorie" \ + "$repo_root/patches/termux-x11/0016-accept-fd-gpu-copy-sources.patch" \ + 'lorieGpuCopySource' +apply_once \ + "$cpp_root/lorie" \ + "$repo_root/patches/termux-x11/0017-export-ahardwarebuffer-pixmaps-through-dri3.patch" \ + 'exportAHardwareBufferDmaBuf' +apply_once \ + "$cpp_root" \ + "$repo_root/patches/termux-x11/0018-present-damage-ordering.patch" \ + 'RegionPtr gpu_copy_damage' +apply_once \ + "$cpp_root/lorie" \ + "$repo_root/patches/termux-x11/0019-dmabuf-exa-cpu-access-sync.patch" \ + 'LorieBuffer_cpuAccessBegin' +apply_once \ + "$cpp_root/lorie" \ + "$repo_root/patches/termux-x11/0020-renderer-route-capability-probe.patch" \ + 'Xlorie: G0 graphics contract' +apply_once \ + "$cpp_root/lorie" \ + "$repo_root/patches/termux-x11/0021-preserve-logical-ahb-pixmap-size.patch" \ + 'logical %ux%u physical %ux%u' +apply_once \ + "$cpp_root/lorie" \ + "$repo_root/patches/termux-x11/0022-disable-incoherent-ahb-gpu-present.patch" \ + 'CPU and GPU views of AHardwareBuffer are not coherent' +apply_once \ + "$cpp_root/lorie" \ + "$repo_root/patches/termux-x11/0023-transfer-ahb-present-ownership.patch" \ + 'Transfer an idle CPU mapping back to the GPU' + +apply_once \ + "$cpp_root/lorie" \ + "$repo_root/patches/termux-x11/0011-lorie-perfetto-slices.patch" \ + 'uDroid/Lorie draw' + echo "Termux:X11 native source is ready ($source_profile profile)." diff --git a/tools/profiling/venus-perfetto.pbtxt b/tools/profiling/venus-perfetto.pbtxt new file mode 100644 index 0000000..eb57dbf --- /dev/null +++ b/tools/profiling/venus-perfetto.pbtxt @@ -0,0 +1,75 @@ +duration_ms: 15000 + +buffers { + size_kb: 65536 + fill_policy: RING_BUFFER +} + +data_sources { + config { + name: "linux.ftrace" + ftrace_config { + ftrace_events: "sched/sched_switch" + ftrace_events: "sched/sched_waking" + ftrace_events: "sched/sched_wakeup_new" + ftrace_events: "power/cpu_frequency" + ftrace_events: "power/gpu_frequency" + ftrace_events: "power/gpu_work_period" + ftrace_events: "gpu_mem/gpu_mem_total" + ftrace_events: "mali/mali_JM_SUBMIT" + ftrace_events: "mali/mali_JM_JOB_DONE" + ftrace_events: "mali/mali_job_slots_event" + atrace_categories: "gfx" + atrace_categories: "input" + atrace_categories: "sched" + atrace_categories: "view" + atrace_apps: "org.randomcoder.udroid" + atrace_apps: "org.randomcoder.udroid.dev" + compact_sched { + enabled: true + } + } + } +} + +data_sources { + config { + name: "android.surfaceflinger.frame" + } +} + +data_sources { + config { + name: "android.surfaceflinger.frametimeline" + } +} + +data_sources { + config { + name: "android.surfaceflinger.layers" + surfaceflinger_layers_config { + mode: MODE_ACTIVE + trace_flags: TRACE_FLAG_COMPOSITION + trace_flags: TRACE_FLAG_BUFFERS + } + } +} + +data_sources { + config { + name: "android.gpu.memory" + } +} + +data_sources { + config { + name: "linux.process_stats" + process_stats_config { + scan_all_processes_on_start: true + proc_stats_poll_ms: 1000 + } + } +} + +write_into_file: true +file_write_period_ms: 1000 diff --git a/tools/test_udroid_debug.py b/tools/test_udroid_debug.py new file mode 100644 index 0000000..743fb38 --- /dev/null +++ b/tools/test_udroid_debug.py @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 + +import importlib.util +from pathlib import Path +import unittest + + +SPEC = importlib.util.spec_from_file_location( + "udroid_debug", + Path(__file__).with_name("udroid-debug.py"), +) +UDROID_DEBUG = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(UDROID_DEBUG) + + +class UdroidDebugTest(unittest.TestCase): + def test_finds_accessibility_node_center(self): + document = """""" + + self.assertEqual( + (1021, 190), + UDROID_DEBUG.accessibility_node_center( + document, + "Collapse desktop controls", + "org.randomcoder.udroid.dev", + ), + ) + + def test_missing_accessibility_node_returns_none(self): + self.assertIsNone( + UDROID_DEBUG.accessibility_node_center( + '', + "Collapse desktop controls", + ) + ) + + def test_ignores_another_apps_accessibility_node(self): + self.assertIsNone( + UDROID_DEBUG.accessibility_node_center( + '', + "Collapse desktop controls", + "org.randomcoder.udroid.dev", + ) + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/udroid-debug.md b/tools/udroid-debug.md new file mode 100644 index 0000000..9c57d6a --- /dev/null +++ b/tools/udroid-debug.md @@ -0,0 +1,31 @@ +# Dev-build ADB controls + +`udroid-debug.py` talks to the installed `org.randomcoder.udroid.dev` app. It uses +an ADB-shell-only receiver and is not included in release builds. It invokes the +same supervised session paths as the UI; it does not run arbitrary guest commands. + +```sh +python3 tools/udroid-debug.py roots +python3 tools/udroid-debug.py status +python3 tools/udroid-debug.py start-desktop --rootfs noble-raw --environment gnome +python3 tools/udroid-debug.py open-desktop +python3 tools/udroid-debug.py logs --limit 40 +python3 tools/udroid-debug.py stop-desktop +``` + +Other controls: `open-system`, `start-terminal`, and `stop-terminal`. Add +`--serial DEVICE` when multiple devices are connected. Set `ADB` to the full +`adb` executable path if it is not on `PATH`. Commands are queued; `status` +reports whether the supervised operation actually started. + +## Panfrost damage reproduction + +Start a clean Panfrost GNOME session, launch Alacritty through uDroid's normal +Linux-app path, and collapse the Android display controls: + +```sh +python3 tools/udroid-debug.py repro-damage +``` + +Then drag the Alacritty window and type `glxgears` one character at a time. +Use `--keep-controls` when the expanded controls are part of the comparison. diff --git a/tools/udroid-debug.py b/tools/udroid-debug.py new file mode 100644 index 0000000..a3145fe --- /dev/null +++ b/tools/udroid-debug.py @@ -0,0 +1,270 @@ +#!/usr/bin/env python3 +"""Inspect and control a connected uDroid dev build without tapping its UI.""" + +import argparse +import json +import os +import re +import subprocess +import sys +import time +import xml.etree.ElementTree as ET + + +def adb(args, *, serial=None, check=True): + command = [os.environ.get("ADB", "adb")] + if serial: + command += ["-s", serial] + result = subprocess.run(command + args, capture_output=True, text=True) + if check and result.returncode: + raise RuntimeError((result.stderr or result.stdout).strip()) + return result + + +def private_file(package, path, serial): + return adb(["exec-out", "run-as", package, "cat", path], serial=serial).stdout + + +def preferences(package, name, serial): + result = adb( + ["exec-out", "run-as", package, "cat", f"shared_prefs/{name}.xml"], + serial=serial, + check=False, + ) + if result.returncode: + if "No such file or directory" in result.stderr: + return {} + raise RuntimeError((result.stderr or result.stdout).strip()) + root = ET.fromstring(result.stdout) + return { + element.attrib["name"]: element.attrib.get("value", element.text or "") + for element in root + } + + +def show_status(package, serial): + state = preferences(package, "runtime-supervisor-state", serial) + active = preferences(package, "installed-rootfs-state", serial).get("active-rootfs", "none") + print(f"Active Linux: {active}") + print( + f"Terminal: {state.get('phase', 'unknown')} · " + f"{state.get('rootfs-name', 'none')} · {state.get('message', '')}" + ) + print( + f"Desktop: {state.get('desktop-phase', 'unknown')} · " + f"{state.get('desktop-environment-name', 'none')} · " + f"display :{state.get('desktop-display-number', '?')} · " + f"{state.get('desktop-message', '')}" + ) + print(f"Boot ID: {state.get('boot-id', 'none')}") + + +def show_roots(package, serial): + result = adb( + ["exec-out", "run-as", package, "ls", "-1", "files/rootfs"], + serial=serial, + check=False, + ) + if result.returncode: + print("No Linux systems found") + return + for name in result.stdout.splitlines(): + if not name or "/" in name: + continue + marker = f"files/rootfs/{name}/.udroid-ready" + ready = adb( + ["exec-out", "run-as", package, "test", "-f", marker], + serial=serial, + check=False, + ).returncode == 0 + print(f"{name}{'' if ready else ' (incomplete)'}") + + +def show_logs(package, serial, limit, raw): + lines = private_file(package, "files/logs/supervisor.jsonl", serial).splitlines()[-limit:] + for line in lines: + if raw: + print(line) + continue + event = json.loads(line) + print( + f"{event.get('timestamp', '?')} " + f"[{event.get('component', '?')}/{event.get('severity', '?')}] " + f"{event.get('event', '?')}: {event.get('message', '')}" + ) + + +def wait_for_desktop(package, serial, rootfs, timeout): + deadline = time.monotonic() + timeout + last_message = "Desktop state is unavailable" + while time.monotonic() < deadline: + state = preferences(package, "runtime-supervisor-state", serial) + phase = state.get("desktop-phase", "unknown") + last_message = state.get("desktop-message", last_message) + if phase == "RUNNING" and state.get("rootfs-name") == rootfs: + return + if phase == "CRASHED": + raise RuntimeError(last_message) + time.sleep(0.5) + raise RuntimeError(f"Timed out waiting for {rootfs} desktop: {last_message}") + + +def launch_application(package, serial, rootfs, application_id): + adb( + [ + "shell", "am", "start", "-n", + f"{package}/org.randomcoder.udroid.MainActivity", + "-a", "org.randomcoder.udroid.action.LAUNCH_LINUX_APPLICATION", + "--es", "org.randomcoder.udroid.extra.LINUX_APPLICATION_ID", application_id, + "--es", "org.randomcoder.udroid.extra.ROOTFS_NAME", rootfs, + "-f", "0x24000000", # CLEAR_TOP | SINGLE_TOP + ], + serial=serial, + ) + + +def accessibility_node_center(document, description, package=None): + root = ET.fromstring(document) + for node in root.iter("node"): + if node.attrib.get("content-desc") != description: + continue + if package and node.attrib.get("package") != package: + continue + match = re.fullmatch( + r"\[(\d+),(\d+)\]\[(\d+),(\d+)\]", + node.attrib.get("bounds", ""), + ) + if match: + left, top, right, bottom = map(int, match.groups()) + return (left + right) // 2, (top + bottom) // 2 + return None + + +def collapse_desktop_controls(package, serial, timeout): + remote = "/sdcard/udroid-debug-window.xml" + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + adb(["shell", "rm", "-f", remote], serial=serial, check=False) + adb(["shell", "uiautomator", "dump", remote], serial=serial, check=False) + result = adb(["exec-out", "cat", remote], serial=serial, check=False) + if result.returncode == 0: + center = accessibility_node_center( + result.stdout, + "Collapse desktop controls", + package, + ) + if center: + adb(["shell", "input", "tap", str(center[0]), str(center[1])], serial=serial) + return + time.sleep(0.5) + raise RuntimeError("Desktop opened, but its Collapse desktop controls button was not found") + + +def send_command(package, serial, command, rootfs, environment): + args = [ + "shell", "am", "broadcast", "--receiver-foreground", + "-n", f"{package}/org.randomcoder.udroid.DebugCommandReceiver", + "-a", "org.randomcoder.udroid.action.DEBUG_COMMAND", + "--es", "command", command, + ] + if rootfs: + args += ["--es", "rootfs", rootfs] + if environment: + args += ["--es", "environment", environment] + result = adb(args, serial=serial) + output = result.stdout.strip() + if not re.search(r"result=-1(?:\D|$)", output): + raise RuntimeError(output or "uDroid did not accept the debug command") + adb( + [ + "shell", "am", "start", "-n", + f"{package}/org.randomcoder.udroid.MainActivity", + "-f", "0x24000000", # CLEAR_TOP | SINGLE_TOP + ], + serial=serial, + ) + print(output) + + +def reproduce_damage(package, serial, rootfs, environment, application, timeout, keep_controls): + state = preferences(package, "runtime-supervisor-state", serial) + if state.get("desktop-phase") != "STOPPED": + send_command(package, serial, "stop-desktop", None, None) + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + state = preferences(package, "runtime-supervisor-state", serial) + if state.get("desktop-phase") == "STOPPED": + break + time.sleep(0.5) + else: + raise RuntimeError("Timed out stopping the existing desktop") + send_command(package, serial, "start-desktop", rootfs, environment) + wait_for_desktop(package, serial, rootfs, timeout) + launch_application(package, serial, rootfs, application) + if not keep_controls: + collapse_desktop_controls(package, serial, timeout) + adb(["shell", "input", "keyevent", "KEYCODE_ESCAPE"], serial=serial) + print( + f"Damage reproduction ready: {environment} + {application}. " + "Drag the window, then type glxgears one character at a time." + ) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--serial", help="ADB device serial") + parser.add_argument("--package", default="org.randomcoder.udroid.dev") + sub = parser.add_subparsers(dest="command", required=True) + for name in ("status", "roots", "open-system", "open-desktop", "start-terminal", "stop-terminal", "start-desktop", "stop-desktop"): + command = sub.add_parser(name) + if name in ("open-system", "start-terminal", "start-desktop"): + command.add_argument("--rootfs", help="Installed Linux system; defaults to active") + if name == "start-desktop": + command.add_argument("--environment", help="Desktop session ID, e.g. gnome or xfce") + logs = sub.add_parser("logs") + logs.add_argument("--limit", type=int, default=80) + logs.add_argument("--raw", action="store_true", help="Print original JSONL") + damage = sub.add_parser("repro-damage", help="Open the Panfrost GNOME damage reproduction") + damage.add_argument("--rootfs", default="noble-raw") + damage.add_argument("--environment", default="udroid-gnome-panfrost") + damage.add_argument("--application", default="Alacritty", help="Desktop application ID") + damage.add_argument("--timeout", type=float, default=30) + damage.add_argument("--keep-controls", action="store_true") + args = parser.parse_args() + try: + if args.command == "status": + show_status(args.package, args.serial) + elif args.command == "roots": + show_roots(args.package, args.serial) + elif args.command == "logs": + if args.limit < 1: + parser.error("--limit must be positive") + show_logs(args.package, args.serial, args.limit, args.raw) + elif args.command == "repro-damage": + if args.timeout <= 0: + parser.error("--timeout must be positive") + reproduce_damage( + args.package, + args.serial, + args.rootfs, + args.environment, + args.application, + args.timeout, + args.keep_controls, + ) + else: + send_command( + args.package, + args.serial, + args.command, + getattr(args, "rootfs", None), + getattr(args, "environment", None), + ) + except (OSError, RuntimeError, ET.ParseError, json.JSONDecodeError) as error: + print(f"udroid-debug: {error}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main())