Audio: keep at least 40 ms queued for the DirectAudio relay; logs: Share latest logs

- module-directaudio-sink keeps the relay's ring at least 40 ms deep. The relay starts at 8 ms,
  which the producer inside proot missed while the client loads (two underruns on the FIT's fresh
  install, steady stutter reported on another phone).
- Setup > Session logs > Share latest logs zips the newest session folder, wherever it was written
  (Downloads, or app-private files when Downloads is not writable), and opens the share sheet via
  a FileProvider scoped to that zip.
This commit is contained in:
The412Banner committed 2026-09-23 11:27:25 -04:00
1 parent fe3b9892a7
commit 019928e783
6 files changed
+81

No files matched your search

+10
View File
@@ -95,5 +95,15 @@
android:name=".session.SessionService"
android:exported="false"
android:stopWithTask="false" />
<!-- Hands a zipped session folder to the share sheet; nothing else is exposed. -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.logs"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/log_share_paths" />
</provider>
</application>
</manifest>
@@ -32,6 +32,7 @@ import com.steamdeck.launcher.ui.DesktopAppsDialog
import com.steamdeck.launcher.ui.PackageRow
import com.steamdeck.launcher.session.OfflineMode
import com.steamdeck.launcher.session.ProtonExtras
import com.steamdeck.launcher.session.SessionLogShare
import com.steamdeck.launcher.session.SessionPrefs
import com.steamdeck.launcher.ui.ProtonDialog
import com.steamdeck.launcher.ui.ProtonRow
@@ -240,6 +241,15 @@ class MainActivity : ComponentActivity() {
SessionPrefs.setLogsEnabled(this, !SessionPrefs.logsEnabled(this))
logsEnabled = SessionPrefs.logsEnabled(this)
},
onShareLogs = {
Thread({
val zip = runCatching { SessionLogShare.zipLatest(this) }.getOrNull()
ui.post {
if (zip == null) android.widget.Toast.makeText(this, "No session logs yet: run a session first.", android.widget.Toast.LENGTH_LONG).show()
else startActivity(SessionLogShare.shareIntent(this, zip))
}
}, "share-logs").start()
},
onOffline = {
OfflineMode.setEnabled(this, !OfflineMode.enabled(this))
offline = OfflineMode.enabled(this)
@@ -0,0 +1,49 @@
package com.steamdeck.launcher.session
import android.content.ClipData
import android.content.Context
import android.content.Intent
import androidx.core.content.FileProvider
import com.steamdeck.launcher.runtime.LinuxRuntime
import java.io.File
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
/**
* Zips the most recent session's log folder and hands it to Android's share sheet. Works where
* the folder could not be written to Downloads (no storage permission, or a device that refuses
* it) and landed in the app's private files instead, which a user cannot otherwise reach.
*/
object SessionLogShare {
/** The newest session folder in either place logs are written, or null if there is none. */
fun latest(context: Context): File? =
listOf(LinuxRuntime.debugLogDir(), File(context.filesDir, "logs"))
.flatMap { dir -> dir.listFiles { f -> f.isDirectory && f.name.startsWith("session-") }?.toList() ?: emptyList() }
.maxByOrNull { it.name }
/** Builds the zip (blocking). Returns null when there is no session to share. */
fun zipLatest(context: Context): File? {
val folder = latest(context) ?: return null
val out = File(context.cacheDir, "shared-logs").apply { deleteRecursively(); mkdirs() }
val zip = File(out, "SteamDeck-${folder.name}.zip")
ZipOutputStream(zip.outputStream().buffered()).use { z ->
folder.walkTopDown().filter { it.isFile }.forEach { f ->
z.putNextEntry(ZipEntry(folder.name + "/" + f.relativeTo(folder).path))
f.inputStream().use { it.copyTo(z) }
z.closeEntry()
}
}
return zip
}
fun shareIntent(context: Context, zip: File): Intent {
val uri = FileProvider.getUriForFile(context, context.packageName + ".logs", zip)
val send = Intent(Intent.ACTION_SEND)
.setType("application/zip")
.putExtra(Intent.EXTRA_STREAM, uri)
.putExtra(Intent.EXTRA_SUBJECT, zip.nameWithoutExtension)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
send.clipData = ClipData.newRawUri(zip.name, uri)
return Intent.createChooser(send, "Share session logs").addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
}
@@ -150,6 +150,7 @@ class FrontEndActions(
val onRoms: () -> Unit,
val onFiles: () -> Unit,
val onLogs: () -> Unit,
val onShareLogs: () -> Unit = {},
val onOffline: () -> Unit,
val onEmulatorHelp: () -> Unit,
val onCredits: () -> Unit,
@@ -363,6 +364,7 @@ private fun Rail(
) {
MenuItem("On", checked = s.logsEnabled) { if (!s.logsEnabled) a.onLogs(); menus.open = null }
MenuItem("Off", checked = !s.logsEnabled) { if (s.logsEnabled) a.onLogs(); menus.open = null }
MenuItem("Share latest logs", checked = false) { a.onShareLogs(); menus.open = null }
}
}
Box {
+4
View File
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name="shared_logs" path="shared-logs/" />
</paths>
@@ -63,6 +63,10 @@ PA_MODULE_USAGE(
#define STATS_EVERY_USEC (30 * PA_USEC_PER_SEC)
/* Never queue more than this ahead of the relay's target, whatever it asks for. */
#define MAX_AHEAD_MS 250
/* Always keep at least this much queued. The relay starts its target at two bursts (8 ms), which
* a producer inside proot misses while the client is loading: two underruns in the first seconds
* on the FIT, steady stutter on a slower phone. 40 ms of slack costs 40 ms of latency at most. */
#define MIN_AHEAD_MS 40
static const char* const valid_modargs[] = {
"socket", "sink_name", "sink_properties", "volume", "performance_mode", "adaptive", NULL
@@ -239,6 +243,8 @@ static int service_ring(struct userdata *u) {
return -1;
}
target = (uint32_t) (r->target_frames > 0 ? r->target_frames : 2 * u->burst);
if (target < u->ss.rate / 1000 * MIN_AHEAD_MS)
target = u->ss.rate / 1000 * MIN_AHEAD_MS;
if (target > (uint32_t) (u->ss.rate / 1000 * MAX_AHEAD_MS))
target = (uint32_t) (u->ss.rate / 1000 * MAX_AHEAD_MS);
avail = da_ring_avail(r);