From 93db74642c3b62f44d78bd74e9afdac31c6a0edd Mon Sep 17 00:00:00 2001 From: magartus Date: Mon, 28 Sep 2026 21:58:39 +0200 Subject: [PATCH] Add an opt-in 16:9 stretch for screens narrower than 16:9 On a foldable's inner panel (Galaxy Z Fold8: 2448x1848, ~4:3) gamescope is started with the nested size equal to the output, so games are asked for the panel's own shape. Many cannot do it and letterbox themselves, and gamescope's scaler has nothing to stretch. "Stretch games to fill the screen" only forces window size, so it does not help. New Steam setting "Stretch 16:9 games to a narrower screen" (off by default): bannerlator-session then gives games a 16:9 surface at the full output height (-w) and adds -S stretch, so they render at a shape they support and fill the panel with no bars and no crop. An output that is already 16:9 or wider is left untouched. Passed as BL_GAMESCOPE_STRETCH_16X9 and listed in device.txt. --- .../main/java/com/droiddeck/launcher/MainActivity.kt | 4 ++++ .../java/com/droiddeck/launcher/core/DeviceReport.kt | 1 + .../com/droiddeck/launcher/session/SessionPrefs.kt | 6 ++++++ .../com/droiddeck/launcher/session/SessionService.kt | 1 + .../com/droiddeck/launcher/ui/ModeSettingsDialog.kt | 7 +++++++ .../linuxfs/overlay/usr/local/bin/bannerlator-session | 11 ++++++++++- 6 files changed, 29 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/droiddeck/launcher/MainActivity.kt b/app/src/main/java/com/droiddeck/launcher/MainActivity.kt index eaec48d8f..7207f4cae 100644 --- a/app/src/main/java/com/droiddeck/launcher/MainActivity.kt +++ b/app/src/main/java/com/droiddeck/launcher/MainActivity.kt @@ -130,6 +130,7 @@ class MainActivity : ComponentActivity() { private var directAudio by mutableStateOf(false) private var clientDirectAudio by mutableStateOf(false) private var forceFullscreen by mutableStateOf(true) + private var stretch16x9 by mutableStateOf(false) private var launcherFullscreen by mutableStateOf(true) private var mic by mutableStateOf(false) @@ -776,6 +777,7 @@ class MainActivity : ComponentActivity() { directAudio = if (mode == SessionService.MODE_STEAM) directAudio else null, clientDirectAudio = clientDirectAudio, forceFullscreen = if (mode == SessionService.MODE_STEAM) forceFullscreen else null, + stretch16x9 = if (mode == SessionService.MODE_STEAM) stretch16x9 else null, mic = if (mode == SessionService.MODE_STEAM) mic else null, renderer = if (mode == SessionService.MODE_DESKTOP) renderer else null, gameStorage = if (mode == SessionService.MODE_STEAM) gameStorage else null, @@ -817,6 +819,7 @@ class MainActivity : ComponentActivity() { onDirectAudio = { on -> SessionPrefs.setDirectAudio(this, on); directAudio = on }, onClientDirectAudio = { on -> SessionPrefs.setClientDirectAudio(this, on); clientDirectAudio = on }, onForceFullscreen = { on -> SessionPrefs.setForceFullscreen(this, on); forceFullscreen = on }, + onStretch16x9 = { on -> SessionPrefs.setStretch16x9(this, on); stretch16x9 = on }, onMic = { on -> SessionPrefs.setMicEnabled(this, on) mic = on @@ -928,6 +931,7 @@ class MainActivity : ComponentActivity() { directAudio = SessionPrefs.directAudio(this) clientDirectAudio = SessionPrefs.clientDirectAudio(this) forceFullscreen = SessionPrefs.forceFullscreen(this) + stretch16x9 = SessionPrefs.stretch16x9(this) mic = SessionPrefs.micEnabled(this) renderer = SessionPrefs.desktopRenderer(this) gameStorage = SessionPrefs.gameStorage(this) diff --git a/app/src/main/java/com/droiddeck/launcher/core/DeviceReport.kt b/app/src/main/java/com/droiddeck/launcher/core/DeviceReport.kt index 9ff0c002a..cae44712c 100644 --- a/app/src/main/java/com/droiddeck/launcher/core/DeviceReport.kt +++ b/app/src/main/java/com/droiddeck/launcher/core/DeviceReport.kt @@ -149,6 +149,7 @@ object DeviceReport { k("proot without seccomp", SessionPrefs.prootNoSeccomp(context)) k("DirectAudio for games", SessionPrefs.directAudio(context)) k("Stretch games to fill", SessionPrefs.forceFullscreen(context)) + k("Stretch 16:9 to panel", SessionPrefs.stretch16x9(context)) k("Client audio", if (SessionPrefs.clientDirectAudio(context)) "DirectAudio" else "classic") k("Microphone", SessionPrefs.micEnabled(context)) k("On-screen controls", SessionPrefs.oscMode(context)) diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt index 0f3aefe6e..74ce0a9e2 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt @@ -120,6 +120,12 @@ object SessionPrefs { writeForceFullscreenFlag(context) } + fun stretch16x9(context: Context): Boolean = prefs(context).getBoolean("stretch16x9", false) + + fun setStretch16x9(context: Context, on: Boolean) { + prefs(context).edit().putBoolean("stretch16x9", on).apply() + } + /** * The same choice as a file the running session watches, so the drawer can change it live: * the session hands every change to gamescope, which reads GAMESCOPE_FORCE_WINDOWS_FULLSCREEN diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt index a071e2686..888d37fa7 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionService.kt @@ -522,6 +522,7 @@ class SessionService : Service() { if (steamHere) guest.add("BL_STEAM_CHANNEL=" + SessionPrefs.steamChannel(this)) if (SessionState.mode == MODE_STEAM) { guest.add("BL_GAMESCOPE_FORCE_FULLSCREEN=" + (if (SessionPrefs.forceFullscreen(this)) "1" else "0")) + guest.add("BL_GAMESCOPE_STRETCH_16X9=" + (if (SessionPrefs.stretch16x9(this)) "1" else "0")) SessionPrefs.writeForceFullscreenFlag(this) } // Proton's own gate for its xalia helper (its `proton` script reads this, and sets 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 0d6bb8003..66d0c5f52 100644 --- a/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt +++ b/app/src/main/java/com/droiddeck/launcher/ui/ModeSettingsDialog.kt @@ -66,6 +66,7 @@ class ModeSettings( val fexPreset: String? = null, /** Steam only: games are stretched to fill the screen (null = not a Steam page). */ val forceFullscreen: Boolean? = null, + val stretch16x9: Boolean? = null, /** Steam only: the client branch forced on the command line. */ val steamChannel: String? = null, /** Steam only: start a Steam session when DroidDeck opens. */ @@ -122,6 +123,7 @@ class ModeSettingsActions( val onPickGameStorageFolder: () -> Unit = {}, val onFexPreset: (String) -> Unit = {}, val onForceFullscreen: (Boolean) -> Unit = {}, + val onStretch16x9: (Boolean) -> Unit = {}, val onSteamChannel: (String) -> Unit = {}, val onRunSteamAtStartup: (Boolean) -> Unit = {}, val onPickAddedGamesDir: () -> Unit = {}, @@ -382,6 +384,11 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) { "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 (s.stretch16x9 != null) ToggleRow( + host, "stretch169", "Stretch 16:9 games to a narrower screen", + "On a screen squarer than 16:9 (a foldable's inner panel), games render at 16:9 and are stretched to fill it: no bars, slightly wider-looking. Applies next session.", + s.stretch16x9, onChange = a.onStretch16x9, + ) } if (steam && 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) diff --git a/tools/linuxfs/overlay/usr/local/bin/bannerlator-session b/tools/linuxfs/overlay/usr/local/bin/bannerlator-session index c0adc95e7..ab1f6f568 100755 --- a/tools/linuxfs/overlay/usr/local/bin/bannerlator-session +++ b/tools/linuxfs/overlay/usr/local/bin/bannerlator-session @@ -125,7 +125,16 @@ if [ -z "${BL_INSIDE:-}" ]; then fi width=${BL_WIDTH:-1280} height=${BL_HEIGHT:-720} - args=(--backend wayland --expose-wayland -f -W "$width" -H "$height" -w "$width" -h "$height") + nested=$width + if [ "$mode" = steam ] && [ "${BL_GAMESCOPE_STRETCH_16X9:-0}" = 1 ]; then + wide=$(( (height * 16 + 4) / 9 / 2 * 2 )) + if [ "$wide" -gt "$width" ]; then + nested=$wide + echo "== gamescope: games at ${nested}x${height} stretched to the ${width}x${height} output" + fi + fi + args=(--backend wayland --expose-wayland -f -W "$width" -H "$height" -w "$nested" -h "$height") + [ "$nested" != "$width" ] && args+=(-S stretch) # -r is what gamescope advertises as the refresh rate, and what a game reads as the display's. # Left unset it defaults to 60, so a 120 Hz panel offers only 60 Hz in game settings. The FPS # limiter wins when it is on (it is a deliberate cap); otherwise use the panel's own rate.