mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 11:31:08 +00:00
Detailed graphics logs for non-Adreno test reports
- graphics-report.txt in every session folder: the graphics path the session draws with and why (Turnip, system Vulkan #570, Mali bridge #517, or an open driver pair with its release), the driver mode and choices, what Auto would pick, the GPU (family, Vulkan device name, kbase gpuinfo, Android, SoC, kernel), the GPU nodes the app can see (access, device numbers, SELinux label; nothing is opened) and the guest's graphics environment. At the end it adds the compositor's driver lines from app.log and the game launches that were logged. - vulkan-summary.txt: the session's own vulkaninfo --summary with the driver as exported, in the background with a 10 s timeout. - Per-game logs under games/<time>-<appid>/: Proton's log, DXVK's and vkd3d-proton's, Mesa's and PanVK's startup messages and the launch environment, from droiddeck-game-env. Each file is cut to 20 MB and the folder to 120 MB at the session's end. - The per-game logs and the Vulkan summary are on for a GPU that is not an Adreno, and on an Adreno only with Setup's new "Detailed graphics logs" switch. Share logs already takes the whole session folder, so all of it is in the zip, scrubbed as before.
This commit is contained in:
1 parent
797840be7e
commit
da45bbc7cf
11 files changed
+321
-1
No files matched your search
@@ -368,6 +368,7 @@ class MainActivity : ComponentActivity() {
|
||||
private var runningLabel by mutableStateOf<String?>(null)
|
||||
private var logsEnabled by mutableStateOf(true)
|
||||
private var agentCommands by mutableStateOf(false)
|
||||
private var graphicsLogs by mutableStateOf(false)
|
||||
private var showRoms by mutableStateOf(false)
|
||||
private var homeAppSelected by mutableStateOf(false)
|
||||
private var homeScreenEnabled by mutableStateOf(false)
|
||||
@@ -563,7 +564,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,
|
||||
frameGenLabel = frameGenLabel, romsDir = romsDir, logsEnabled = logsEnabled, agentCommands = agentCommands, graphicsLogs = graphicsLogs,
|
||||
steamGames = steamGames, emulators = emulatorList, running = runningLabel,
|
||||
frameGen = FrameGen.mode(this),
|
||||
lossless = lossless,
|
||||
@@ -709,6 +710,10 @@ class MainActivity : ComponentActivity() {
|
||||
SessionPrefs.setLogsEnabled(this, !SessionPrefs.logsEnabled(this))
|
||||
logsEnabled = SessionPrefs.logsEnabled(this)
|
||||
},
|
||||
onGraphicsLogs = {
|
||||
SessionPrefs.setDetailedGraphicsLogs(this, !SessionPrefs.detailedGraphicsLogs(this))
|
||||
graphicsLogs = SessionPrefs.detailedGraphicsLogs(this)
|
||||
},
|
||||
onAgentCommands = {
|
||||
com.droiddeck.launcher.agent.AgentAccess.setCommandsEnabled(this, !com.droiddeck.launcher.agent.AgentAccess.commandsEnabled(this))
|
||||
agentCommands = com.droiddeck.launcher.agent.AgentAccess.commandsEnabled(this)
|
||||
@@ -1606,6 +1611,7 @@ class MainActivity : ComponentActivity() {
|
||||
romsDir = SessionPrefs.romsDir(this).takeIf { it.isNotEmpty() }
|
||||
logsEnabled = SessionPrefs.logsEnabled(this)
|
||||
agentCommands = com.droiddeck.launcher.agent.AgentAccess.commandsEnabled(this)
|
||||
graphicsLogs = SessionPrefs.detailedGraphicsLogs(this)
|
||||
runningLabel = if (SessionState.running) when (SessionState.mode) {
|
||||
SessionService.MODE_DESKTOP -> getString(R.string.rail_desktop)
|
||||
SessionService.MODE_RUN -> Library.nameForProgram(SessionState.program)
|
||||
|
||||
@@ -0,0 +1,174 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.system.Os
|
||||
import com.droiddeck.launcher.core.DeviceSupport
|
||||
import com.droiddeck.launcher.core.FileUtils
|
||||
import com.droiddeck.launcher.gpu.DriverBundle
|
||||
import com.droiddeck.launcher.gpu.DriverPairs
|
||||
import com.droiddeck.launcher.gpu.GpuInfo
|
||||
import com.droiddeck.launcher.gpu.SystemVulkanDriver
|
||||
import com.droiddeck.launcher.gpu.TurnipReleases
|
||||
import com.droiddeck.launcher.gpu.VulkanInfo
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* graphics-report.txt in each session's folder: which graphics path the session drew with and why,
|
||||
* the GPU and its device nodes, the environment the guest got, and at the end what the compositor
|
||||
* said about its driver. Written for a non-Adreno test report, where a black screen otherwise comes
|
||||
* with nothing to read; the Share logs zip takes the whole folder, so it travels with the logs.
|
||||
* Read-only: nothing here opens a GPU node or changes a setting.
|
||||
*/
|
||||
object GraphicsReport {
|
||||
const val FILE = "graphics-report.txt"
|
||||
const val GAMES_DIR = "games"
|
||||
|
||||
/** One file in games/ past this is cut to its first part at the session's end. */
|
||||
private const val GAME_FILE_MAX = 20L * 1024 * 1024
|
||||
/** All of games/ past this loses its oldest launches. */
|
||||
private const val GAMES_TOTAL_MAX = 120L * 1024 * 1024
|
||||
|
||||
/** The guest keys the report repeats: everything that decides which Vulkan driver loads. */
|
||||
private val ENV_PREFIXES = listOf(
|
||||
"VK_", "BL_VK", "BL_DRM", "BL_VKBRIDGE", "VKBRIDGE", "MESA_", "GALLIUM_", "ZINK", "TU_", "PANVK",
|
||||
"DROIDDECK_POWERVR", "DROIDDECK_HYBRIS", "BL_GAME_LOGS", "LIBGL_",
|
||||
)
|
||||
|
||||
/** 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)
|
||||
|
||||
/** Writes the start of the report; never throws. */
|
||||
fun begin(context: Context, dir: File, guest: List<String>) {
|
||||
runCatching { File(dir, FILE).writeText(start(context, guest)) }
|
||||
.onFailure { android.util.Log.w("GraphicsReport", "could not write $FILE", it) }
|
||||
}
|
||||
|
||||
private fun start(context: Context, guest: List<String>): String = buildString {
|
||||
val time = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).format(Date())
|
||||
appendLine("== graphics report, session start $time")
|
||||
appendLine()
|
||||
val gpu = runCatching { GpuInfo.detect() }.getOrNull()
|
||||
appendLine("-- path")
|
||||
appendLine("path: " + path(context))
|
||||
appendLine("driver mode: " + SessionPrefs.gpuDriverMode(context))
|
||||
appendLine("display driver: " + SessionPrefs.androidDriver(context).ifEmpty { "(auto)" })
|
||||
appendLine("runtime driver: " + SessionPrefs.linuxDriver(context).ifEmpty { "(runtime default)" })
|
||||
DriverBundle.active(context)?.let { b ->
|
||||
appendLine("pair: ${b.name} ${b.version} (bundle ${b.id})")
|
||||
release(context, b.id)?.let { appendLine("pair release: $it") }
|
||||
}
|
||||
if (gpu != null) {
|
||||
val pairs = DriverPairs.from(TurnipReleases.cached(context))
|
||||
appendLine("auto would pick: " + (DriverPairs.recommendedKey(gpu, pairs) ?: "(no pair: system driver or bridge)"))
|
||||
}
|
||||
appendLine("mali bridge setting: " + SessionPrefs.vkBridge(context) + ", in use: " + DeviceSupport.vkBridge(context))
|
||||
appendLine()
|
||||
appendLine("-- gpu")
|
||||
if (gpu != null) {
|
||||
appendLine("family: ${gpu.family} (${gpu.supportText})")
|
||||
appendLine("name: ${gpu.name}" + if (gpu.model > 0) ", model ${gpu.model} (${gpu.modelSource})" else "")
|
||||
}
|
||||
appendLine("vulkan device (app): " + (VulkanInfo.cachedOrNull()?.get("device") ?: "(not probed)"))
|
||||
appendLine("system vulkan hal: " + (SystemVulkanDriver.hal() ?: "none"))
|
||||
read("/sys/class/misc/mali0/device/gpuinfo")?.let { appendLine("mali gpuinfo: $it") }
|
||||
appendLine("android: ${Build.VERSION.RELEASE} (sdk ${Build.VERSION.SDK_INT}), ${Build.MANUFACTURER} ${Build.MODEL}")
|
||||
val soc = if (Build.VERSION.SDK_INT >= 31) "${Build.SOC_MANUFACTURER} ${Build.SOC_MODEL}" else Build.HARDWARE
|
||||
appendLine("soc: $soc, board ${Build.BOARD}, hardware ${Build.HARDWARE}")
|
||||
appendLine("kernel: " + (System.getProperty("os.version") ?: "?"))
|
||||
appendLine()
|
||||
appendLine("-- gpu nodes (from the app; not opened)")
|
||||
val dri = File("/dev/dri").list()?.sorted()?.map { "/dev/dri/$it" }.orEmpty()
|
||||
for (path in listOf("/dev/kgsl-3d0", "/dev/mali0") + dri) appendLine(node(path))
|
||||
if (dri.isEmpty()) appendLine("/dev/dri: " + if (File("/dev/dri").exists()) "not listable" else "absent")
|
||||
appendLine()
|
||||
appendLine("-- guest environment (graphics)")
|
||||
guest.filter { e -> ENV_PREFIXES.any { e.startsWith(it) } }.sorted().forEach { appendLine(it) }
|
||||
appendLine()
|
||||
}
|
||||
|
||||
/** Which backend this session's Vulkan goes through, as the session decides it. */
|
||||
private fun path(context: Context): String = when {
|
||||
DeviceSupport.adreno() -> "Turnip (Adreno, KGSL)"
|
||||
DeviceSupport.vkBridge(context) -> "Mali bridge (#517): the phone's own Vulkan driver through the bridge"
|
||||
DeviceSupport.openDriverActive(context) -> "open driver pair (PanVK / RADV)"
|
||||
SystemVulkanDriver.usesDefault(SessionPrefs.linuxDriver(context)) && SystemVulkanDriver.isAvailable() ->
|
||||
"system Vulkan (#570): ${SystemVulkanDriver.gpuName()} driver through libhybris"
|
||||
SessionPrefs.linuxDriver(context).isNotEmpty() -> "imported runtime driver ${SessionPrefs.linuxDriver(context)}"
|
||||
else -> "none for this GPU (no Adreno, no system HAL, no pair, bridge off)"
|
||||
}
|
||||
|
||||
/** The release tag and download a pair bundle came from, when the last release check knows it. */
|
||||
private fun release(context: Context, bundleId: String): String? {
|
||||
val asset = TurnipReleases.cached(context)?.assets?.firstOrNull { a ->
|
||||
val id = a.name.removeSuffix("-Pair.zip")
|
||||
a.bundle && (bundleId == id || bundleId.startsWith("$id-"))
|
||||
}
|
||||
?: return null
|
||||
return "${asset.tag} ${asset.url}"
|
||||
}
|
||||
|
||||
private fun node(path: String): String {
|
||||
val f = File(path)
|
||||
if (!f.exists()) return "$path: absent"
|
||||
val access = (if (f.canRead()) "r" else "-") + (if (f.canWrite()) "w" else "-")
|
||||
val rdev = runCatching { Os.stat(path).st_rdev }.getOrNull()
|
||||
val numbers = rdev?.let { " ${major(it)}:${minor(it)}" }.orEmpty()
|
||||
val label = runCatching { String(Os.getxattr(path, "security.selinux")).trimEnd('\u0000') }.getOrNull()
|
||||
return "$path: $access$numbers" + (label?.let { " $it" } ?: "")
|
||||
}
|
||||
|
||||
/** Linux's dev_t split (glibc's gnu_dev_major / gnu_dev_minor). */
|
||||
internal fun major(dev: Long): Long = ((dev ushr 8) and 0xfff) or ((dev ushr 32) and 0xfffff000L)
|
||||
internal fun minor(dev: Long): Long = (dev and 0xff) or ((dev ushr 12) and 0xffffff00L)
|
||||
|
||||
private fun read(path: String): String? = FileUtils.readString(File(path))?.trim()?.takeIf { it.isNotEmpty() }
|
||||
|
||||
/** The compositor's driver lines from the app's own log. */
|
||||
private val COMPOSITOR = Regex(
|
||||
"""vk_present|waylandcomp|vk_loader|compositor|vkCreateDevice|extension|adrenotools|TurnipDriver|""" +
|
||||
"""graphics driver|SystemVulkan|vkbridge|VkBridge|mali_gralloc|panvk|PanVK|radv|MESA|native buffer|first frame|present""",
|
||||
RegexOption.IGNORE_CASE,
|
||||
)
|
||||
|
||||
/**
|
||||
* The end of the report, from the finished folder: the compositor's driver lines out of
|
||||
* app.log, and games/ held to its size limits. Never throws.
|
||||
*/
|
||||
fun finish(dir: File) {
|
||||
runCatching {
|
||||
val report = File(dir, FILE)
|
||||
if (!report.isFile) return
|
||||
val lines = File(dir, "app.log").takeIf { it.isFile }?.useLines { seq -> seq.filter { COMPOSITOR.containsMatchIn(it) }.take(400).toList() }.orEmpty()
|
||||
val games = File(dir, GAMES_DIR).listFiles()?.filter { it.isDirectory }?.sortedBy { it.name }.orEmpty()
|
||||
report.appendText(buildString {
|
||||
appendLine("-- compositor and driver lines from app.log (${lines.size})")
|
||||
lines.forEach { appendLine(it) }
|
||||
appendLine()
|
||||
appendLine("-- game launches with detailed logs: ${games.size}")
|
||||
games.forEach { appendLine(it.name) }
|
||||
appendLine("== session end " + SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US).format(Date()))
|
||||
})
|
||||
capGames(File(dir, GAMES_DIR))
|
||||
}.onFailure { android.util.Log.w("GraphicsReport", "could not finish $FILE", it) }
|
||||
}
|
||||
|
||||
/** Cuts each file in [games] to [GAME_FILE_MAX] and drops the oldest launches past [GAMES_TOTAL_MAX]. */
|
||||
internal fun capGames(games: File, fileMax: Long = GAME_FILE_MAX, totalMax: Long = GAMES_TOTAL_MAX) {
|
||||
if (!games.isDirectory) return
|
||||
games.walkTopDown().filter { it.isFile && it.length() > fileMax }.forEach { f ->
|
||||
runCatching { java.io.RandomAccessFile(f, "rw").use { it.setLength(fileMax) } }
|
||||
}
|
||||
val launches = games.listFiles()?.filter { it.isDirectory }?.sortedBy { it.name }.orEmpty().toMutableList()
|
||||
fun size(d: File) = d.walkTopDown().filter { it.isFile }.sumOf { it.length() }
|
||||
var total = launches.sumOf { size(it) }
|
||||
while (total > totalMax && launches.size > 1) {
|
||||
val oldest = launches.removeAt(0)
|
||||
total -= size(oldest)
|
||||
FileUtils.delete(oldest)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -547,6 +547,17 @@ object SessionPrefs {
|
||||
prefs(context).edit().putBoolean("logs", on).apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Detailed graphics logs: per-game Proton, DXVK, vkd3d-proton and driver logs in the session's
|
||||
* folder. Always on for a GPU that is not an Adreno (test drivers); opt-in on an Adreno.
|
||||
*/
|
||||
fun detailedGraphicsLogs(context: Context): Boolean =
|
||||
prefs(context).getBoolean("detailedGraphicsLogs", false)
|
||||
|
||||
fun setDetailedGraphicsLogs(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("detailedGraphicsLogs", on).apply()
|
||||
}
|
||||
|
||||
/** Steam storage-call diagnostics are opt-in because they add timing work to file operations. */
|
||||
fun storageDiagnosticsEnabled(context: Context): Boolean =
|
||||
prefs(context).getBoolean("storageDiagnostics", false)
|
||||
|
||||
@@ -284,6 +284,7 @@ class SessionService : Service() {
|
||||
*/
|
||||
private fun collectSessionArtifacts(dir: File) {
|
||||
try {
|
||||
GraphicsReport.finish(dir)
|
||||
SessionArtifacts.collect(this, dir, "session stopped")
|
||||
} finally {
|
||||
// Last, so everything above is in the file it is about - and only this session's:
|
||||
@@ -418,6 +419,10 @@ class SessionService : Service() {
|
||||
// The second library's name, for droiddeck-steam-library; the bind itself is made below.
|
||||
GameStorage.effective(this)?.let { guest.add("BL_LIBRARY_LABEL=" + it.label.replace('"', ' ')) }
|
||||
if (SessionState.mode == MODE_STEAM && SessionState.steamUi == "desktop") guest.add("BL_STEAM_UI=desktop")
|
||||
// Detailed graphics logs: per-game logs and the guest's Vulkan summary (a GPU that is not
|
||||
// an Adreno, or Setup's switch), and graphics-report.txt for every session.
|
||||
if (GraphicsReport.detailed(this)) guest.add("BL_GAME_LOGS=1")
|
||||
GraphicsReport.begin(this, sessionDir, guest)
|
||||
val shellGuest = ArrayList(guest).apply {
|
||||
add("SHELL=/bin/bash")
|
||||
add("TERM=xterm-256color")
|
||||
|
||||
@@ -99,6 +99,7 @@ class FrontEndState(
|
||||
val logsEnabled: Boolean,
|
||||
/** Setup's Debugging tools toggle (AgentAccess): off unless the user turns it on. */
|
||||
val agentCommands: Boolean = false,
|
||||
val graphicsLogs: Boolean = false,
|
||||
val steamGames: List<Library.SteamGame>,
|
||||
val emulators: List<Library.Emulator>,
|
||||
val running: String?,
|
||||
@@ -208,6 +209,7 @@ class FrontEndActions(
|
||||
val onBrowseFiles: (File) -> Unit = {},
|
||||
val onLogs: () -> Unit,
|
||||
val onAgentCommands: () -> Unit = {},
|
||||
val onGraphicsLogs: () -> Unit = {},
|
||||
val onShareLogs: () -> Unit = {},
|
||||
val onClearLogs: () -> Unit = {},
|
||||
val onOffline: () -> Unit,
|
||||
|
||||
@@ -410,6 +410,7 @@ internal fun SetupPanel(
|
||||
SettingsGroup(stringResource(R.string.setup_tab_diagnostics)) {
|
||||
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() }
|
||||
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)
|
||||
|
||||
@@ -679,6 +679,8 @@
|
||||
<string name="setup_logs">Session logs</string>
|
||||
<string name="setup_logs_hint">Saved after each session</string>
|
||||
<string name="setup_agent_commands">Debugging tools</string>
|
||||
<string name="setup_graphics_logs">Detailed graphics logs</string>
|
||||
<string name="setup_graphics_logs_hint">Each game\'s Proton, DXVK and driver logs in the session\'s logs. Always on when the GPU is not an Adreno.</string>
|
||||
<string name="setup_latest_logs">Latest session logs</string>
|
||||
<string name="setup_saved_logs">Saved session logs</string>
|
||||
<string name="setup_saved_logs_hint">Keeps the newest %1$d</string>
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.droiddeck.launcher.session
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
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 GraphicsReportTest {
|
||||
@get:Rule val tmp = TemporaryFolder()
|
||||
|
||||
@Test fun deviceNumbersSplitAsLinuxDoes() {
|
||||
// /dev/mali0 on the testers' phones: 10:103 (misc); a DRM render node: 226:128.
|
||||
assertEquals(10L, GraphicsReport.major(10L shl 8 or 103L))
|
||||
assertEquals(103L, GraphicsReport.minor(10L shl 8 or 103L))
|
||||
assertEquals(226L, GraphicsReport.major(226L shl 8 or 128L))
|
||||
assertEquals(128L, GraphicsReport.minor(226L shl 8 or 128L))
|
||||
}
|
||||
|
||||
@Test fun gameLogsAreCutPerFileAndOldestLaunchesGoFirst() {
|
||||
val games = tmp.newFolder("games")
|
||||
fun launch(name: String, bytes: Int) = File(games, name).apply { mkdirs() }.also { File(it, "steam-1.log").writeBytes(ByteArray(bytes)) }
|
||||
val old = launch("20261012-100000-1", 60)
|
||||
val mid = launch("20261012-110000-2", 60)
|
||||
val new = launch("20261012-120000-3", 200)
|
||||
GraphicsReport.capGames(games, fileMax = 100, totalMax = 150)
|
||||
assertEquals(100L, File(new, "steam-1.log").length())
|
||||
assertFalse(old.exists())
|
||||
assertTrue(mid.exists())
|
||||
assertTrue(new.exists())
|
||||
}
|
||||
|
||||
@Test fun aMissingGamesFolderIsFine() {
|
||||
GraphicsReport.capGames(File(tmp.root, "none"))
|
||||
GraphicsReport.finish(tmp.newFolder("session"))
|
||||
}
|
||||
}
|
||||
@@ -353,6 +353,48 @@ def system_graphics_launch(command, env):
|
||||
return [str(wrapper), *command], result
|
||||
|
||||
|
||||
def game_logs(command, env, app_id, now=None):
|
||||
"""Detailed graphics logs for one game launch, in the session's log folder: Proton's own log,
|
||||
DXVK's and vkd3d-proton's, and the Vulkan driver's startup messages. The app turns this on
|
||||
(BL_GAME_LOGS=1) on a GPU that is not an Adreno, or with Setup's "Detailed graphics logs"."""
|
||||
if env.get("BL_GAME_LOGS") != "1" or not env.get("BL_DEBUG_DIR") or len(command) < 3 \
|
||||
or command[1] != "waitforexitandrun":
|
||||
return env
|
||||
import time
|
||||
stamp = time.strftime("%Y%m%d-%H%M%S", time.localtime(now))
|
||||
name = app_id or re.sub(r"[^A-Za-z0-9._-]+", "_", Path(command[2]).stem)[:40] or "game"
|
||||
folder = Path(env["BL_DEBUG_DIR"]) / "games" / ("%s-%s" % (stamp, name))
|
||||
try:
|
||||
folder.mkdir(parents=True, exist_ok=True)
|
||||
except OSError as error:
|
||||
print("droiddeck-game-env: no game log folder (%s)" % error, file=sys.stderr)
|
||||
return env
|
||||
result = dict(env)
|
||||
result.update({
|
||||
"PROTON_LOG": "1",
|
||||
"PROTON_LOG_DIR": str(folder),
|
||||
"DXVK_LOG_PATH": str(folder),
|
||||
"DXVK_LOG_LEVEL": env.get("DXVK_LOG_LEVEL", "info"),
|
||||
"VKD3D_LOG_FILE": str(folder / "vkd3d.log"),
|
||||
"VKD3D_DEBUG": env.get("VKD3D_DEBUG", "warn"),
|
||||
"VKD3D_SHADER_DEBUG": env.get("VKD3D_SHADER_DEBUG", "none"),
|
||||
"MESA_LOG_FILE": str(folder / "mesa.log"),
|
||||
"MESA_DEBUG": env.get("MESA_DEBUG", "silent"),
|
||||
"PANVK_DEBUG": env.get("PANVK_DEBUG", "startup"),
|
||||
})
|
||||
try:
|
||||
(folder / "launch.txt").write_text("\n".join([
|
||||
"app: %s" % (app_id or "-"),
|
||||
"exe: %s" % command[2],
|
||||
"proton: %s" % command[0],
|
||||
"started: %s" % time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(now)),
|
||||
] + ["%s=%s" % (key, env[key]) for key in sorted(env)
|
||||
if key.startswith(("VK_", "BL_VK", "BL_DRM", "MESA_", "PANVK", "DXVK_", "VKD3D", "ZINK", "TU_"))]) + "\n")
|
||||
except OSError:
|
||||
pass
|
||||
return result
|
||||
|
||||
|
||||
def exec_proton(command, env):
|
||||
# Synchronization selection has already picked the actual Proton executable.
|
||||
command, env = system_graphics_launch(command, env)
|
||||
@@ -392,6 +434,7 @@ def main():
|
||||
env = apply_config(env, {"version": 1}, app_id, fixes)
|
||||
except (OSError, ValueError, TypeError, AttributeError):
|
||||
print("droiddeck-game-env: invalid configuration; using inherited environment", file=sys.stderr)
|
||||
env = game_logs(command, env, app_id)
|
||||
env = force_ssbs(command, env)
|
||||
command, env = start_without_steam(command, env, app_id)
|
||||
sync = load_sync()
|
||||
|
||||
@@ -163,6 +163,23 @@ if [ "${BL_VKBRIDGE:-0}" = 1 ]; then
|
||||
fi
|
||||
unset bl_vkb_icd
|
||||
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.
|
||||
if [ "${BL_GAME_LOGS:-0}" = 1 ] && [ -z "${BL_INSIDE:-}" ] && [ -n "${BL_DEBUG_DIR:-}" ]; then
|
||||
(
|
||||
{
|
||||
echo "== VK_DRIVER_FILES=${VK_DRIVER_FILES:-} VK_ICD_FILENAMES=${VK_ICD_FILENAMES:-} BL_DRM_STANDIN=${BL_DRM_STANDIN:-}"
|
||||
if command -v vulkaninfo >/dev/null 2>&1; then
|
||||
timeout 10 vulkaninfo --summary
|
||||
echo "== vulkaninfo exit: $?"
|
||||
else
|
||||
echo "== vulkaninfo is not in this runtime; GPU nodes:"
|
||||
ls -l /dev/kgsl-3d0 /dev/mali0 /dev/dri 2>&1
|
||||
fi
|
||||
} > "$BL_DEBUG_DIR/vulkan-summary.txt" 2>&1
|
||||
) &
|
||||
fi
|
||||
# The app's gamescope build (tools/gamescope) asks for realtime-priority Vulkan queues on this
|
||||
# switch instead of on CAP_SYS_NICE, which proot never has; it falls back to normal priority
|
||||
# where the driver refuses, and the runtime's own build ignores it. On KGSL Turnip (Mesa 26.2) it
|
||||
|
||||
@@ -373,6 +373,26 @@ class GameEnvironmentTest(unittest.TestCase):
|
||||
with self.assertRaises(ValueError):
|
||||
MODULE["apply_config"]({}, {**config, "dxvkConfigGames": []}, "42")
|
||||
|
||||
def test_game_logs_only_when_asked_and_only_for_game_launches(self):
|
||||
game_logs = MODULE["game_logs"]
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
command = ["/proton", "waitforexitandrun", "/games/Hades/Hades.exe"]
|
||||
env = {"BL_DEBUG_DIR": tmp, "VK_DRIVER_FILES": "/icd.json"}
|
||||
self.assertEqual(game_logs(command, env, "1145360", now=0), env)
|
||||
on = dict(env, BL_GAME_LOGS="1")
|
||||
self.assertEqual(game_logs(["/proton", "run", "x.exe"], on, "1145360", now=0), on)
|
||||
result = game_logs(command, on, "1145360", now=0)
|
||||
folder = Path(result["PROTON_LOG_DIR"])
|
||||
self.assertEqual(folder.parent, Path(tmp) / "games")
|
||||
self.assertTrue(folder.name.endswith("-1145360"))
|
||||
self.assertEqual(result["DXVK_LOG_PATH"], str(folder))
|
||||
self.assertEqual(result["PROTON_LOG"], "1")
|
||||
launch = (folder / "launch.txt").read_text()
|
||||
self.assertIn("VK_DRIVER_FILES=/icd.json", launch)
|
||||
self.assertNotIn("PROTON_LOG", env)
|
||||
named = game_logs(command, on, None, now=0)
|
||||
self.assertTrue(Path(named["PROTON_LOG_DIR"]).name.endswith("-Hades"))
|
||||
|
||||
def test_game_ids_and_probes(self):
|
||||
for prefix in ("", "/compatdata/0", "/compatdata/0-123", "/compatdata/nope", "/compatdata/4294967296"):
|
||||
self.assertIsNone(MODULE["game_id"]({"STEAM_COMPAT_DATA_PATH": prefix}))
|
||||
|
||||
Reference in new issue
Block a user