Compare commits

...
17 changed files with 205 additions and 1 deletions

No files matched your search

@@ -121,6 +121,7 @@ class MainActivity : ComponentActivity() {
private var steamDeckMode by mutableStateOf(false)
private var steamRepairQueued by mutableStateOf(false)
private var mangoapp by mutableStateOf(true)
private var dualXwayland by mutableStateOf(true)
private var steamController by mutableStateOf(SessionPrefs.CONTROLLER_DECK)
private var showProtons by mutableStateOf(false)
// Components page: FEX / DXVK / VKD3D-Proton per Proton (ComponentsManager).
@@ -1182,6 +1183,7 @@ class MainActivity : ComponentActivity() {
steamDeckMode = mode == SessionService.MODE_STEAM && steamDeckMode,
steamRepairQueued = steamRepairQueued,
mangoapp = mangoapp,
dualXwayland = dualXwayland,
steamController = if (mode == SessionService.MODE_STEAM) steamController else null,
runSteamAtStartup = mode == SessionService.MODE_STEAM && runSteamAtStartup,
wifiDiscovery = if (mode == SessionService.MODE_STEAM) wifiDiscovery else null,
@@ -1265,6 +1267,7 @@ class MainActivity : ComponentActivity() {
steamChannel = SessionPrefs.steamChannel(this)
},
onMangoapp = { on -> SessionPrefs.setMangoapp(this, on); mangoapp = on },
onDualXwayland = { on -> SessionPrefs.setDualXwayland(this, on); dualXwayland = on },
onSteamController = { id -> SessionPrefs.setSteamController(this, id); steamController = id },
onRunSteamAtStartup = { on ->
SessionPrefs.setRunSteamAtStartup(this, on)
@@ -1380,6 +1383,7 @@ class MainActivity : ComponentActivity() {
steamDeckMode = SessionPrefs.steamDeckMode(this)
steamRepairQueued = SteamRepair.queued(this)
mangoapp = SessionPrefs.mangoapp(this)
dualXwayland = SessionPrefs.dualXwayland(this)
steamController = SessionPrefs.steamController(this)
runSteamAtStartup = SessionPrefs.runSteamAtStartup(this)
addedGamesDirs = SessionPrefs.addedGamesDirs(this)
@@ -154,6 +154,7 @@ object DeviceReport {
k("No GL error checks", SessionPrefs.noGlError(context))
k("Steam Deck mode", SessionPrefs.steamDeckMode(context))
k("Steam controller", SessionPrefs.steamController(context))
k("Two Xwayland servers", SessionPrefs.dualXwayland(context))
k("Upscaler", (SessionPrefs.upscalerChoices.firstOrNull { it.first == SessionPrefs.upscaler(context) }?.second ?: "Off") +
" (sharpness ${SessionPrefs.upscaleSharpness(context)}%)")
k("FEX preset", SessionPrefs.fexPreset(context).ifEmpty { "FEX defaults" })
@@ -41,6 +41,7 @@ object SessionFiles {
"libblfastpath.so" to "usr/local/lib/libblfastpath.so",
"libssbs.so" to "usr/local/lib/libssbs.so",
"usr/local/bin/droiddeck-session" to "usr/local/bin/droiddeck-session",
"usr/local/bin/droiddeck-xwindows" to "usr/local/bin/droiddeck-xwindows",
"usr/local/bin/steam-compatibility" to "usr/local/bin/steam-compatibility",
"usr/local/bin/droiddeck-clipboard" to "usr/local/bin/droiddeck-clipboard",
"usr/local/bin/droiddeck-steam-install" to "usr/local/bin/droiddeck-steam-install",
@@ -418,6 +418,14 @@ object SessionPrefs {
fun mangoapp(context: Context): Boolean = prefs(context).getBoolean("mangoapp", true)
fun setMangoapp(context: Context, on: Boolean) { prefs(context).edit().putBoolean("mangoapp", on).apply() }
/**
* Steam on two Xwayland servers, as SteamOS runs it: the client on one, its games on the other
* (droiddeck-session, BL_XWAYLAND_COUNT). Off puts both back on one server, for a game whose
* window opens behind the client and never takes focus. On by default.
*/
fun dualXwayland(context: Context): Boolean = prefs(context).getBoolean("dualXwayland", true)
fun setDualXwayland(context: Context, on: Boolean) { prefs(context).edit().putBoolean("dualXwayland", on).apply() }
/**
* Zink's lazy descriptor mode (ZINK_DESCRIPTORS=lazy) with its compact set layout
* (ZINK_DEBUG=compact) for the client's GL-on-Vulkan UI, as WinNative runs it. On by default.
@@ -614,6 +614,7 @@ class SessionService : Service() {
guest.add("GLIBC_TUNABLES=glibc.pthread.rseq=0:glibc.malloc.top_pad=16777216")
if (SessionState.mode == MODE_STEAM) guest.add("BL_STEAMDECK=" + (if (SessionPrefs.steamDeckMode(this)) "1" else "0"))
if (SessionState.mode == MODE_STEAM) guest.add("BL_MANGOAPP=" + (if (SessionPrefs.mangoapp(this)) "1" else "0"))
if (SessionState.mode == MODE_STEAM) guest.add("BL_XWAYLAND_COUNT=" + (if (SessionPrefs.dualXwayland(this)) "2" else "1"))
if (steamHere) guest.add("BL_STEAM_CHANNEL=" + SessionPrefs.steamChannel(this))
if (SessionState.mode == MODE_STEAM) {
guest.add("BL_GAMESCOPE_STRETCH_16X9=" + (if (SessionPrefs.stretch16x9(this)) "1" else "0"))
@@ -94,6 +94,8 @@ class ModeSettings(
val steamRepairQueued: Boolean = false,
/** Steam, Deck mode: the QAM's performance overlay (mangoapp) is started. */
val mangoapp: Boolean = true,
/** Steam only: the client and its games on separate Xwayland servers. */
val dualXwayland: Boolean = true,
/** Steam only: what the pad is to the client (SessionPrefs.CONTROLLER_*); null outside Steam. */
val steamController: String? = null,
/** Steam only: start a Steam session when DroidDeck opens. */
@@ -152,6 +154,7 @@ class ModeSettingsActions(
val onSteamRepair: () -> Unit = {},
val onSteamDeckMode: (Boolean) -> Unit = {},
val onMangoapp: (Boolean) -> Unit = {},
val onDualXwayland: (Boolean) -> Unit = {},
val onSteamController: (String) -> Unit = {},
val onRunSteamAtStartup: (Boolean) -> Unit = {},
val onWifiDiscovery: (Boolean) -> Unit = {},
@@ -390,6 +393,11 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
stringResource(R.string.mode_mangoapp_hint),
s.mangoapp, onChange = a.onMangoapp,
)
ToggleRow(
host, "dualxwayland", stringResource(R.string.mode_dual_xwayland),
stringResource(R.string.mode_dual_xwayland_hint),
s.dualXwayland, onChange = a.onDualXwayland,
)
// Deck mode fixes the branch (SessionPrefs.steamChannel); the choice is for Deck mode off.
if (s.steamDeckMode) SettingsRow(stringResource(R.string.mode_branch), stringResource(R.string.mode_branch_deck)) {}
else ChoiceRow(
+2
View File
@@ -289,6 +289,8 @@
<string name="mode_deck_mode_hint">La interfaz Deck de Steam y los controles de rendimiento de Quick Access. Se aplica en la próxima sesión.</string>
<string name="mode_mangoapp">Overlay de rendimiento</string>
<string name="mode_mangoapp_hint">Desactívalo si los juegos fallan o se quedan en negro en modo Deck. Se aplica en la próxima sesión.</string>
<string name="mode_dual_xwayland">Dos servidores Xwayland</string>
<string name="mode_dual_xwayland_hint">Ejecuta los juegos en su propio servidor X, como SteamOS. Desactívalo si un juego se abre detrás de Steam y nunca toma el foco. Se aplica en la próxima sesión.</string>
<string name="mode_branch">Rama del cliente</string>
<string name="mode_branch_deck">Beta pública de Steam Deck (requerida por el modo Deck).</string>
<string name="mode_branch_hint">Se aplica en la próxima sesión. Steam puede actualizarse una vez.</string>
+2
View File
@@ -263,6 +263,8 @@
<string name="mode_deck_mode_hint">Interface Steam Deck et contrôles de performance à accès rapide. Prendra effet à la prochaine session.</string>
<string name="mode_mangoapp">Superposition de performance</string>
<string name="mode_mangoapp_hint">Désactivez cette option si les jeux plantent ou affichent un écran noir en mode Deck. Prendra effet à la prochaine session.</string>
<string name="mode_dual_xwayland">Deux serveurs Xwayland</string>
<string name="mode_dual_xwayland_hint">Lance les jeux sur leur propre serveur X, comme SteamOS. Désactivez cette option si un jeu s’ouvre derrière Steam et ne prend jamais le focus. Prendra effet à la prochaine session.</string>
<string name="mode_branch">Branche du client</string>
<string name="mode_branch_deck">Bêta Steam Deck (requis pour le mode Deck).</string>
<string name="mode_branch_hint">Prendra effet à la prochaine session. Steam peut se mettre à jour une fois de lui-même.</string>
+2
View File
@@ -289,6 +289,8 @@
<string name="mode_deck_mode_hint">Steam DeckのUIとクイックアクセスパフォーマンスコントロール。次回のセッションで有効になります。</string>
<string name="mode_mangoapp">パフォーマンスオーバーレイ</string>
<string name="mode_mangoapp_hint">Deckモードでゲームがクラッシュしたり画面が真っ暗になる場合は、これをオフにしてください。次回のセッションで有効になります。</string>
<string name="mode_dual_xwayland">2つのXwaylandサーバー</string>
<string name="mode_dual_xwayland_hint">SteamOSと同様に、ゲームを専用のXサーバーで実行します。ゲームがSteamの背後で開き、フォーカスされない場合はオフにしてください。次回のセッションで有効になります。</string>
<string name="mode_branch">クライアントブランチ</string>
<string name="mode_branch_deck">Steam Deckベータ版(Deckモードに必要)。</string>
<string name="mode_branch_hint">次回のセッションで有効になります。Steamが自動的に一度アップデートされる場合があります。</string>
+2
View File
@@ -289,6 +289,8 @@
<string name="mode_deck_mode_hint">Steam Deck UI 및 빠른 액세스 성능 제어. 다음 세션에 적용됩니다.</string>
<string name="mode_mangoapp">성능 오버레이</string>
<string name="mode_mangoapp_hint">Deck 모드에서 게임이 충돌하거나 화면이 검게 변하면 이 옵션을 끄세요. 다음 세션에 적용됩니다.</string>
<string name="mode_dual_xwayland">Xwayland 서버 2개</string>
<string name="mode_dual_xwayland_hint">SteamOS처럼 게임을 별도의 X 서버에서 실행합니다. 게임이 Steam 뒤에서 열리고 포커스를 받지 못하면 이 옵션을 끄세요. 다음 세션에 적용됩니다.</string>
<string name="mode_branch">클라이언트 브랜치</string>
<string name="mode_branch_deck">Steam Deck 베타(Deck 모드에 필요).</string>
<string name="mode_branch_hint">다음 세션에 적용됩니다. Steam이 자체적으로 한 번 업데이트될 수 있습니다.</string>
+2
View File
@@ -263,6 +263,8 @@
<string name="mode_deck_mode_hint">Интерфейс Steam Deck и управление производительностью через быстрый доступ. Вступит в силу в следующей сессии.</string>
<string name="mode_mangoapp">Оверлей производительности</string>
<string name="mode_mangoapp_hint">Отключите, если игры падают или появляется черный экран в режиме Deck. Вступит в силу в следующей сессии.</string>
<string name="mode_dual_xwayland">Два сервера Xwayland</string>
<string name="mode_dual_xwayland_hint">Запускает игры на отдельном X-сервере, как в SteamOS. Отключите, если игра открывается позади Steam и не получает фокус. Вступит в силу в следующей сессии.</string>
<string name="mode_branch">Ветка клиента</string>
<string name="mode_branch_deck">Бета-версия Steam Deck (требуется для режима Deck).</string>
<string name="mode_branch_hint">Вступит в силу в следующей сессии. Steam может обновиться самостоятельно один раз.</string>
@@ -289,6 +289,8 @@
<string name="mode_deck_mode_hint">Steam 的 Deck 界面和快捷访问性能控制。将在下次会话生效。</string>
<string name="mode_mangoapp">性能覆盖层</string>
<string name="mode_mangoapp_hint">若游戏在 Deck 模式下崩溃或黑屏,请关闭性能覆盖层。将在下次会话生效。</string>
<string name="mode_dual_xwayland">双 Xwayland 服务器</string>
<string name="mode_dual_xwayland_hint">像 SteamOS 一样在独立的 X 服务器上运行游戏。若游戏在 Steam 后面打开且始终无法获得焦点,请关闭此项。将在下次会话生效。</string>
<string name="mode_branch">客户端分支</string>
<string name="mode_branch_deck">Steam Deck 公开测试版(Deck 模式所必需)。</string>
<string name="mode_branch_hint">将在下次会话生效。Steam 可能会自行更新一次。</string>
@@ -289,6 +289,8 @@
<string name="mode_deck_mode_hint">Steam 的 Deck 介面及快速存取效能控制。下次會話生效。</string>
<string name="mode_mangoapp">效能疊加層</string>
<string name="mode_mangoapp_hint">如果在 Deck 模式下遊戲當機或黑畫面,請關閉此項。下次會話生效。</string>
<string name="mode_dual_xwayland">雙 Xwayland 伺服器</string>
<string name="mode_dual_xwayland_hint">像 SteamOS 一樣在獨立的 X 伺服器上執行遊戲。如果遊戲在 Steam 後面開啟且一直無法取得焦點,請關閉此項。下次會話生效。</string>
<string name="mode_branch">用戶端分支</string>
<string name="mode_branch_deck">Steam Deck 公開測試版(Deck 模式需要)。</string>
<string name="mode_branch_hint">下次會話生效。Steam 可能會自行更新一次。</string>
@@ -289,6 +289,8 @@
<string name="mode_deck_mode_hint">Steam Deck 介面和快速存取效能控制。下次工作階段生效。</string>
<string name="mode_mangoapp">效能重疊層</string>
<string name="mode_mangoapp_hint">如果 Deck 模式下遊戲崩潰或黑畫面,請關閉此項。下次工作階段生效。</string>
<string name="mode_dual_xwayland">雙 Xwayland 伺服器</string>
<string name="mode_dual_xwayland_hint">像 SteamOS 一樣在獨立的 X 伺服器上執行遊戲。如果遊戲在 Steam 後面開啟且一直無法取得焦點,請關閉此項。下次工作階段生效。</string>
<string name="mode_branch">用戶端分支</string>
<string name="mode_branch_deck">Steam Deck 公開測試版(Deck 模式所需)。</string>
<string name="mode_branch_hint">下次工作階段生效。Steam 可能會自行更新一次。</string>
+2
View File
@@ -289,6 +289,8 @@
<string name="mode_deck_mode_hint">Steam\'s Deck interface and Quick Access performance controls. Applies next session.</string>
<string name="mode_mangoapp">Performance overlay</string>
<string name="mode_mangoapp_hint">Turn off if games crash or go black in Deck mode. Applies next session.</string>
<string name="mode_dual_xwayland">Two Xwayland servers</string>
<string name="mode_dual_xwayland_hint">Runs games on their own X server, as SteamOS does. Turn off if a game opens behind Steam and never takes focus. Applies next session.</string>
<string name="mode_branch">Client branch</string>
<string name="mode_branch_deck">Steam Deck public beta (required by Deck mode).</string>
<string name="mode_branch_hint">Applies next session. Steam may update itself once.</string>
@@ -182,7 +182,8 @@ if [ -z "${BL_INSIDE:-}" ]; then
# Resolution never reaches its display (docs/development/steam-display-scaling.md). The client's
# overlay is also on another server than the game, so opening it no longer takes the game's X
# keyboard focus, which made games like FlatOut shrink their window.
# BL_XWAYLAND_COUNT=1 in Download/droiddeck-env goes back to one.
# The app sends BL_XWAYLAND_COUNT from Steam settings (Two Xwayland servers); 1 goes back to one,
# and a line in Download/droiddeck-env still wins.
if [ "$mode" = steam ] && [ "${BL_XWAYLAND_COUNT:-2}" -gt 1 ]; then
args+=(--xwayland-count "${BL_XWAYLAND_COUNT:-2}")
export STEAM_MULTIPLE_XWAYLANDS=1
@@ -515,6 +516,26 @@ case $mode in
v=${v# }
echo "${v:-$2}"
}
# What gamescope had to choose from when focus moved, and again as late windows map:
# the apps and windows it counts as focusable (window, app id, pid; on the client's
# server, for every server), the one it picked, and the client's base-layer order; then
# each server's top-level windows with the app id they carry (STEAM_GAME) and their
# WM_STATE (droiddeck-xwindows). A game window that opens behind the client and never
# takes focus shows here as missing from the focusable list, or listed with no app id.
focus_snapshot() {
local d
echo "== focus state (after $1 at $2s):" \
$(xprop -root GAMESCOPE_FOCUSED_APP GAMESCOPE_FOCUSED_WINDOW GAMESCOPE_FOCUSABLE_APPS \
GAMESCOPE_FOCUSABLE_WINDOWS GAMESCOPECTRL_BASELAYER_APPID 2>/dev/null | sed 's/(CARDINAL)//' | tr '\n' ' ')
for d in $(bl_displays); do droiddeck-xwindows "$d" 2>/dev/null; done
}
focus_snapshots() {
local at=0 t
for t in 0 5 15 30; do
nap $((t - at)); at=$t
focus_snapshot "$1" "$t"
done
}
tmode= app=
xprop -spy -root STEAM_TOUCH_CLICK_MODE GAMESCOPE_FOCUSED_APP 2>/dev/null | while read -r line; do
case $line in
@@ -529,6 +550,7 @@ case $mode in
[ "$v" = "$app" ] && continue
app=$v
echo "== focused app: $app (touch mode ${tmode:-unset}) at $(date +%T)"
focus_snapshots "$app" &
;;
esac
done
+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/python3
# The top-level windows of one Xwayland server, one line each, for the session log: what gamescope
# had to pick focus from. Each line has the window, whether it is mapped, override-redirect, its
# geometry, the app id it carries (STEAM_GAME, which gamescope focuses by), its WM_STATE, pid and
# class. Then whatever is set of what else gamescope weighs before it counts a mapped window as
# focusable: an InputOnly class, _NET_WM_WINDOW_OPACITY (0 is never focusable), STEAM_OVERLAY,
# GAMESCOPE_EXTERNAL_OVERLAY, STEAM_INPUT_FOCUS and _NET_WM_STATE (skip taskbar and pager without
# fullscreen is left out of the focusable list the client reads). Windows that are neither mapped
# nor tagged with an app id are left out - wine keeps many hidden ones. The rootfs has xprop but no xwininfo, so this speaks the X protocol itself.
#
# droiddeck-xwindows :1
import os
import socket
import struct
import sys
MAP_STATE = ("unmapped", "unviewable", "viewable")
WM_STATES = {0: "Withdrawn", 1: "Normal", 3: "Iconic"}
class X:
def __init__(self, display):
num = display.lstrip(":").split(".")[0]
self.s = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.s.settimeout(2)
self.s.connect("/tmp/.X11-unix/X" + num)
self.s.sendall(struct.pack("<BxHHHHxx", 0x6C, 11, 0, 0, 0))
head = self.read(8)
if head[0] != 1:
raise OSError("X connection refused")
setup = self.read(struct.unpack_from("<H", head, 6)[0] * 4)
vendor_len = struct.unpack_from("<H", setup, 16)[0]
formats = setup[21]
self.root = struct.unpack_from("<I", setup, 32 + (vendor_len + 3) // 4 * 4 + formats * 8)[0]
def read(self, n):
buf = b""
while len(buf) < n:
chunk = self.s.recv(n - len(buf))
if not chunk:
raise OSError("X connection closed")
buf += chunk
return buf
def request(self, data):
"""Send one request and return its reply, or None for an X error (a window that went)."""
self.s.sendall(data)
while True:
head = self.read(32)
if head[0] == 0:
return None
if head[0] == 1:
return head + self.read(struct.unpack_from("<I", head, 4)[0] * 4)
# an event; none are selected, but skip anything that arrives
def atom(self, name):
n = name.encode()
pad = (4 - len(n) % 4) % 4
r = self.request(struct.pack("<BBHHxx", 16, 1, 2 + (len(n) + pad) // 4, len(n)) + n + b"\0" * pad)
return struct.unpack_from("<I", r, 8)[0] if r else 0
def children(self, win):
r = self.request(struct.pack("<BxHI", 15, 2, win))
if not r:
return []
n = struct.unpack_from("<H", r, 16)[0]
return list(struct.unpack_from("<%dI" % n, r, 32))
def attributes(self, win):
r = self.request(struct.pack("<BxHI", 3, 2, win))
return (r[26], r[27], struct.unpack_from("<H", r, 12)[0]) if r else None
def atom_name(self, atom):
r = self.request(struct.pack("<BxHI", 17, 2, atom))
if not r:
return str(atom)
return r[32:32 + struct.unpack_from("<H", r, 8)[0]].decode(errors="replace")
def geometry(self, win):
r = self.request(struct.pack("<BxHI", 14, 2, win))
return struct.unpack_from("<hhHH", r, 12) if r else None
def prop(self, win, atom):
if not atom:
return None
r = self.request(struct.pack("<BBHIIIII", 20, 0, 6, win, atom, 0, 0, 64))
if not r or r[1] == 0:
return None
fmt, n = r[1], struct.unpack_from("<I", r, 16)[0]
data = r[32:32 + n * fmt // 8]
if fmt == 32:
return list(struct.unpack("<%dI" % n, data))
return data
def main():
display = sys.argv[1] if len(sys.argv) > 1 else os.environ.get("DISPLAY", ":0")
try:
x = X(display)
except OSError as e:
print("== %s: %s" % (display, e))
return
atoms = {name: x.atom(name) for name in (
"STEAM_GAME", "WM_STATE", "_NET_WM_PID", "WM_CLASS", "_NET_WM_WINDOW_OPACITY", "STEAM_OVERLAY",
"GAMESCOPE_EXTERNAL_OVERLAY", "STEAM_INPUT_FOCUS", "_NET_WM_STATE")}
for win in x.children(x.root):
attrs = x.attributes(win)
if not attrs:
continue
game = x.prop(win, atoms["STEAM_GAME"])
if attrs[0] != 2 and not game:
continue
geo = x.geometry(win)
state = x.prop(win, atoms["WM_STATE"])
pid = x.prop(win, atoms["_NET_WM_PID"])
cls = x.prop(win, atoms["WM_CLASS"])
extra = []
if attrs[2] == 2:
extra.append("InputOnly")
for label, name in (("opacity", "_NET_WM_WINDOW_OPACITY"), ("overlay", "STEAM_OVERLAY"),
("external-overlay", "GAMESCOPE_EXTERNAL_OVERLAY"),
("input-focus", "STEAM_INPUT_FOCUS")):
value = x.prop(win, atoms[name])
if value:
extra.append("%s 0x%x" % (label, value[0]) if label == "opacity" else "%s %d" % (label, value[0]))
net_state = x.prop(win, atoms["_NET_WM_STATE"])
if net_state:
extra.append("net-state " + ",".join(
x.atom_name(a).replace("_NET_WM_STATE_", "") for a in net_state))
print("== %s window 0x%x: %s%s %s app %s state %s pid %s class %s" % (
display, win, MAP_STATE[attrs[0]] if attrs[0] < 3 else attrs[0],
" override-redirect" if attrs[1] else "",
"%dx%d+%d+%d" % (geo[2], geo[3], geo[0], geo[1]) if geo else "?",
game[0] if game else "-",
WM_STATES.get(state[0], state[0]) if state else "-",
pid[0] if pid else "-",
cls.split(b"\0")[-2].decode(errors="replace") if cls and b"\0" in cls else "-",
) + "".join(" " + e for e in extra))
main()