mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 11:01:07 +00:00
Merge pull request #32 from The412Banner/feat/fullscreen-toggle
Stretch games to fill the screen (cog + live drawer), Quake-engine titles windowed at the session's size
This commit is contained in:
9 files changed
+101
-4
No files matched your search
@@ -105,6 +105,7 @@ class MainActivity : ComponentActivity() {
|
||||
private var phantomWarning by mutableStateOf<String?>(null)
|
||||
private var directAudio by mutableStateOf(false)
|
||||
private var clientDirectAudio by mutableStateOf(false)
|
||||
private var forceFullscreen by mutableStateOf(true)
|
||||
private var mic by mutableStateOf(false)
|
||||
private var linuxRows by mutableStateOf<List<DriverRow>>(emptyList())
|
||||
/** The latest Banners-Turnip release as each driver menu offers it (see [refreshReleaseRows]). */
|
||||
@@ -526,6 +527,7 @@ class MainActivity : ComponentActivity() {
|
||||
backActionsInverted = backActionsInverted,
|
||||
directAudio = if (mode == SessionService.MODE_STEAM) directAudio else null,
|
||||
clientDirectAudio = clientDirectAudio,
|
||||
forceFullscreen = if (mode == SessionService.MODE_STEAM) forceFullscreen 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,
|
||||
@@ -561,6 +563,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 },
|
||||
onMic = { on ->
|
||||
SessionPrefs.setMicEnabled(this, on)
|
||||
mic = on
|
||||
@@ -657,6 +660,7 @@ class MainActivity : ComponentActivity() {
|
||||
backActionsInverted = SessionPrefs.backActionsInverted(this)
|
||||
directAudio = SessionPrefs.directAudio(this)
|
||||
clientDirectAudio = SessionPrefs.clientDirectAudio(this)
|
||||
forceFullscreen = SessionPrefs.forceFullscreen(this)
|
||||
mic = SessionPrefs.micEnabled(this)
|
||||
renderer = SessionPrefs.desktopRenderer(this)
|
||||
gameStorage = SessionPrefs.gameStorage(this)
|
||||
|
||||
@@ -101,6 +101,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
/** The on-screen PC keyboard (ui/PcKeyboard): real key presses, Esc and F1 included. */
|
||||
private var pcKeyboardOpen by mutableStateOf(false)
|
||||
private var hudOn by mutableStateOf(true)
|
||||
private var fillScreen by mutableStateOf(true)
|
||||
private var frameGenLabel by mutableStateOf("Off")
|
||||
private var frameGenEngine by mutableStateOf(FrameGen.ENGINE_OFF)
|
||||
private var frameGenMultiplier by mutableStateOf(2)
|
||||
@@ -217,6 +218,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
isHomeApp = isHomeApp,
|
||||
androidApps = androidApps,
|
||||
hudOn = hudOn,
|
||||
fillScreen = if (SessionState.mode == SessionService.MODE_STEAM) fillScreen else null,
|
||||
frameGenEngine = frameGenEngine, frameGenMultiplier = frameGenMultiplier,
|
||||
lsfgReady = LsfgNative.isInstalled(this@SessionActivity),
|
||||
oscMode = oscMode, touchMode = touchMode,
|
||||
@@ -226,6 +228,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
secondScreenDisplays = secondScreenDisplays,
|
||||
selectedSecondScreenDisplay = selectedSecondScreenDisplay,
|
||||
onHud = { on -> SessionPrefs.setHudEnabled(this@SessionActivity, on); hudOn = on; hud.refresh() },
|
||||
onFillScreen = { on -> SessionPrefs.setForceFullscreen(this@SessionActivity, on); fillScreen = on },
|
||||
onFrameGenPick = { engine, multiplier ->
|
||||
FrameGen.set(this@SessionActivity, engine, multiplier)
|
||||
readPrefs()
|
||||
@@ -340,6 +343,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
|
||||
private fun readPrefs() {
|
||||
hudOn = SessionPrefs.hudEnabled(this)
|
||||
fillScreen = SessionPrefs.forceFullscreen(this)
|
||||
touchMode = SessionPrefs.touchMode(this)
|
||||
frameGenLabel = FrameGen.label(this)
|
||||
frameGenEngine = FrameGen.engine(this)
|
||||
|
||||
@@ -149,6 +149,7 @@ object DeviceReport {
|
||||
k("Skip xalia", SessionPrefs.noXalia(context))
|
||||
k("proot without seccomp", SessionPrefs.prootNoSeccomp(context))
|
||||
k("DirectAudio for games", SessionPrefs.directAudio(context))
|
||||
k("Stretch games to fill", SessionPrefs.forceFullscreen(context))
|
||||
k("Client audio", if (SessionPrefs.clientDirectAudio(context)) "DirectAudio" else "classic")
|
||||
k("Microphone", SessionPrefs.micEnabled(context))
|
||||
k("On-screen controls", SessionPrefs.oscMode(context))
|
||||
|
||||
@@ -100,6 +100,32 @@ object SessionPrefs {
|
||||
prefs(context).edit().putBoolean("clientDirectAudio", on).apply()
|
||||
}
|
||||
|
||||
/**
|
||||
* Steam only: gamescope makes every game window the size of the screen. A game that resizes
|
||||
* its own window when it loses focus (FlatOut) otherwise comes back smaller, drawn in a
|
||||
* corner; a game that sets its own resolution and never looks at its window again (Quake 3)
|
||||
* instead draws small in the bottom-left of the stretched one. On unless turned off.
|
||||
*/
|
||||
fun forceFullscreen(context: Context): Boolean = prefs(context).getBoolean("forceFullscreen", true)
|
||||
|
||||
fun setForceFullscreen(context: Context, on: Boolean) {
|
||||
prefs(context).edit().putBoolean("forceFullscreen", on).apply()
|
||||
writeForceFullscreenFlag(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* off its root window whenever it changes. Written again at every session start so a file left
|
||||
* by an earlier session never disagrees with the setting.
|
||||
*/
|
||||
fun writeForceFullscreenFlag(context: Context) {
|
||||
runCatching {
|
||||
java.io.File(com.droiddeck.launcher.runtime.LinuxRuntime.rootDir(context), "root/.droiddeck-fill")
|
||||
.writeText(if (forceFullscreen(context)) "1\n" else "0\n")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DirectAudio for games: their Wine audio driver talks to the relay helper on this side. On
|
||||
* unless the user turned it off.
|
||||
|
||||
@@ -304,6 +304,10 @@ class SessionService : Service() {
|
||||
if (SessionPrefs.noGlError(this)) guest.add("MESA_NO_ERROR=1")
|
||||
if (SessionState.mode == MODE_STEAM) guest.add("BL_STEAMDECK=" + (if (SessionPrefs.steamDeckMode(this)) "1" else "0"))
|
||||
if (SessionState.mode == MODE_STEAM) 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"))
|
||||
SessionPrefs.writeForceFullscreenFlag(this)
|
||||
}
|
||||
// Proton's own gate for its xalia helper (its `proton` script reads this, and sets
|
||||
// XALIA_SUPPORTED_ONLY itself otherwise). Off by default: xalia is Valve's, and on a device
|
||||
// whose seccomp answers its syscalls normally there is no reason to take it away.
|
||||
|
||||
@@ -55,6 +55,8 @@ class ModeSettings(
|
||||
val gameStorage: String? = null,
|
||||
val storageOptions: List<Pair<String, String>> = emptyList(),
|
||||
val fexPreset: String? = null,
|
||||
/** Steam only: games are stretched to fill the screen (null = not a Steam page). */
|
||||
val forceFullscreen: Boolean? = null,
|
||||
/** Steam only: the client branch forced on the command line. */
|
||||
val steamChannel: String? = null,
|
||||
/** Steam only: the user's own games folder and what was found in it. */
|
||||
@@ -101,6 +103,7 @@ class ModeSettingsActions(
|
||||
val onGameStorage: (path: String, label: String) -> Unit = { _, _ -> },
|
||||
val onPickGameStorageFolder: () -> Unit = {},
|
||||
val onFexPreset: (String) -> Unit = {},
|
||||
val onForceFullscreen: (Boolean) -> Unit = {},
|
||||
val onSteamChannel: (String) -> Unit = {},
|
||||
val onPickAddedGamesDir: () -> Unit = {},
|
||||
val onForgetAddedGamesDir: (path: String) -> Unit = {},
|
||||
@@ -290,6 +293,11 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
FexPreset.all.map { it.id to it.label }, s.fexPreset,
|
||||
note = FexPreset.byId(s.fexPreset).detail, onPick = a.onFexPreset,
|
||||
)
|
||||
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") {
|
||||
ToggleRow(host, "da", "DirectAudio for games", "Bypasses PulseAudio for lower latency in games.", s.directAudio, onChange = a.onDirectAudio)
|
||||
|
||||
@@ -161,6 +161,8 @@ class DrawerActions(
|
||||
val touchAuto: String,
|
||||
val shapeMode: String,
|
||||
val fexPreset: String,
|
||||
/** Steam only: games stretched to the screen's size, changed live (null = not Steam). */
|
||||
val fillScreen: Boolean? = null,
|
||||
val secondScreenMode: SecondScreenMode,
|
||||
val secondScreenDisplays: List<SecondScreenDisplay>,
|
||||
val selectedSecondScreenDisplay: Int,
|
||||
@@ -177,6 +179,7 @@ class DrawerActions(
|
||||
val onTouch: (String) -> Unit,
|
||||
val onShape: (String) -> Unit,
|
||||
val onFexPreset: (String) -> Unit,
|
||||
val onFillScreen: (Boolean) -> Unit = {},
|
||||
val onSecondScreenMode: (SecondScreenMode) -> Unit,
|
||||
val onSecondScreenDisplay: (Int) -> Unit,
|
||||
val onLaunchAndroidApp: (HomeApp.LaunchableApp) -> Unit,
|
||||
@@ -317,6 +320,10 @@ fun SessionDrawer(open: Boolean, a: DrawerActions) {
|
||||
chipModifier = if (steamButtons) Modifier else Modifier.focusRequester(firstFocus)
|
||||
.onFocusChanged { firstControlFocused = it.isFocused }, onChange = a.onHud,
|
||||
)
|
||||
if (a.fillScreen != null) ToggleRow(
|
||||
host, "fill", "Stretch games to fill the screen",
|
||||
"Off for a game that shows up small in a corner (Quake 3).", a.fillScreen, onChange = a.onFillScreen,
|
||||
)
|
||||
val fgOpen = host.open == "fg"
|
||||
val fgLabel = when (a.frameGenEngine) {
|
||||
FrameGen.ENGINE_WINFG -> "Win-FG ${a.frameGenMultiplier}×"
|
||||
|
||||
@@ -347,6 +347,22 @@ case $mode in
|
||||
# moments later, so the seeder has only that gap to mark the redistributables as already run.
|
||||
# It stamps each prefix it has covered and skips it thereafter, so the extra passes cost
|
||||
# almost nothing.
|
||||
# The drawer's "Stretch games to fill the screen" works live: the app writes its choice to
|
||||
# ~/.droiddeck-fill, and each change goes to gamescope as GAMESCOPE_FORCE_WINDOWS_FULLSCREEN on
|
||||
# the root window, which it watches. What gamescope was started with is where this begins.
|
||||
(
|
||||
last=${BL_GAMESCOPE_FORCE_FULLSCREEN:-1}
|
||||
while :; do
|
||||
sleep 1
|
||||
want=$(cat "$HOME/.droiddeck-fill" 2>/dev/null) || continue
|
||||
case $want in 0|1) ;; *) continue ;; esac
|
||||
[ "$want" = "$last" ] && continue
|
||||
if xprop -root -f GAMESCOPE_FORCE_WINDOWS_FULLSCREEN 32c -set GAMESCOPE_FORCE_WINDOWS_FULLSCREEN "$want" 2>/dev/null; then
|
||||
echo "== stretch games to fill the screen: $want (live)"
|
||||
last=$want
|
||||
fi
|
||||
done
|
||||
) &
|
||||
(
|
||||
beat=0
|
||||
for _ in $(seq 1 720); do
|
||||
|
||||
@@ -87,6 +87,31 @@ bl_game_cpus() {
|
||||
}
|
||||
"""
|
||||
|
||||
# Quake III engine games (id Tech 3) set their own resolution and switch the display to it. Under
|
||||
# gamescope with the ARM64 Proton that switch is refused ("calling CDS: failed, bad mode") and the
|
||||
# game never gets OpenGL; left at its own size in a window, it draws small in a corner. So these
|
||||
# titles are told to stay windowed at the session's own size - unless the user's launch options
|
||||
# already say something about the video mode, which are left as they are.
|
||||
BL_IDTECH3_SETUP = """
|
||||
bl_idtech3() {
|
||||
BL_EXTRA_ARGS=()
|
||||
case "${SteamAppId:-${STEAM_COMPAT_APP_ID:-}}" in
|
||||
# Quake III Arena, Quake III: Team Arena, Return to Castle Wolfenstein, Jedi Academy.
|
||||
2200|2350|9010|6020) ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
case " $* " in
|
||||
*r_fullscreen*|*r_mode*|*r_customwidth*|*r_customheight*)
|
||||
echo "bannerlator-proton: Quake-engine title; its launch options set the video mode, left as they are" >&2
|
||||
return 0 ;;
|
||||
esac
|
||||
w=${BL_WIDTH:-1280}
|
||||
h=${BL_HEIGHT:-720}
|
||||
BL_EXTRA_ARGS=(+set r_fullscreen 0 +set r_mode -1 +set r_customwidth "$w" +set r_customheight "$h")
|
||||
echo "bannerlator-proton: Quake-engine title: windowed at ${w}x${h}" >&2
|
||||
}
|
||||
"""
|
||||
|
||||
BL_DIRECTAUDIO_SETUP = """
|
||||
bl_directaudio() {
|
||||
base=$1
|
||||
@@ -152,7 +177,8 @@ for name in %s; do
|
||||
if [ -d "$depot/files/bin-arm64" ] && [ -x "$depot/proton" ]; then
|
||||
bl_directaudio "$depot"
|
||||
bl_game_cpus
|
||||
exec ${BL_TASKSET:-} "$depot/proton" "$verb" "$@"
|
||||
bl_idtech3 "$@"
|
||||
exec ${BL_TASKSET:-} "$depot/proton" "$verb" "$@" "${BL_EXTRA_ARGS[@]}"
|
||||
fi
|
||||
done
|
||||
echo "bannerlator-proton: no ARM64 Proton depot yet - the client downloads one as this tool's dependency; retry the launch once it has" >&2
|
||||
@@ -162,7 +188,7 @@ exit 1
|
||||
# and a % format would eat that into a single %, leaving a shell expansion that silently does
|
||||
# something else.
|
||||
LAUNCHER_SH = LAUNCHER_SH.replace(
|
||||
"verb=$1; shift", "verb=$1; shift\n" + BL_DIRECTAUDIO_SETUP + BL_GAME_CPUS_SETUP, 1)
|
||||
"verb=$1; shift", "verb=$1; shift\n" + BL_DIRECTAUDIO_SETUP + BL_GAME_CPUS_SETUP + BL_IDTECH3_SETUP, 1)
|
||||
|
||||
# A third-party Proton dropped into compatibilitytools.d - GE-Proton, proton-cachyos, anything of
|
||||
# that shape - is adopted rather than used as shipped, for the same two reasons Valve's ARM64 depot
|
||||
@@ -196,9 +222,10 @@ if [ -f "$fake" ]; then
|
||||
fi
|
||||
bl_directaudio "$here"
|
||||
bl_game_cpus
|
||||
bl_idtech3 "$@"
|
||||
# Proton takes its base directory from dirname(sys.argv[0]) without resolving it, so it has to be
|
||||
# started from its own tree.
|
||||
exec ${BL_TASKSET:-} "$here/%s" "$verb" "$@"
|
||||
exec ${BL_TASKSET:-} "$here/%s" "$verb" "$@" "${BL_EXTRA_ARGS[@]}"
|
||||
"""
|
||||
EXTRA_COMPAT_TOOL_VDF = """"compatibilitytools"
|
||||
{
|
||||
@@ -357,7 +384,7 @@ def adopt_extras(tools):
|
||||
# ${major%%.*} of its own, and putting it through the format would eat both.
|
||||
body = (EXTRA_WRAPPER_SH % entry_point).replace(
|
||||
'here=$(dirname "$0")',
|
||||
'here=$(dirname "$0")\n' + BL_DIRECTAUDIO_SETUP + BL_GAME_CPUS_SETUP, 1)
|
||||
'here=$(dirname "$0")\n' + BL_DIRECTAUDIO_SETUP + BL_GAME_CPUS_SETUP + BL_IDTECH3_SETUP, 1)
|
||||
with open(wrapper, "w", encoding="utf-8") as f:
|
||||
f.write(body)
|
||||
os.chmod(wrapper, 0o755)
|
||||
|
||||
Reference in new issue
Block a user