diff --git a/app/src/main/java/com/droiddeck/launcher/MainActivity.kt b/app/src/main/java/com/droiddeck/launcher/MainActivity.kt index e40c575c9..9087ab70e 100644 --- a/app/src/main/java/com/droiddeck/launcher/MainActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/MainActivity.kt @@ -366,10 +366,6 @@ class MainActivity : ComponentActivity() { } }, "share-logs").start() }, - onOffline = { - OfflineMode.setEnabled(this, !OfflineMode.enabled(this)) - offline = OfflineMode.enabled(this) - }, onCredits = { showCredits = true }, onPageBack = { settingsMode = null; showPerformance = false; showProtons = false; showComponents = false; showMapping = false }, onTheme = { id -> SessionPrefs.setTheme(this, id); theme = id }, @@ -816,6 +812,8 @@ class MainActivity : ComponentActivity() { deckyLatestRelease = if (mode == SessionService.MODE_STEAM) decky.deckyReleases.firstOrNull() else null, deckyChecking = decky.deckyChecking, deckyStage = decky.deckyStage, deckyPercent = decky.deckyPercent, deckyEnabled = decky.deckySupervisor, deckySessionRunning = SessionState.running, + offlineAccount = if (mode == SessionService.MODE_STEAM) offlineAccount else null, + offline = mode == SessionService.MODE_STEAM && offline, ), ModeSettingsActions( onResolution = { cap -> SessionPrefs.setResolutionCap(this, mode, cap); resolutionCap = cap }, @@ -866,6 +864,10 @@ class MainActivity : ComponentActivity() { SessionPrefs.setRunSteamAtStartup(this, on) runSteamAtStartup = on }, + onOffline = { + OfflineMode.setEnabled(this, !OfflineMode.enabled(this)) + offline = OfflineMode.enabled(this) + }, onPickAddedGamesDir = { pickAddedGamesDir.launch(InAppFilePicker.buildDirIntent(this, "Choose a folder of your own games", addedGamesDirs.lastOrNull())) }, onAddedGamesArt = { on -> SessionPrefs.setAddedGamesArt(this, on); addedGamesArt = on; if (on) refreshAddedGames() }, onForgetAddedGamesDir = { dir -> SessionPrefs.setAddedGamesDirs(this, addedGamesDirs - dir); addedGamesDirs = SessionPrefs.addedGamesDirs(this); refreshAddedGames(); refresh() }, @@ -894,6 +896,12 @@ class MainActivity : ComponentActivity() { clientOverride = clientOverride, clientCores = clientCores, gameCores = gameCores, tuSysmem = tuSysmem, zinkLazy = zinkLazy, glThread = glThread, noGlError = noGlError, noXalia = noXalia, prootNoSeccomp = prootNoSeccomp, guestHostname = guestHostname, phantomWarning = phantomWarning, + frameGenEngine = FrameGen.engine(this), frameGenMultiplier = FrameGen.multiplier(this), + lsfgReady = LsfgNative.isInstalled(this), + onFrameGenPick = { engine, multiplier -> + FrameGen.set(this, engine, multiplier) + frameGenLabel = FrameGen.label(this) + }, onClientOverride = { on -> SessionPrefs.setClientCpusOverride(this, on); clientOverride = on }, onTuSysmem = { on -> SessionPrefs.setTuSysmem(this, on); tuSysmem = on }, onZinkLazy = { on -> SessionPrefs.setZinkLazy(this, on); zinkLazy = on }, 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 d245f0346..5728b679a 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndScreen.kt @@ -139,7 +139,6 @@ class FrontEndActions( val onBrowseFiles: (File) -> Unit = {}, val onLogs: () -> Unit, val onShareLogs: () -> Unit = {}, - val onOffline: () -> Unit, val onCredits: () -> Unit, val onPageBack: () -> Unit = {}, val onTheme: (String) -> 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 a8a225dfe..b0c330128 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/FrontEndSetup.kt @@ -158,8 +158,8 @@ internal fun SetupPanel( var showLimitDetails by rememberSaveable { mutableStateOf(false) } val checks = 4 val readyCount = listOf(gpuOk, s.ready && !s.busy, !limitBlocks, signedIn).count { it } - // Five tabs instead of one long scroll; LB and RB turn them from anywhere on the page. - val tabs = listOf("Overview", "Controller", "Session", "Launcher", "About") + // Four durable buckets instead of a miscellaneous Session tab; LB and RB turn them from anywhere on the page. + val tabs = listOf("Overview", "Controller", "Launcher", "Support") var tab by rememberSaveable { mutableStateOf(0) } val tabFocus = remember { List(tabs.size) { FocusRequester() } } var tabTurned by remember { mutableStateOf(false) } @@ -258,9 +258,7 @@ internal fun SetupPanel( ControllerRows(host, s.oscMode, controller, a.controller) } if (s.controller == null || a.controller == null) Note("Controller settings are unavailable.") - } - 2 -> { - SettingsGroup("Session") { + SettingsGroup("Navigation") { ChoiceRow( host, "back-actions", "Back", SessionPrefs.backActionsOrder(s.backActionsInverted), listOf( @@ -268,22 +266,9 @@ internal fun SetupPanel( true to SessionPrefs.BACK_QAM_THEN_MENU, ), s.backActionsInverted, onPick = a.onBackActionsInverted, ) - SettingsRow("Frame generation", "Select the frame generation mode") { - Box { - ValueChip(s.frameGenLabel, host.open == "fg") { host.open = if (host.open == "fg") null else "fg" } - FrameGenMenu(s, a, host) - } - } - ToggleRow(host, "logs", "Session logs", "Saved after each session", s.logsEnabled) { a.onLogs() } - ActionRow("Latest session logs", "Send them with a bug report", "Share logs", a.onShareLogs) - ToggleRow( - host, "offline", "Offline mode", - s.offlineAccount?.let { "Signed in as $it" } ?: "Sign in to Steam first", - s.offline, enabled = s.offlineAccount != null, - ) { a.onOffline() } } } - 3 -> { + 2 -> { SettingsGroup("Launcher") { SettingsRow("Theme", "Choose the launcher appearance") { Box { @@ -314,6 +299,10 @@ internal fun SetupPanel( } } else -> { + SettingsGroup("Support") { + ToggleRow(host, "logs", "Session logs", "Saved after each session", s.logsEnabled) { a.onLogs() } + ActionRow("Latest session logs", "Send them with a bug report", "Share logs", a.onShareLogs) + } SettingsGroup("About") { ActionRow("Build", s.buildLabel, "Check for newer", a.onCheckLatestBuild) ActionRow("Credits", "The people and projects DroidDeck builds on", "View", a.onCredits) @@ -332,7 +321,7 @@ private fun ToolGrid(s: FrontEndState, a: FrontEndActions) { val tools = listOf( ToolSpec(Icons.Outlined.Folder, "Files", "Browse and manage files", a.onFiles), ToolSpec(Icons.Outlined.Extension, "Proton versions", "Install ARM64 Proton builds", a.onProtons), - ToolSpec(Icons.Outlined.Speed, "Performance", "CPU core assignment", a.onPerformance), + ToolSpec(Icons.Outlined.Speed, "Performance", "CPU, rendering and session tuning", a.onPerformance), ToolSpec(Icons.Outlined.VideogameAsset, "ROMs folder", s.romsDir ?: "Choose where emulator games are stored", a.onRoms), ) Column(verticalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.fillMaxWidth()) { diff --git a/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt b/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt index 00bb92839..71de4ac16 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt @@ -1,6 +1,7 @@ package com.droiddeck.launcher.ui import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.Row @@ -15,6 +16,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.res.stringResource import com.droiddeck.launcher.R @@ -72,6 +74,9 @@ class ModeSettings( val steamDeckMode: Boolean = false, /** Steam only: start a Steam session when DroidDeck opens. */ val runSteamAtStartup: Boolean = false, + /** Steam only: remembered login and requested offline state. */ + val offlineAccount: String? = null, + val offline: Boolean = false, /** Steam only: the user's chosen Games folders; null outside Steam. */ val addedGamesDirs: List? = null, val addedGames: List = emptyList(), @@ -127,6 +132,7 @@ class ModeSettingsActions( val onSteamChannel: (String) -> Unit = {}, val onSteamDeckMode: (Boolean) -> Unit = {}, val onRunSteamAtStartup: (Boolean) -> Unit = {}, + val onOffline: () -> Unit = {}, val onPickAddedGamesDir: () -> Unit = {}, val onForgetAddedGamesDir: (path: String) -> Unit = {}, val onAddedGamesArt: (Boolean) -> Unit = {}, @@ -150,6 +156,22 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) { var driverPage by remember { mutableStateOf(null) } var returnTo by remember { mutableStateOf(null) } val pageScroll = androidx.compose.foundation.rememberScrollState() + val tabs = if (steam) listOf("Display", "Controls", "Runtime", "Steam", "Games") + else listOf("Display", "Controls", "Runtime") + var tab by rememberSaveable(s.mode) { mutableStateOf(0) } + val tabFocus = remember { List(tabs.size) { androidx.compose.ui.focus.FocusRequester() } } + var tabTurned by remember { mutableStateOf(false) } + val inputModeManager = androidx.compose.ui.platform.LocalInputModeManager.current + val pickTab: (Int) -> Unit = { index -> host.open = null; tab = index; tabTurned = true } + androidx.compose.runtime.LaunchedEffect(tab) { + if (tabTurned) { + pageScroll.scrollTo(0) + if (inputModeManager.inputMode == androidx.compose.ui.input.InputMode.Keyboard) { + androidx.compose.runtime.withFrameNanos { } + runCatching { tabFocus[tab].requestFocus() } + } + } + } val runtimeChip = remember { androidx.compose.ui.focus.FocusRequester() } val displayChip = remember { androidx.compose.ui.focus.FocusRequester() } val firstChip = remember { androidx.compose.ui.focus.FocusRequester() } @@ -196,7 +218,14 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) { onBack = a.onDismiss, scroll = pageScroll, ) { - SettingsGroup("Display") { + Column( + modifier = Modifier.fillMaxWidth().bumpers( + onPrevious = { pickTab((tab + tabs.size - 1) % tabs.size) }, + onNext = { pickTab((tab + 1) % tabs.size) }, + ), + ) { + TabStrip(tabs, tab, pickTab, Modifier.padding(bottom = 4.dp), tabFocus) + if (tab == 0) SettingsGroup("Display") { val default = SessionPrefs.defaultResolutionCap(s.mode) var editCustom by remember { mutableStateOf(false) } val custom = s.customResolution @@ -214,13 +243,18 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) { if (custom != null) "Set by the custom resolution." else "Auto uses at least 16:9.", com.droiddeck.launcher.session.SessionPrefs.shapeChoices, s.shapeMode, enabled = custom == null, onPick = a.onShape, ) + if (steam && s.forceFullscreen != null) ToggleRow( + host, "fill", "Stretch games to fill the screen", + "Keeps games that resize their own window full screen. Turn it off if a game shows up small in a corner. Applies next session.", + s.forceFullscreen, onChange = a.onForceFullscreen, + ) if (editCustom) CustomResolutionDialog( initial = custom, onSave = { size -> editCustom = false; a.onCustomResolution(size) }, onDismiss = { editCustom = false }, ) } - SettingsGroup("HDR") { + if (tab == 0) SettingsGroup("HDR") { ToggleRow( host, "hdr", "HDR10 output", s.hdrReason?.let { "Not available: $it." } @@ -228,7 +262,7 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) { checked = s.hdr && s.hdrReason == null, enabled = s.hdrReason == null, onChange = a.onHdr, ) } - SettingsGroup("Drivers") { + if (tab == 2) SettingsGroup("Drivers") { SettingsRow("Runtime driver", (if (steam) "Used by Steam and games." else "Used by desktop apps.") + " Applies next session.") { ValueChip( s.linuxRows.firstOrNull { it.id == s.linuxSelected }?.name ?: "Runtime default", open = false, @@ -242,7 +276,7 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) { ) { openDriverPage("panel") } } } - SettingsGroup(if (steam) "Touch & controls" else "Touch") { + if (tab == 1) SettingsGroup("Controls") { ChoiceRow( host, "touch", "Touch", null, listOf("auto" to "Auto", "touchpad" to "Touchpad", "direct" to "Direct"), s.touchMode, @@ -267,7 +301,7 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) { ), s.backActionsInverted, onPick = a.onBackActionsInverted, ) } - SettingsGroup("Session") { + if (tab == 2) SettingsGroup("Session") { ChoiceRow( host, "suspend", "Background behavior", "How this session behaves when the app leaves the screen or the display turns off.", @@ -281,14 +315,19 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) { onPick = a.onSuspendPolicy, ) } - if (steam) SettingsGroup("Startup") { + if (steam && tab == 3) SettingsGroup("Startup & account") { ToggleRow( host, "steam-startup", "Run Steam when DroidDeck starts", "Open the Steam session when you launch DroidDeck.", s.runSteamAtStartup, onChange = a.onRunSteamAtStartup, ) + ToggleRow( + host, "offline", "Offline mode", + s.offlineAccount?.let { "Signed in as $it" } ?: "Sign in to Steam first", + s.offline, enabled = s.offlineAccount != null, + ) { a.onOffline() } } - if (steam) SettingsGroup("Decky") { + if (steam && tab == 3) SettingsGroup("Decky") { val updateAvailable = s.deckyInstalled != null && s.deckyLatestRelease != null && s.deckyInstalled != s.deckyLatestRelease.tag val status = when { @@ -340,7 +379,7 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) { ) } } - if (steam && s.steamChannel != null) SettingsGroup("Client") { + if (steam && tab == 3 && s.steamChannel != null) SettingsGroup("Client") { ToggleRow( host, "steamdeck", "Steam Deck mode", "Enables Steam's Deck interface and Quick Access performance overlay controls. Applies next session.", @@ -353,7 +392,7 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) { onPick = a.onSteamChannel, ) } - if (steam && s.addedGamesDirs != null) SettingsGroup("Added games") { + if (steam && tab == 4 && s.addedGamesDirs != null) SettingsGroup("Added games") { for (dir in s.addedGamesDirs) { val n = s.addedGames.count { it.folderPath.startsWith("$dir/") } ActionRow( @@ -378,20 +417,15 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) { onPick = { path -> if (path == "__pick__") a.onPickAddedGameExe(g.folderPath) else a.onAddedGameExe(g.folderPath, path) }, ) } - if (steam && s.fexPreset != null) SettingsGroup(stringResource(R.string.game_settings_title)) { + if (steam && tab == 4 && s.fexPreset != null) SettingsGroup(stringResource(R.string.game_settings_title)) { ChoiceRow( host, "fex", stringResource(R.string.fex_preset_title), stringResource(R.string.fex_next_launch), FexPreset.all.map { it.id to stringResource(it.label) }, s.fexPreset, note = stringResource(FexPreset.byId(s.fexPreset).detail), onPick = a.onFexPreset, ) GameEnvironmentRow() - if (s.forceFullscreen != null) ToggleRow( - host, "fill", "Stretch games to fill the screen", - "Keeps games that resize their own window (FlatOut) full screen. Turn it off if a game shows up small in a corner (Quake 3). Applies next session.", - s.forceFullscreen, onChange = a.onForceFullscreen, - ) } - if (steam && s.directAudio != null && s.mic != null) SettingsGroup("Audio") { + if (steam && tab == 2 && s.directAudio != null && s.mic != null) SettingsGroup("Audio") { ToggleRow(host, "da", "DirectAudio for games", "Bypasses PulseAudio for lower latency in games.", s.directAudio, onChange = a.onDirectAudio) ChoiceRow( host, "clientAudio", "Steam client audio", "Classic is the AAudio sink from 0.1.5. DirectAudio goes through the relay. Applies next session.", @@ -400,7 +434,7 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) { ) ToggleRow(host, "mic", "Microphone", "Uses the device microphone for voice chat.", s.mic, onChange = a.onMic) } - if (steam && s.gameStorage != null) SettingsGroup("Game storage") { + if (steam && tab == 4 && s.gameStorage != null) SettingsGroup("Game storage") { val custom = s.gameStorage.isNotEmpty() && s.gameStorage != "off" && s.storageOptions.none { it.second == s.gameStorage } val options = buildList { add("" to ("Automatic - the SD card when one is in" + (if (s.storageOptions.isEmpty()) " (none right now)" else ""))) @@ -431,7 +465,7 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) { } } } - if (!steam && s.renderer != null) SettingsGroup("Renderer") { + if (!steam && tab == 2 && s.renderer != null) SettingsGroup("Renderer") { ChoiceRow( host, "renderer", "Desktop renderer", "Composites the desktop.", listOf("vulkan" to "vulkan - GPU", "gles2" to "gles2 - GPU (experimental)", "pixman" to "pixman - software"), s.renderer, @@ -441,6 +475,7 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) { onPick = a.onRenderer, ) } + } } if (confirmDeckyRemoval) AlertDialog( onDismissRequest = { confirmDeckyRemoval = false }, diff --git a/app/src/main/java/com/droiddeck/launcher/ui/PerformanceDialog.kt b/app/src/main/java/com/droiddeck/launcher/ui/PerformanceDialog.kt index 7756cd72f..e4c407c83 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/PerformanceDialog.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/PerformanceDialog.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import com.droiddeck.launcher.gpu.FrameGen import com.droiddeck.launcher.session.SessionPrefs class CoreRow(val core: Int, val label: String) @@ -40,6 +41,10 @@ fun PerformancePage( prootNoSeccomp: Boolean, guestHostname: String, phantomWarning: String?, + frameGenEngine: String, + frameGenMultiplier: Int, + lsfgReady: Boolean, + onFrameGenPick: (engine: String, multiplier: Int) -> Unit, onClientOverride: (Boolean) -> Unit, onTuSysmem: (Boolean) -> Unit, onZinkLazy: (Boolean) -> Unit, @@ -57,9 +62,20 @@ fun PerformancePage( val coreItems = cores.map { it.core to it.label } SettingsPage( host, title = "Performance", - lede = "CPU cores, session fixes and the host name. Applies next session.", + lede = "Rendering, CPU cores and session tuning. Most changes apply next session.", onBack = onDismiss, ) { + SettingsGroup("Rendering") { + val fgOpen = host.open == "fg" + SettingsRow("Frame generation", "Default for game launches; it can still be changed per game or while running.", highlighted = fgOpen) { + androidx.compose.foundation.layout.Box { + ValueChip(FrameGen.label(frameGenEngine, frameGenMultiplier), fgOpen) { + host.open = if (fgOpen) null else "fg" + } + FrameGenMenu(host, frameGenEngine, frameGenMultiplier, lsfgReady, onFrameGenPick) + } + } + } SettingsGroup("Steam client cores") { ToggleRow( host, "override", "Override Steam's own core choice", diff --git a/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt b/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt index d853906a8..286133b5f 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/SessionOverlay.kt @@ -102,7 +102,7 @@ import com.droiddeck.launcher.input.SecondScreenDisplay import com.droiddeck.launcher.input.SecondScreenMode import kotlinx.coroutines.flow.collect -private val drawerPageTitles = listOf("Display", "Controls", "Components", "Settings") +private val drawerPageTitles = listOf("Display", "Controls", "Components", "Session") /** One icon per drawer page, in page order (QAM-style tabs). */ private val drawerPageIcons = listOf(Icons.Outlined.DesktopWindows, Icons.Outlined.SportsEsports, Icons.Outlined.Layers, Icons.Outlined.Settings)