diff --git a/README.md b/README.md index e3648d4ec..039c6fa1f 100644 --- a/README.md +++ b/README.md @@ -70,9 +70,10 @@ Pick an engine and 2×/3×/4× on the main screen; it applies immediately, mid-g A session belongs to a foreground service, not to the activity, so leaving Big Picture does not end it: the process stays at perceptible priority, a partial wake lock keeps the CPU from dropping the guest's threads, and a high-performance WiFi lock keeps a backgrounded download from being -throttled to nothing. Back puts the app away and leaves Steam running; the (silent) notification -brings it back. A session ends only when you say so — the notification's **Stop session** action — -or when the app is swiped out of recents. +throttled to nothing. **Back opens the drawer**: the performance HUD switch, frame generation, on-screen controls +(auto / always / never), *Send to background* and *Stop session*. Backgrounded, Steam keeps +running and the (silent) notification brings it back. A session ends only when you say so — the +drawer or the notification's **Stop session** — or when the app is swiped out of recents. ## Device switches diff --git a/app/src/main/java/com/steamdeck/launcher/MainActivity.kt b/app/src/main/java/com/steamdeck/launcher/MainActivity.kt index 71d9c613f..f8ac2bdae 100644 --- a/app/src/main/java/com/steamdeck/launcher/MainActivity.kt +++ b/app/src/main/java/com/steamdeck/launcher/MainActivity.kt @@ -44,7 +44,7 @@ class MainActivity : Activity() { playButton = findViewById(R.id.play) runtimeButton = findViewById(R.id.runtime) frameGenButton = findViewById(R.id.frame_gen) - frameGenButton.setOnClickListener { pickFrameGen() } + frameGenButton.setOnClickListener { FrameGen.showPicker(this) { render() } } playButton.setOnClickListener { startActivity(Intent(this, SessionActivity::class.java)) } runtimeButton.setOnClickListener { onRuntimeButton() } @@ -123,34 +123,6 @@ class MainActivity : Activity() { ui.post { render() } } - /** Off, or an engine at 2x/3x/4x. Saved, and pushed to a running session on the next resume. */ - private fun pickFrameGen() { - val choices = arrayOf("Off", - "Win-FG 2×", "Win-FG 3×", "Win-FG 4×", - "LSFG 2× (needs Lossless Scaling)", "LSFG 3× (needs Lossless Scaling)", "LSFG 4× (needs Lossless Scaling)") - val engine = FrameGen.engine(this) - val current = when (engine) { - FrameGen.ENGINE_WINFG -> FrameGen.multiplier(this) - 1 - FrameGen.ENGINE_LSFG -> FrameGen.multiplier(this) + 2 - else -> 0 - } - AlertDialog.Builder(this) - .setTitle(R.string.frame_gen_title) - .setSingleChoiceItems(choices, current) { dialog, which -> - when (which) { - 0 -> FrameGen.set(this, FrameGen.ENGINE_OFF, 2) - in 1..3 -> FrameGen.set(this, FrameGen.ENGINE_WINFG, which + 1) - else -> FrameGen.set(this, FrameGen.ENGINE_LSFG, which - 2) - } - dialog.dismiss() - render() - } - .setNeutralButton(android.R.string.ok, null) - .setMessage(null) - .show() - Log.i(TAG, "frame generation picker: " + FrameGen.label(this)) - } - private fun render() { frameGenButton.text = getString(R.string.frame_gen, FrameGen.label(this)) val installed = LinuxRuntimeInstaller.installedVersion(this) diff --git a/app/src/main/java/com/steamdeck/launcher/SessionActivity.kt b/app/src/main/java/com/steamdeck/launcher/SessionActivity.kt index 722ef187b..2b86046d9 100644 --- a/app/src/main/java/com/steamdeck/launcher/SessionActivity.kt +++ b/app/src/main/java/com/steamdeck/launcher/SessionActivity.kt @@ -24,6 +24,8 @@ import com.steamdeck.launcher.input.PadBridge import com.steamdeck.launcher.runtime.LinuxRuntime import com.steamdeck.launcher.session.LoadingPanel import com.steamdeck.launcher.session.PerfHud +import com.steamdeck.launcher.session.SessionDrawer +import com.steamdeck.launcher.session.SessionPrefs import com.steamdeck.launcher.session.SessionService import com.steamdeck.launcher.session.SessionState import com.steamdeck.launcher.wayland.CompositorHost @@ -44,6 +46,7 @@ class SessionActivity : Activity(), SurfaceHolder.Callback { private lateinit var surfaceView: SurfaceView private lateinit var loading: LoadingPanel private lateinit var hud: PerfHud + private lateinit var drawer: SessionDrawer private var padBridge: PadBridge? = null private var onScreenControls: OnScreenControls? = null private var watching = true @@ -83,6 +86,13 @@ class SessionActivity : Activity(), SurfaceHolder.Callback { // client's first frame, since before that there is nothing to count. hud = PerfHud(this) root.addView(hud.view, root.indexOfChild(panel)) + // Back opens this; it sits above everything, the loading panel included. + drawer = SessionDrawer(this, root, + onHudChanged = { hud.refresh() }, + onOscChanged = { updateOnScreenControls() }, + onFrameGenChanged = { applyFrameGen() }, + onBackground = { moveTaskToBack(true) }, + onStop = { SessionService.stop(this); finish() }) // A session that has already drawn is past its milestones; do not cover its picture. if (SessionState.running && SessionState.firstFrameSeen) { loading.hide() @@ -259,26 +269,27 @@ class SessionActivity : Activity(), SurfaceHolder.Callback { // ── Lifecycle ─────────────────────────────────────────────────────────────────────────── /** - * Back leaves the session running and puts the app behind whatever the user wants next; the - * notification brings it back. Ending a session is done deliberately — from the - * notification's Stop action, or by swiping the app out of recents. + * Back opens the drawer (and closes it again). Leaving the session running in the background + * and ending it are both actions in there, so neither can happen by accident from a button + * a game might also be reading. */ @Suppress("DEPRECATION") override fun onBackPressed() { - moveTaskToBack(true) + drawer.toggle() } /** - * `steamdeck-osc` in Downloads forces the matter either way ("always" / "never"); with no such - * file the controls follow what is attached. + * The drawer's mode ("always" / "never") decides outright; on "auto" the controls follow + * what is attached. `steamdeck-osc` in Downloads still overrides, for a device we cannot reach. */ private fun updateOnScreenControls() { val forced = File(Environment.getExternalStorageDirectory(), "Download/steamdeck-osc") .takeIf { it.isFile } ?.let { FileUtils.readString(it)?.trim()?.lowercase() } - val show = when { - forced == "always" -> true - forced == "never" -> false + ?: SessionPrefs.oscMode(this) + val show = when (forced) { + SessionPrefs.OSC_ALWAYS -> true + SessionPrefs.OSC_NEVER -> false else -> !PadBridge.anyControllerConnected() } val controls = onScreenControls ?: return diff --git a/app/src/main/java/com/steamdeck/launcher/gpu/FrameGen.kt b/app/src/main/java/com/steamdeck/launcher/gpu/FrameGen.kt index 901d9ca82..d7b4d12b8 100644 --- a/app/src/main/java/com/steamdeck/launcher/gpu/FrameGen.kt +++ b/app/src/main/java/com/steamdeck/launcher/gpu/FrameGen.kt @@ -82,5 +82,30 @@ object FrameGen { return problem } + /** Off, or an engine at 2x/3x/4x; saved on pick, then [onChanged]. */ + fun showPicker(activity: android.app.Activity, onChanged: () -> Unit) { + val choices = arrayOf("Off", + "Win-FG 2×", "Win-FG 3×", "Win-FG 4×", + "LSFG 2× (needs Lossless Scaling)", "LSFG 3× (needs Lossless Scaling)", "LSFG 4× (needs Lossless Scaling)") + val current = when (engine(activity)) { + ENGINE_WINFG -> multiplier(activity) - 1 + ENGINE_LSFG -> multiplier(activity) + 2 + else -> 0 + } + android.app.AlertDialog.Builder(activity) + .setTitle(com.steamdeck.launcher.R.string.frame_gen_title) + .setSingleChoiceItems(choices, current) { dialog, which -> + when (which) { + 0 -> set(activity, ENGINE_OFF, 2) + in 1..3 -> set(activity, ENGINE_WINFG, which + 1) + else -> set(activity, ENGINE_LSFG, which - 2) + } + dialog.dismiss() + onChanged() + } + .setNeutralButton(android.R.string.ok, null) + .show() + } + private const val TAG = "FrameGen" } diff --git a/app/src/main/java/com/steamdeck/launcher/session/PerfHud.kt b/app/src/main/java/com/steamdeck/launcher/session/PerfHud.kt index b2add2abf..356a16a10 100644 --- a/app/src/main/java/com/steamdeck/launcher/session/PerfHud.kt +++ b/app/src/main/java/com/steamdeck/launcher/session/PerfHud.kt @@ -49,7 +49,10 @@ class PerfHud(context: Context) { private var lastTick = 0L private var running = false private val handler = Handler(Looper.getMainLooper()) - private val enabled = !File(Environment.getExternalStorageDirectory(), "Download/steamdeck-no-hud").exists() + /** The session's own switch (drawer), with the Downloads file as a device-side override. */ + private val enabled: Boolean + get() = SessionPrefs.hudEnabled(context) + && !File(Environment.getExternalStorageDirectory(), "Download/steamdeck-no-hud").exists() private val listener = object : WaylandCompositor.GameListener { override fun onGameSurface(window: String?, gpuName: String?) {} @@ -68,6 +71,11 @@ class PerfHud(context: Context) { } } + /** Re-reads the switch: starts or stops to match it. */ + fun refresh() { + if (enabled) start() else stop() + } + fun start() { if (!enabled || running) return running = true diff --git a/app/src/main/java/com/steamdeck/launcher/session/SessionDrawer.kt b/app/src/main/java/com/steamdeck/launcher/session/SessionDrawer.kt new file mode 100644 index 000000000..e2a9a81eb --- /dev/null +++ b/app/src/main/java/com/steamdeck/launcher/session/SessionDrawer.kt @@ -0,0 +1,89 @@ +package com.steamdeck.launcher.session + +import android.app.Activity +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.Switch +import com.steamdeck.launcher.R +import com.steamdeck.launcher.gpu.FrameGen + +/** + * The drawer Back opens over a running session. It owns nothing: every control reads and writes + * a preference or calls back into the activity, so closing it changes nothing by itself. + */ +class SessionDrawer( + private val activity: Activity, + parent: ViewGroup, + private val onHudChanged: (Boolean) -> Unit, + private val onOscChanged: () -> Unit, + private val onFrameGenChanged: () -> Unit, + private val onBackground: () -> Unit, + private val onStop: () -> Unit, +) { + private val root: View = activity.layoutInflater.inflate(R.layout.session_drawer, parent, false) + private val hud = root.findViewById(R.id.drawer_hud) + private val frameGen = root.findViewById