From 55b30fbf1dfc1e4baea530eb72a3eaecec5e2478 Mon Sep 17 00:00:00 2001 From: The412Banner Date: Tue, 13 Oct 2026 08:44:45 -0400 Subject: [PATCH] Non-Adreno round 3: bridge images, Venus GL, shared buffers, Steam GPU drawing - vkbridge: the memory model skips lazily allocated types. Mali-G615 lists DEVICE_LOCAL|LAZILY_ALLOCATED first; taken as the device-local type it left images with no memory type (self-test "image clear/draw: setup" FAIL, gamescope "No compatible image memory type"). - Session: a bridge self-test that fails images or draws falls back to the runtime's driver pair for that session (BL_VKBRIDGE_FALLBACK_ICD); Setup and the graphics report say so. - Venus: the Mali compatibility layer runs in every process but gamescope (DROIDDECK_MALI_COMPAT=1), so Zink in Steam and Xwayland gets nullDescriptor; null texel-buffer views get stand-ins too. Without nullDescriptor the session's GL goes to llvmpipe. - gamescope 0129: a refused compute pipeline is named with its SPIR-V capabilities. - Compositor: a buffer on two surfaces at once (gamescope's planes while a notification is up) is released only when the last one lets go. - Open driver pairs: processes.txt timeline for the first three minutes; Setup "Steam interface: GPU drawing" turns off software web compositing. --- app/src/main/cpp/waylandcomp/src/compositor.c | 22 +++++- .../com/droiddeck/launcher/MainActivity.kt | 10 ++- .../launcher/session/GraphicsReport.kt | 7 ++ .../launcher/session/SessionPrefs.kt | 19 ++++- .../launcher/session/SessionService.kt | 15 +++- .../launcher/session/VenusComponent.kt | 5 ++ .../droiddeck/launcher/ui/FrontEndScreen.kt | 3 + .../droiddeck/launcher/ui/FrontEndSetup.kt | 3 + app/src/main/res/values/strings.xml | 3 + tools/gamescope/PATCHES.md | 4 ++ .../0129-log-failing-compute-pipeline.patch | 55 +++++++++++++++ .../overlay/usr/local/bin/droiddeck-session | 69 ++++++++++++++++++- tools/tests/test_vk_dmabuf_quarantine.py | 4 ++ tools/venus/layer/draw.c | 51 ++++++++++++-- tools/venus/layer/layer.c | 16 +++-- tools/venus/layer/mali_compat.h | 5 ++ tools/vkbridge/client/physdev.c | 9 ++- 17 files changed, 282 insertions(+), 18 deletions(-) create mode 100644 tools/gamescope/patches/0129-log-failing-compute-pipeline.patch diff --git a/app/src/main/cpp/waylandcomp/src/compositor.c b/app/src/main/cpp/waylandcomp/src/compositor.c index e51d934c3..9c937fff9 100644 --- a/app/src/main/cpp/waylandcomp/src/compositor.c +++ b/app/src/main/cpp/waylandcomp/src/compositor.c @@ -420,6 +420,19 @@ static void send_buffer_release_at(struct wl_resource *buffer, int line) { if (!buffer_release_blocked(buffer)) { buf_trace_release(buffer, line); wl_buffer_send_release(buffer); } } +/* Whether a surface other than `except` is showing this buffer right now. gamescope's Wayland + * backend moves one buffer between its planes (the game's frame goes from the window to a + * subsurface above a black backing while a Steam notification is up): releasing it when the first + * surface lets go handed gamescope a buffer still on screen ("Compositor released us but we were + * not acquired"). Only the last surface to let go releases it. */ +static int buffer_shown_elsewhere(struct wl_resource *buffer, const struct surface *except) { + struct surface *o; + if (!buffer) return 0; + wl_list_for_each(o, &g_surfaces, link) + if (o != except && o->dmabuf == buffer) return 1; + return 0; +} + static void pending_release_free(struct pending_release *pr) { if (pr->surface && pr->surface->releases_pending > 0) pr->surface->releases_pending--; wl_list_remove(&pr->destroy.link); @@ -459,6 +472,11 @@ static int on_release_timer(int fd, uint32_t mask, void *data) { if (read(fd, &expirations, sizeof(expirations)) < 0 && errno != EAGAIN) return 0; wl_list_for_each_safe(pr, tmp, &g_pending_releases, link) { if (pr->at_ns <= now && !buffer_release_blocked(pr->buffer)) { + /* Attached again since (to its surface or another): that surface releases it later. */ + if (buffer_shown_elsewhere(pr->buffer, NULL)) { + pending_release_free(pr); + continue; + } send_buffer_release(pr->buffer); perf_note_release(pr->since_ns); pending_release_free(pr); @@ -735,7 +753,9 @@ static void drop_dmabuf(struct surface *s, int paced) { wl_list_remove(&s->dmabuf_destroy.link); /* A buffer on the zero-copy layer is the display's until SurfaceFlinger says otherwise: * ahb_swapchain.c releases it then. */ - if (!ahb_swapchain_defer_release(s->dmabuf_buf, s->dmabuf, s, paced)) { + if (buffer_shown_elsewhere(s->dmabuf, s)) { + /* still on another surface: released when that one lets go */ + } else if (!ahb_swapchain_defer_release(s->dmabuf_buf, s->dmabuf, s, paced)) { if (paced) release_buffer(s, s->dmabuf, now_ns()); else send_buffer_release(s->dmabuf); } diff --git a/app/src/main/java/com/droiddeck/launcher/MainActivity.kt b/app/src/main/java/com/droiddeck/launcher/MainActivity.kt index 384334683..cba5dbed8 100644 --- a/app/src/main/java/com/droiddeck/launcher/MainActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/MainActivity.kt @@ -370,6 +370,8 @@ class MainActivity : ComponentActivity() { private var logsEnabled by mutableStateOf(true) private var agentCommands by mutableStateOf(false) private var graphicsLogs by mutableStateOf(false) + private var steamGpuDrawing by mutableStateOf(false) + private var bridgeFellBack by mutableStateOf(false) private var showRoms by mutableStateOf(false) private var homeAppSelected by mutableStateOf(false) private var homeScreenEnabled by mutableStateOf(false) @@ -565,7 +567,7 @@ class MainActivity : ComponentActivity() { busy = busy, stage = stage, percent = percent, desktopInstalled = desktopInstalled, desktopPresent = desktopPresent, offlineAccount = offlineAccount, offline = offline, - frameGenLabel = frameGenLabel, romsDir = romsDir, logsEnabled = logsEnabled, agentCommands = agentCommands, graphicsLogs = graphicsLogs, + frameGenLabel = frameGenLabel, romsDir = romsDir, logsEnabled = logsEnabled, agentCommands = agentCommands, graphicsLogs = graphicsLogs, steamGpuDrawing = steamGpuDrawing, bridgeFellBack = bridgeFellBack, steamGames = steamGames, emulators = emulatorList, running = runningLabel, frameGen = FrameGen.mode(this), lossless = lossless, @@ -712,6 +714,10 @@ class MainActivity : ComponentActivity() { SessionPrefs.setLogsEnabled(this, !SessionPrefs.logsEnabled(this)) logsEnabled = SessionPrefs.logsEnabled(this) }, + onSteamGpuDrawing = { + SessionPrefs.setSteamGpuCompositing(this, !SessionPrefs.steamGpuCompositing(this)) + steamGpuDrawing = SessionPrefs.steamGpuCompositing(this) + }, onGraphicsLogs = { SessionPrefs.setDetailedGraphicsLogs(this, !SessionPrefs.detailedGraphicsLogs(this)) graphicsLogs = SessionPrefs.detailedGraphicsLogs(this) @@ -1615,6 +1621,8 @@ class MainActivity : ComponentActivity() { logsEnabled = SessionPrefs.logsEnabled(this) agentCommands = com.droiddeck.launcher.agent.AgentAccess.commandsEnabled(this) graphicsLogs = SessionPrefs.detailedGraphicsLogs(this) + steamGpuDrawing = SessionPrefs.steamGpuCompositing(this) + bridgeFellBack = SessionPrefs.bridgeFellBack(this) runningLabel = if (SessionState.running) when (SessionState.mode) { SessionService.MODE_DESKTOP -> getString(R.string.rail_desktop) SessionService.MODE_RUN -> Library.nameForProgram(SessionState.program) diff --git a/app/src/main/java/com/droiddeck/launcher/session/GraphicsReport.kt b/app/src/main/java/com/droiddeck/launcher/session/GraphicsReport.kt index 3d7247d8c..52af5a21f 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/GraphicsReport.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/GraphicsReport.kt @@ -41,8 +41,12 @@ object GraphicsReport { /** Per-game graphics logs and the guest's Vulkan summary: on for a non-Adreno GPU, or by Setup's switch. */ fun detailed(context: Context): Boolean = !DeviceSupport.adreno() || SessionPrefs.detailedGraphicsLogs(context) + /** For finish(), which runs from the session's end without a context of its own. */ + @Volatile private var appContext: Context? = null + /** Writes the start of the report; never throws. */ fun begin(context: Context, dir: File, guest: List) { + appContext = context.applicationContext runCatching { File(dir, FILE).writeText(start(context, guest)) } .onFailure { android.util.Log.w("GraphicsReport", "could not write $FILE", it) } } @@ -177,10 +181,13 @@ object GraphicsReport { val guestLines = File(dir, "session.log").takeIf { it.isFile }?.useLines { seq -> seq.filter { GUEST.containsMatchIn(it) }.take(200).toList() }.orEmpty() + val fellBack = File(dir, "vkbridge-fallback").takeIf { it.isFile }?.readText()?.trim() + runCatching { SessionPrefs.setBridgeFellBack(appContext ?: return@runCatching, fellBack != null) } val limits = File(dir, "limits.txt").takeIf { it.isFile }?.readText()?.trim() val audio = File(dir, "audio.log").takeIf { it.isFile }?.readText().orEmpty() report.appendText(buildString { limits?.let { appendLine("open files limit: $it") } + fellBack?.let { appendLine("!! MALI BRIDGE FELL BACK: the self-test failed images or draws; the session ran on $it") } if (audio.contains("AAUDIO_ERROR_DISCONNECTED")) { appendLine("!! SOUND: Android refused the audio output (AAUDIO_ERROR_DISCONNECTED) - rebooting the phone usually fixes this") } diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt index 6770bfa50..09f858bde 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt @@ -515,12 +515,29 @@ object SessionPrefs { */ fun venus(context: Context): Boolean = prefs(context).getBoolean("venus", false) + /** The last session's Mali bridge self-test failed and it ran on the pair instead (Setup says so). */ + fun bridgeFellBack(context: Context): Boolean = prefs(context).getBoolean("bridgeFellBack", false) + + fun setBridgeFellBack(context: Context, on: Boolean) { + prefs(context).edit().putBoolean("bridgeFellBack", on).apply() + } + + /** + * Setup's "Steam interface: GPU drawing": with an open driver pair the Steam client composites + * its web views in software (stale tiles on RADV/Xclipse); this turns GPU compositing back on. + */ + fun steamGpuCompositing(context: Context): Boolean = prefs(context).getBoolean("steamGpuCompositing", false) + + fun setSteamGpuCompositing(context: Context, on: Boolean) { + prefs(context).edit().putBoolean("steamGpuCompositing", on).apply() + } + fun setVenus(context: Context, on: Boolean) { prefs(context).edit().putBoolean("venus", on).apply() } fun setVkBridge(context: Context, on: Boolean) { - prefs(context).edit().putBoolean("vkBridge", on).apply() + prefs(context).edit().putBoolean("vkBridge", on).putBoolean("bridgeFellBack", false).apply() } fun setZinkLazy(context: Context, on: Boolean) { diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt index 532c3a77f..719688bf6 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt @@ -700,10 +700,15 @@ class SessionService : Service() { if (!bridge && com.droiddeck.launcher.core.DeviceSupport.openDriverActive(this)) { // CEF's accelerated shared-texture output showed stale, half-written tiles on RADV // (Xclipse), as #570 found on PowerVR: web compositing in software, GPU raster kept. - guest.add("BL_STEAM_SOFT_COMPOSITING=1") + // Setup's "Steam interface: GPU drawing" turns GPU compositing back on (Xclipse users). + if (SessionPrefs.steamGpuCompositing(this)) { + Log.i(TAG, "steam: GPU web compositing (Setup's \"Steam interface: GPU drawing\")") + } else { + guest.add("BL_STEAM_SOFT_COMPOSITING=1") + Log.i(TAG, "steam: software web compositing for the open driver pair (GPU raster kept)") + } // droiddeck-game-env leaves vkd3d-proton to report the driver's own caps on this path. guest.add("BL_OPEN_DRIVER=1") - Log.i(TAG, "steam: software web compositing for the open driver pair (GPU raster kept)") LinuxRuntime.gpuStandIn(this)?.takeIf { it == LinuxRuntime.MALI_DEVICE }?.let { guest.add("BL_DRM_STANDIN=$it") } } // Turnip's own debug switches, for the runtime's driver and everything on it. The file in @@ -786,6 +791,12 @@ class SessionService : Service() { components.add(it) } guest.add("BL_VKBRIDGE=1") + if (SessionPrefs.steamGpuCompositing(this)) guest.add("BL_STEAM_GPU_COMPOSITING=1") + // If the bridge's self-test fails images or draws, the session script falls back to the + // Mali driver pair set for the runtime (its ICD here), rather than crashing gamescope. + SessionPrefs.linuxDriver(this).takeIf { it.isNotEmpty() } + ?.let { runCatching { LinuxVulkanDriver.resolveIcdPath(this, it) }.getOrNull() } + ?.let { guest.add("BL_VKBRIDGE_FALLBACK_ICD=$it") } guest.add("VKBRIDGE_SOCKET=" + socket.path) guest.add("VKBRIDGE_CLIENT_LOG=" + File(sessionDir, "vkbridge-client.log").path) // The render node gamescope sees is Mali's device standing in (LinuxRuntime.bindGpuNode): diff --git a/app/src/main/java/com/droiddeck/launcher/session/VenusComponent.kt b/app/src/main/java/com/droiddeck/launcher/session/VenusComponent.kt index de5a3a160..5a4fb04ce 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/VenusComponent.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/VenusComponent.kt @@ -89,6 +89,11 @@ class VenusComponent(private val logFile: File?) : SessionPart() { "VTEST_SOCKET_NAME=" + socket(context).path, // Xwayland has no DRI3 without a DRM node, so swapchains present through shared memory. "MESA_VK_WSI_DEBUG=sw", + // The Mali compatibility layer in every process but gamescope, not only Wine: Zink + // (Steam's GL, Xwayland's glamor) requires robustness2's nullDescriptor, which the + // layer stands in for. The session script checks it and falls back to llvmpipe. + "DROIDDECK_MALI_COMPAT=1", + "BL_VENUS=1", ) } diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt index 0d5ddf29e..ebd63e06e 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt @@ -100,6 +100,8 @@ class FrontEndState( /** Setup's Debugging tools toggle (AgentAccess): off unless the user turns it on. */ val agentCommands: Boolean = false, val graphicsLogs: Boolean = false, + val steamGpuDrawing: Boolean = false, + val bridgeFellBack: Boolean = false, val steamGames: List, val emulators: List, val running: String?, @@ -213,6 +215,7 @@ class FrontEndActions( val onLogs: () -> Unit, val onAgentCommands: () -> Unit = {}, val onGraphicsLogs: () -> Unit = {}, + val onSteamGpuDrawing: () -> Unit = {}, val onShareLogs: () -> Unit = {}, val onClearLogs: () -> Unit = {}, val onOffline: () -> Unit, diff --git a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt index 60dc5ae84..2264ece70 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt @@ -213,8 +213,10 @@ internal fun SetupPanel( com.droiddeck.launcher.gpu.GpuInfo.Support.TESTED -> stringResource(R.string.setup_gpu_detail, gpu.displayName(ctx), gpuName) com.droiddeck.launcher.gpu.GpuInfo.Support.UNTESTED -> if (!s.adreno && s.vkBridge && !com.droiddeck.launcher.core.DeviceSupport.bridgeUsable()) stringResource(R.string.setup_gpu_bridge_unusable, gpu.displayName(ctx)) + else if (!s.adreno && s.vkBridge && s.bridgeFellBack) stringResource(R.string.setup_gpu_bridge_fell_back, gpu.displayName(ctx)) else stringResource(R.string.setup_gpu_untested_detail, gpu.displayName(ctx), gpu.supportText(ctx).replaceFirstChar { it.lowercase() }) else -> if (s.vkBridge && !com.droiddeck.launcher.core.DeviceSupport.bridgeUsable()) stringResource(R.string.setup_gpu_bridge_unusable, gpuName) + else if (s.vkBridge && s.bridgeFellBack) stringResource(R.string.setup_gpu_bridge_fell_back, gpuName) else if (s.vkBridge) stringResource(R.string.setup_gpu_bridge_detail, gpuName) else stringResource(R.string.setup_gpu_unsupported_detail, gpuName) }, @@ -420,6 +422,7 @@ internal fun SetupPanel( ToggleRow(host, "logs", stringResource(R.string.setup_logs), stringResource(R.string.setup_logs_hint), s.logsEnabled) { a.onLogs() } ToggleRow(host, "agent-commands", stringResource(R.string.setup_agent_commands), null, s.agentCommands) { a.onAgentCommands() } ToggleRow(host, "graphics-logs", stringResource(R.string.setup_graphics_logs), stringResource(R.string.setup_graphics_logs_hint), s.graphicsLogs) { a.onGraphicsLogs() } + if (!s.adreno) ToggleRow(host, "steam-gpu-drawing", stringResource(R.string.setup_steam_gpu_drawing), stringResource(R.string.setup_steam_gpu_drawing_hint), s.steamGpuDrawing) { a.onSteamGpuDrawing() } ActionRow(stringResource(R.string.setup_latest_logs), stringResource(R.string.drawer_logs_hint), stringResource(R.string.drawer_share_logs), a.onShareLogs, progress = com.droiddeck.launcher.session.SessionLogShare.progress) ActionRow(stringResource(R.string.setup_saved_logs), stringResource(R.string.setup_saved_logs_hint, com.droiddeck.launcher.session.SessionPaths.KEEP_SESSIONS), stringResource(R.string.setup_clear_logs), a.onClearLogs) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 717f7f67a..d89799d0e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -680,6 +680,9 @@ Saved after each session Debugging tools Detailed graphics logs + Steam interface: GPU drawing + With a Mali or Xclipse driver pair, Steam draws its pages in software (GPU drawing showed broken tiles). Turn on for a faster Steam interface if it looks right on this phone. + The Mali bridge failed its self-test on %1$s last session (images or draws), so the session used the PanVK driver pair instead. Each game\'s Proton, DXVK and driver logs in the session\'s logs. Always on when the GPU is not an Adreno. Latest session logs Saved session logs diff --git a/tools/gamescope/PATCHES.md b/tools/gamescope/PATCHES.md index 9dd697b1b..05a355ed3 100644 --- a/tools/gamescope/PATCHES.md +++ b/tools/gamescope/PATCHES.md @@ -74,3 +74,7 @@ app's Wayland-hosted gamescope never reaches, or need a newer gamescope than the import, so the screen cycled through the first few frames forever while Steam ran on behind them. Only dma-buf textures are memoized now; a shm buffer is uploaded on every commit. Adreno never takes this path, since its Xwayland hands over dma-bufs. Also what XWAYLAND_NO_GLAMOR sessions need. +- `0129-log-failing-compute-pipeline.patch`: when `vkCreateComputePipelines` fails, one extra line + per shader type names the pipeline (shader type, layer count, ycbcr/colorspace masks, fp16) and the + SPIR-V capabilities its shader declares. A Mali-G615 through Venus refused all 119 composite + pipelines with VK_ERROR_INITIALIZATION_FAILED and nothing said which feature it objected to. diff --git a/tools/gamescope/patches/0129-log-failing-compute-pipeline.patch b/tools/gamescope/patches/0129-log-failing-compute-pipeline.patch new file mode 100644 index 000000000..3d5ebc00f --- /dev/null +++ b/tools/gamescope/patches/0129-log-failing-compute-pipeline.patch @@ -0,0 +1,55 @@ +diff --git a/src/rendervulkan.cpp b/src/rendervulkan.cpp +index 42b2ec6..103baee 100644 +--- a/src/rendervulkan.cpp ++++ b/src/rendervulkan.cpp +@@ -994,6 +994,8 @@ bool CVulkanDevice::createPools() + return true; + } + ++static std::string s_shaderCaps[SHADER_TYPE_COUNT]; ++ + bool CVulkanDevice::createShaders() + { + struct ShaderInfo_t +@@ -1040,6 +1042,25 @@ bool CVulkanDevice::createShaders() + .pCode = shaderInfos[i].spirv, + }; + ++ // The SPIR-V capabilities each shader declares, for the log when its pipeline is refused. ++ { ++ const uint32_t *pWords = shaderInfos[i].spirv; ++ size_t nWords = shaderInfos[i].size / sizeof(uint32_t); ++ std::string &sCaps = s_shaderCaps[i]; ++ sCaps.clear(); ++ for ( size_t w = 5; w < nWords; ) ++ { ++ uint32_t uLen = pWords[w] >> 16; ++ if ( !uLen ) ++ break; ++ if ( ( pWords[w] & 0xffffu ) == 17 /* OpCapability */ && w + 1 < nWords ) ++ sCaps += " " + std::to_string( pWords[w + 1] ); ++ else if ( ( pWords[w] & 0xffffu ) == 54 /* OpFunction: past the header */ ) ++ break; ++ w += uLen; ++ } ++ } ++ + VkResult res = vk.CreateShaderModule(device(), &shaderCreateInfo, nullptr, &m_shaderModules[i]); + if ( res != VK_SUCCESS ) + { +@@ -1240,6 +1261,15 @@ VkPipeline CVulkanDevice::compilePipeline(uint32_t layerCount, uint32_t ycbcrMas + VkResult res = vk.CreateComputePipelines(device(), VK_NULL_HANDLE, 1, &computePipelineCreateInfo, nullptr, &result); + if (res != VK_SUCCESS) { + vk_errorf( res, "vkCreateComputePipelines failed" ); ++ // Which pipeline, once per shader type: a vendor driver through Venus may refuse one variant only. ++ static uint32_t s_uLoggedTypes = 0; ++ if ( !( s_uLoggedTypes & ( 1u << ( (uint32_t)type & 31u ) ) ) ) ++ { ++ s_uLoggedTypes |= 1u << ( (uint32_t)type & 31u ); ++ vk_log.errorf( " failing compute pipeline: shader type %u, layers %u, ycbcr mask 0x%x, blur layers %u, colorspace mask 0x%x, output eotf %u, itm %u, fp16 %d, SPIR-V capabilities:%s", ++ (uint32_t)type, layerCount, ycbcrMask, blur_layer_count, colorspace_mask, (uint32_t)output_eotf, (uint32_t)itm_enable, m_bSupportsFp16 ? 1 : 0, ++ (uint32_t)type < SHADER_TYPE_COUNT ? s_shaderCaps[type].c_str() : "" ); ++ } + return VK_NULL_HANDLE; + } + diff --git a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session index aff2f38a4..b8e282263 100755 --- a/tools/linuxfs/overlay/usr/local/bin/droiddeck-session +++ b/tools/linuxfs/overlay/usr/local/bin/droiddeck-session @@ -162,17 +162,50 @@ if [ "${BL_VKBRIDGE:-0}" = 1 ]; then unset TU_DEBUG if [ -z "${BL_INSIDE:-}" ]; then step "vkbridge self-test" - timeout 60 /usr/local/bin/droiddeck-vkbridge-smoke 2>&1 | sed 's/^/== vkbridge: /' + bl_vkb_smoke=$(timeout 60 /usr/local/bin/droiddeck-vkbridge-smoke 2>&1) + printf '%s\n' "$bl_vkb_smoke" | sed 's/^/== vkbridge: /' + # Images or draws failing the self-test mean gamescope cannot start on the bridge (it + # asserted in CVulkanTexture::BInit): this session falls back to the Mali driver pair the + # app named, and says so (the app shows it in Setup). + if printf '%s\n' "$bl_vkb_smoke" | grep -qE '^FAIL (image clear|draw)' && [ -r "${BL_VKBRIDGE_FALLBACK_ICD:-}" ]; then + export VK_DRIVER_FILES=$BL_VKBRIDGE_FALLBACK_ICD VK_ICD_FILENAMES=$BL_VKBRIDGE_FALLBACK_ICD BL_VKBRIDGE=0 + # What the app sets for a pair session (SessionService): the bridge already bound /dev/mali0 + # as the stand-in render node, which is what PanVK reports too. + export BL_OPEN_DRIVER=1 BL_DRM_STANDIN=/dev/mali0 + [ "${BL_STEAM_GPU_COMPOSITING:-0}" = 1 ] || export BL_STEAM_SOFT_COMPOSITING=1 + echo "== vkbridge: self-test failed images or draws; this session uses the PanVK pair instead ($BL_VKBRIDGE_FALLBACK_ICD)" + [ -n "${BL_DEBUG_DIR:-}" ] && echo "$BL_VKBRIDGE_FALLBACK_ICD" > "$BL_DEBUG_DIR/vkbridge-fallback" 2>/dev/null + fi + unset bl_vkb_smoke if command -v vulkaninfo >/dev/null 2>&1; then timeout 30 vulkaninfo --summary 2>&1 | sed -n '/^Devices:/,$p' | head -40 | sed 's/^/== vulkaninfo: /' fi fi - echo "== vulkan driver: Mali bridge ($bl_vkb_icd, server at $VKBRIDGE_SOCKET)" + [ "${BL_VKBRIDGE:-0}" = 1 ] && echo "== vulkan driver: Mali bridge ($bl_vkb_icd, server at $VKBRIDGE_SOCKET)" else echo "== vulkan driver: Mali bridge requested but NOT usable (icd $bl_vkb_icd, socket ${VKBRIDGE_SOCKET:-unset}); this GPU will show a black screen" fi unset bl_vkb_icd fi +# Mali (Venus): Steam's GL and Xwayland's glamor run on Zink, which refuses a device without +# robustness2's nullDescriptor (Mali has none; the Mali compatibility layer stands in for it). When +# the session's Vulkan still lacks it, GL goes to llvmpipe instead: slower, but Steam starts rather +# than sitting at 0% with "glXChooseVisual failed". +if [ "${BL_VENUS:-0}" = 1 ] && [ -z "${BL_INSIDE:-}" ] && command -v vulkaninfo >/dev/null 2>&1; then + if bl_vn_info=$(timeout 30 vulkaninfo 2>/dev/null); then + case $(printf '%s\n' "$bl_vn_info" | grep -m1 'nullDescriptor') in + *true*) echo "== venus: nullDescriptor present (Zink keeps the GPU)" ;; + *) + export GALLIUM_DRIVER=llvmpipe LIBGL_ALWAYS_SOFTWARE=1 + unset MESA_LOADER_DRIVER_OVERRIDE LIBGL_KOPPER_DRI2 + echo "== venus: no nullDescriptor on this device even with the Mali compatibility layer; Steam's GL and Xwayland use llvmpipe (software)" + ;; + esac + else + echo "== venus: vulkaninfo failed or timed out; GL left on Zink" + fi + unset bl_vn_info +fi # Detailed graphics logs (BL_GAME_LOGS=1: a GPU that is not an Adreno, or Setup's switch): the # session's own view of Vulkan, with the driver exactly as exported above, once per session. In the # background with a timeout, so a driver that hangs here never holds the session up. @@ -213,6 +246,38 @@ nap() { [ -n "${bl_nap_fd:-}" ] || exec {bl_nap_fd}<> <(:) read -r -t "$1" -u "$bl_nap_fd" _ 2>/dev/null || : } +# Which program a driver error came from: Mesa's lines ("radv/amdgpu: The CS has been rejected", +# "ZINK: vkQueueWaitIdle failed") carry no process name. For the first three minutes of an open +# driver pair's session, every process that appears goes into processes.txt with the session log's +# line count at that moment, so an error's line number in session.log points at what had just +# started. Pure bash over /proc (no execs: see nap). +watch_processes() { + local out=$BL_DEBUG_DIR/processes.txt log=$BL_DEBUG_DIR/session.log i p pid comm lines + local -a argv new + local -A seen=() + echo "# seconds session.log-lines pid name command line" > "$out" 2>/dev/null || return 0 + for ((i = 0; i < 360; i++)); do + new=() + for p in /proc/[0-9]*; do + pid=${p#/proc/} + [ -n "${seen[$pid]:-}" ] && continue + seen[$pid]=1 + read -r comm < "$p/comm" 2>/dev/null || continue + argv=() + mapfile -d '' -t argv < "$p/cmdline" 2>/dev/null + new+=("$pid"$'\t'"$comm"$'\t'"${argv[*]:0:8}") + done + if [ ${#new[@]} -gt 0 ]; then + lines=0 + [ -r "$log" ] && { mapfile -t argv < "$log"; lines=${#argv[@]}; argv=(); } + for p in "${new[@]}"; do printf '%s\t%s\t%.200s\n' "$SECONDS" "$lines" "$p"; done >> "$out" + fi + nap 0.5 + done +} +if [ "${BL_OPEN_DRIVER:-0}" = 1 ] && [ -z "${BL_INSIDE:-}" ] && [ -n "${BL_DEBUG_DIR:-}" ]; then + watch_processes & +fi export XDG_DATA_HOME=${XDG_DATA_HOME:-$HOME/.local/share} export XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-$HOME/.config} export XDG_CACHE_HOME=${XDG_CACHE_HOME:-$HOME/.cache} diff --git a/tools/tests/test_vk_dmabuf_quarantine.py b/tools/tests/test_vk_dmabuf_quarantine.py index 009b5640b..f954a2e23 100644 --- a/tools/tests/test_vk_dmabuf_quarantine.py +++ b/tools/tests/test_vk_dmabuf_quarantine.py @@ -47,6 +47,10 @@ class VkDmabufQuarantineTest(unittest.TestCase): helpers += [function(comp, 'send_buffer_release')] else: helpers = ['static int __attribute__((unused)) buffer_release_blocked(struct wl_resource *b) { (void)b; return 0; }'] + if re.search(r'\bstatic int buffer_shown_elsewhere\(', comp): + # One surface per buffer in these scenarios: never shown elsewhere. + helpers += ['static int buffer_shown_elsewhere(struct wl_resource *b, const struct surface *e) ' + '{ (void)b; (void)e; return 0; }'] query = function(vk, 'vkp_completion_unknown') if 'int vkp_completion_unknown(' in vk else \ 'int vkp_completion_unknown(void) { return g_dev_state == -3; }' comp_functions = [function(comp, names[0])] + helpers + [function(comp, n) for n in names[1:]] diff --git a/tools/venus/layer/draw.c b/tools/venus/layer/draw.c index d121e933b..f3621355b 100644 --- a/tools/venus/layer/draw.c +++ b/tools/venus/layer/draw.c @@ -6,7 +6,8 @@ * - robustness2's nullDescriptor: VK_NULL_HANDLE buffers, image views and vertex buffers in * descriptor writes (plain, template and push) and vertex-buffer binds are replaced with small * stand-ins the layer owns. robustBufferAccess2 itself maps to robustBufferAccess (layer.c). - * A texel-buffer view stays null: its format is not known here. + * A null texel-buffer view gets an RGBA8 (uniform) or R32_UINT (storage) view of the stand-in + * buffer: its format is not known here, but Zink leaves them unbound and never reads them. */ #include #include @@ -72,7 +73,8 @@ static bool null_resources(struct device *dev) .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, .size = NULL_BUFFER_SIZE, .usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_BUFFER_BIT | - VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT, + VK_BUFFER_USAGE_VERTEX_BUFFER_BIT | VK_BUFFER_USAGE_INDEX_BUFFER_BIT | + VK_BUFFER_USAGE_UNIFORM_TEXEL_BUFFER_BIT | VK_BUFFER_USAGE_STORAGE_TEXEL_BUFFER_BIT, .sharingMode = VK_SHARING_MODE_EXCLUSIVE, }; if (dev->CreateBuffer(dev->handle, &binfo, NULL, &dev->null_buffer) != VK_SUCCESS) @@ -130,6 +132,18 @@ static bool null_resources(struct device *dev) }; if (dev->CreateImageView(dev->handle, &vinfo, NULL, &dev->null_view) != VK_SUCCESS) return false; + /* Texel views are optional: a write that needs one and has none keeps its null. */ + VkBufferViewCreateInfo tinfo = { + .sType = VK_STRUCTURE_TYPE_BUFFER_VIEW_CREATE_INFO, + .buffer = dev->null_buffer, + .format = VK_FORMAT_R8G8B8A8_UNORM, + .range = VK_WHOLE_SIZE, + }; + if (dev->CreateBufferView(dev->handle, &tinfo, NULL, &dev->null_uniform_texel) != VK_SUCCESS) + dev->null_uniform_texel = VK_NULL_HANDLE; + tinfo.format = VK_FORMAT_R32_UINT; + if (dev->CreateBufferView(dev->handle, &tinfo, NULL, &dev->null_storage_texel) != VK_SUCCESS) + dev->null_storage_texel = VK_NULL_HANDLE; dev->null_ready = 1; layer_log("nullDescriptor: stand-in buffer and 1x1 image ready"); return true; @@ -145,6 +159,8 @@ void null_resources_destroy(struct device *dev) } } map_free(&dev->templates); + if (dev->null_uniform_texel) dev->DestroyBufferView(dev->handle, dev->null_uniform_texel, NULL); + if (dev->null_storage_texel) dev->DestroyBufferView(dev->handle, dev->null_storage_texel, NULL); if (dev->null_view) dev->DestroyImageView(dev->handle, dev->null_view, NULL); if (dev->null_image) dev->DestroyImage(dev->handle, dev->null_image, NULL); if (dev->null_image_memory) dev->FreeMemory(dev->handle, dev->null_image_memory, NULL); @@ -164,6 +180,18 @@ static bool is_image_type(VkDescriptorType t) t == VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER || t == VK_DESCRIPTOR_TYPE_INPUT_ATTACHMENT; } +static bool is_texel_type(VkDescriptorType t) +{ + return t == VK_DESCRIPTOR_TYPE_UNIFORM_TEXEL_BUFFER || t == VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER; +} + +static void fix_texel(struct device *dev, VkDescriptorType t, VkBufferView *view) +{ + if (*view != VK_NULL_HANDLE || !null_resources(dev)) + return; + *view = t == VK_DESCRIPTOR_TYPE_STORAGE_TEXEL_BUFFER ? dev->null_storage_texel : dev->null_uniform_texel; +} + static void fix_buffer(struct device *dev, VkDescriptorBufferInfo *info) { if (info->buffer != VK_NULL_HANDLE || !null_resources(dev)) @@ -194,6 +222,8 @@ static VkWriteDescriptorSet *fix_writes(struct device *dev, uint32_t count, cons if (is_image_type(w->descriptorType) && w->pImageInfo && w->pImageInfo[j].imageView == VK_NULL_HANDLE && w->descriptorType != VK_DESCRIPTOR_TYPE_SAMPLER) null_found = true; + if (is_texel_type(w->descriptorType) && w->pTexelBufferView && w->pTexelBufferView[j] == VK_NULL_HANDLE) + null_found = true; } if (!null_found) continue; @@ -209,6 +239,13 @@ static VkWriteDescriptorSet *fix_writes(struct device *dev, uint32_t count, cons fix_buffer(dev, &b[j]); copies[i].pBufferInfo = b; (*scratch)[(*scratch_count)++] = b; + } else if (is_texel_type(w->descriptorType)) { + VkBufferView *v = malloc(w->descriptorCount * sizeof(*v)); + memcpy(v, w->pTexelBufferView, w->descriptorCount * sizeof(*v)); + for (uint32_t j = 0; j < w->descriptorCount; j++) + fix_texel(dev, w->descriptorType, &v[j]); + copies[i].pTexelBufferView = v; + (*scratch)[(*scratch_count)++] = v; } else { VkDescriptorImageInfo *im = malloc(w->descriptorCount * sizeof(*im)); memcpy(im, w->pImageInfo, w->descriptorCount * sizeof(*im)); @@ -320,13 +357,15 @@ static void *fix_template_data(struct device *dev, VkDescriptorUpdateTemplate tm const VkDescriptorUpdateTemplateEntry *e = &t->entries[i]; bool buffer = is_buffer_type(e->descriptorType); bool image = is_image_type(e->descriptorType); - if (!buffer && !image) + bool texel = is_texel_type(e->descriptorType); + if (!buffer && !image && !texel) continue; for (uint32_t j = 0; j < e->descriptorCount; j++) { size_t at = e->offset + (size_t)j * e->stride; const char *src = (const char *)(copy ? copy : data) + at; - bool null_handle = buffer ? ((const VkDescriptorBufferInfo *)src)->buffer == VK_NULL_HANDLE - : ((const VkDescriptorImageInfo *)src)->imageView == VK_NULL_HANDLE; + bool null_handle = buffer ? ((const VkDescriptorBufferInfo *)src)->buffer == VK_NULL_HANDLE + : texel ? *(const VkBufferView *)src == VK_NULL_HANDLE + : ((const VkDescriptorImageInfo *)src)->imageView == VK_NULL_HANDLE; if (!null_handle) continue; if (!copy) { @@ -335,6 +374,8 @@ static void *fix_template_data(struct device *dev, VkDescriptorUpdateTemplate tm } if (buffer) fix_buffer(dev, (VkDescriptorBufferInfo *)((char *)copy + at)); + else if (texel) + fix_texel(dev, e->descriptorType, (VkBufferView *)((char *)copy + at)); else fix_image(dev, (VkDescriptorImageInfo *)((char *)copy + at)); } diff --git a/tools/venus/layer/layer.c b/tools/venus/layer/layer.c index 9b2d34e03..cded94335 100644 --- a/tools/venus/layer/layer.c +++ b/tools/venus/layer/layer.c @@ -1,5 +1,7 @@ +#define _GNU_SOURCE #include "mali_compat.h" +#include #include #include #include @@ -133,13 +135,19 @@ void map_free(struct handle_map *map) *map = (struct handle_map){0}; } -/* Wine processes only: the Steam client and gamescope run fine without the layer, and their - * Zink would otherwise start using the made-up features too. */ +/* Wine processes by default. DROIDDECK_MALI_COMPAT=1 (the app's Venus route) turns it on everywhere + * but gamescope, so Zink in the Steam client and Xwayland gets robustness2's nullDescriptor, which it + * requires; gamescope runs fine on the driver's own features. =all includes gamescope too. */ static bool wanted(void) { const char *setting = getenv("DROIDDECK_MALI_COMPAT"); - if (setting) - return strcmp(setting, "0") != 0; + if (setting) { + if (strcmp(setting, "0") == 0) + return false; + if (strcmp(setting, "all") == 0) + return true; + return strcmp(program_invocation_short_name, "gamescope") != 0; + } return getenv("WINEPREFIX") != NULL; } diff --git a/tools/venus/layer/mali_compat.h b/tools/venus/layer/mali_compat.h index ff4ed53e6..9a356f1cb 100644 --- a/tools/venus/layer/mali_compat.h +++ b/tools/venus/layer/mali_compat.h @@ -119,6 +119,8 @@ struct instance { X(GetImageMemoryRequirements) \ X(BindImageMemory) \ X(DestroyImageView) \ + X(CreateBufferView) \ + X(DestroyBufferView) \ X(UpdateDescriptorSets) \ X(CmdPushDescriptorSet) \ X(CreateDescriptorUpdateTemplate) \ @@ -157,6 +159,9 @@ struct device { VkImage null_image; VkDeviceMemory null_image_memory; VkImageView null_view; + /* Texel-buffer stand-ins: RGBA8 for uniform (sampled) texel buffers, R32_UINT for storage. */ + VkBufferView null_uniform_texel; + VkBufferView null_storage_texel; struct device *next; }; diff --git a/tools/vkbridge/client/physdev.c b/tools/vkbridge/client/physdev.c index 2f655f9b6..c545d32ff 100644 --- a/tools/vkbridge/client/physdev.c +++ b/tools/vkbridge/client/physdev.c @@ -30,8 +30,12 @@ static void build_memory_model(vkb_physdev *pd) int pure_dl = -1; for (uint32_t t = 0; t < s->memoryTypeCount; t++) { VkMemoryPropertyFlags f = s->memoryTypes[t].propertyFlags; + /* Not a lazily allocated type (Mali lists one first, for transient attachments): an + * ordinary image cannot use it, and taking it as "the" device-local type left images with + * no memory type at all (self-test "image clear: setup", gamescope "No compatible image + * memory type"). */ if ((f & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) && !(f & VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT) && - !(f & VK_MEMORY_PROPERTY_PROTECTED_BIT)) { + !(f & VK_MEMORY_PROPERTY_PROTECTED_BIT) && !(f & VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT)) { pure_dl = (int)t; break; } @@ -55,7 +59,8 @@ static void build_memory_model(vkb_physdev *pd) /* A device-local type with no host access, backed by the first device-local type. */ for (uint32_t t = 0; t < s->memoryTypeCount; t++) { VkMemoryPropertyFlags f = s->memoryTypes[t].propertyFlags; - if ((f & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) && !(f & VK_MEMORY_PROPERTY_PROTECTED_BIT)) { + if ((f & VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT) && !(f & VK_MEMORY_PROPERTY_PROTECTED_BIT) && + !(f & VK_MEMORY_PROPERTY_LAZILY_ALLOCATED_BIT)) { cands[nc++] = (struct cand){VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT, s->memoryTypes[t].heapIndex, t, 0}; break; }