diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 341fd5356..887478f9c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -95,5 +95,15 @@
android:name=".session.SessionService"
android:exported="false"
android:stopWithTask="false" />
+
+
+
+
diff --git a/app/src/main/java/com/steamdeck/launcher/MainActivity.kt b/app/src/main/java/com/steamdeck/launcher/MainActivity.kt
index b02e367b8..78e97c304 100644
--- a/app/src/main/java/com/steamdeck/launcher/MainActivity.kt
+++ b/app/src/main/java/com/steamdeck/launcher/MainActivity.kt
@@ -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)
diff --git a/app/src/main/java/com/steamdeck/launcher/session/SessionLogShare.kt b/app/src/main/java/com/steamdeck/launcher/session/SessionLogShare.kt
new file mode 100644
index 000000000..4ed4fc347
--- /dev/null
+++ b/app/src/main/java/com/steamdeck/launcher/session/SessionLogShare.kt
@@ -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)
+ }
+}
diff --git a/app/src/main/java/com/steamdeck/launcher/ui/FrontEndScreen.kt b/app/src/main/java/com/steamdeck/launcher/ui/FrontEndScreen.kt
index e7294af2b..806c56160 100644
--- a/app/src/main/java/com/steamdeck/launcher/ui/FrontEndScreen.kt
+++ b/app/src/main/java/com/steamdeck/launcher/ui/FrontEndScreen.kt
@@ -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 {
diff --git a/app/src/main/res/xml/log_share_paths.xml b/app/src/main/res/xml/log_share_paths.xml
new file mode 100644
index 000000000..cc98eee1e
--- /dev/null
+++ b/app/src/main/res/xml/log_share_paths.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/tools/aaudio-sink/module-directaudio-sink.c b/tools/aaudio-sink/module-directaudio-sink.c
index eca10b307..533ff82f8 100644
--- a/tools/aaudio-sink/module-directaudio-sink.c
+++ b/tools/aaudio-sink/module-directaudio-sink.c
@@ -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);