Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove gamescope session workaround #136

Merged
merged 8 commits into from
Jul 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/umu-python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ jobs:
- name: Install dependencies
run: |
python3 -m pip install --upgrade pip
pip install ruff
pip install python-xlib
- name: Lint umu_*.py files with Ruff
run: |
pip install ruff
Expand Down
7 changes: 0 additions & 7 deletions docs/umu.1.scd
Original file line number Diff line number Diff line change
Expand Up @@ -163,13 +163,6 @@ _PROTON_VERB_
_UMU_ZENITY_
Optional. Creates a *zenity*[5] popup window when downloading large files.

_UMU_NO_RUNTIME_
Optional and only applicable to Flatpak applications. Disables the usage of the *Steam Linux Runtime*[6] (SLR).

Set to _1_ to disable the entirety of the SLR or _pressure-vessel_ to only disable Pressure Vessel and the container runtime.

Defaults to _pressure-vessel_.

# SEE ALSO

_umu_(5), _winetricks_(1), _zenity_(1)
Expand Down
136 changes: 81 additions & 55 deletions umu/umu_run.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#!/usr/bin/env python3
import os
import os # noqa
import sys
import time
import threading
from Xlib import X, display, Xatom
from Xlib import X, display, Xatom # noqa
from _ctypes import CFuncPtr
from argparse import ArgumentParser, Namespace, RawTextHelpFormatter
from concurrent.futures import Future, ThreadPoolExecutor
Expand All @@ -14,7 +14,7 @@
from pwd import getpwuid
from re import match
from socket import AF_INET, SOCK_DGRAM, socket
from subprocess import PIPE, Popen, run
from subprocess import PIPE, Popen, run # noqa
from typing import Any

from umu_consts import (
Expand All @@ -34,7 +34,7 @@
get_libc,
is_installed_verb,
is_winetricks_verb,
whereis,
whereis, # noqa
)

AnyPath = os.PathLike | str
Expand Down Expand Up @@ -303,22 +303,7 @@ def set_env(
)

# Runtime
# if FLATPAK_PATH:
# env["UMU_NO_RUNTIME"] = os.environ.get("UMU_NO_RUNTIME") or ""

# FIXME: Currently, running games when using the Steam Runtime in a Flatpak
# environment will cause the game window to not display within the SteamOS
# gamescope session. Note, this is a workaround until the runtime is built
# or the issue is fixed upstream.
# See https://github.com/ValveSoftware/gamescope/issues/1341
# if (
# not os.environ.get("UMU_NO_RUNTIME")
# and FLATPAK_PATH
# and os.environ.get("XDG_CURRENT_DESKTOP") == "gamescope"
# ):
# log.debug("SteamOS gamescope session detected")
# log.debug("Disabling Pressure Vessel and container runtime")
# env["UMU_NO_RUNTIME"] = "pressure-vessel"
env["UMU_NO_RUNTIME"] = os.environ.get("UMU_NO_RUNTIME") or ""

return env

Expand Down Expand Up @@ -460,12 +445,12 @@ def get_window_client_ids() -> list[str]:
children = root.query_tree().children
if children and len(children) > 1:
for child in children:
log.debug(f"Window ID: {child.id}")
log.debug(f"Window Name: {child.get_wm_name()}")
log.debug(f"Window Class: {child.get_wm_class()}")
log.debug(f"Window Geometry: {child.get_geometry()}")
log.debug(f"Window Attributes: {child.get_attributes()}")
#if "steam_app" in str(child.get_wm_class()):
log.debug(f"Window ID: {child.id}") # noqa
log.debug(f"Window Name: {child.get_wm_name()}") # noqa
log.debug(f"Window Class: {child.get_wm_class()}") # noqa
log.debug(f"Window Geometry: {child.get_geometry()}") # noqa
log.debug(f"Window Attributes: {child.get_attributes()}") # noqa
# if "steam_app" in str(child.get_wm_class()):
window_ids.append(child.id)
return window_ids
time.sleep(wait_interval)
Expand All @@ -478,48 +463,71 @@ def get_window_client_ids() -> list[str]:
def set_steam_game_property( # noqa: D103
window_ids: list[str], steam_assigned_layer_id: int
) -> None:
d = display.Display(":1")
try:
d = display.Display(":1")
root = d.screen().root
root = d.screen().root # noqa

for window_id in window_ids:
log.debug("window_id: %s steam_layer: %s", window_id, steam_assigned_layer_id)
log.debug(
"window_id: %s steam_layer: %s",
window_id,
steam_assigned_layer_id,
)
try:
window = d.create_resource_object('window', int(window_id))
window.get_full_property(d.intern_atom('STEAM_GAME'), Xatom.CARDINAL)
window.change_property(d.intern_atom('STEAM_GAME'), Xatom.CARDINAL, 32, [int(steam_assigned_layer_id)])
log.debug("Successfully set STEAM_GAME property for window ID: %s", window_id)
except Exception as e:
log.error("Error setting STEAM_GAME property for window ID %s: %s", window_id, e)
window = d.create_resource_object("window", int(window_id))
window.get_full_property(
d.intern_atom("STEAM_GAME"), Xatom.CARDINAL
)
window.change_property(
d.intern_atom("STEAM_GAME"),
Xatom.CARDINAL,
32,
[int(steam_assigned_layer_id)],
)
log.debug(
"Successfully set STEAM_GAME property for window ID: %s",
window_id,
)
except Exception as e: # noqa
log.error(
"Error setting STEAM_GAME property for window ID %s: %s",
window_id,
e,
)
except Exception as e:
log.exception(e)
finally:
d.close()


def get_gamescope_baselayer_order() -> list[int] | None: # noqa: D103
d = display.Display(":0")
try:
d = display.Display(":0")
root = d.screen().root

# Intern the atom for GAMESCOPECTRL_BASELAYER_APPID
atom = d.intern_atom('GAMESCOPECTRL_BASELAYER_APPID')
atom = d.intern_atom("GAMESCOPECTRL_BASELAYER_APPID")

# Get the property value
prop = root.get_full_property(atom, Xatom.CARDINAL)

if prop:
# Extract and return the value
return prop.value
else:
log.debug("GAMESCOPECTRL_BASELAYER_APPID property not found")
return None
return prop.value # type: ignore
log.debug("GAMESCOPECTRL_BASELAYER_APPID property not found")
except Exception as e:
log.exception("Error getting GAMESCOPECTRL_BASELAYER_APPID property: %s", e)
return None
log.exception(
"Error getting GAMESCOPECTRL_BASELAYER_APPID property: %s", e
)
finally:
d.close()

def rearrange_gamescope_baselayer_order(sequence: list[int]) -> tuple[list[int], int]: # noqa: D103
return None


def rearrange_gamescope_baselayer_order( # noqa
sequence: list[int],
) -> tuple[list[int], int]:
# Ensure there are exactly 4 numbers
if len(sequence) != 4:
err = "Unexpected number of elements in sequence"
Expand All @@ -531,34 +539,46 @@ def rearrange_gamescope_baselayer_order(sequence: list[int]) -> tuple[list[int],
# Return the rearranged sequence and the second element
return rearranged, rearranged[1]

def set_gamescope_baselayer_order(rearranged: list[int]) -> None:

def set_gamescope_baselayer_order(rearranged: list[int]) -> None: # noqa
try:
d = display.Display(":0")
root = d.screen().root

# Intern the atom for GAMESCOPECTRL_BASELAYER_APPID
atom = d.intern_atom('GAMESCOPECTRL_BASELAYER_APPID')
atom = d.intern_atom("GAMESCOPECTRL_BASELAYER_APPID")

# Set the property value
root.change_property(atom, Xatom.CARDINAL, 32, rearranged)
log.debug("Successfully set GAMESCOPECTRL_BASELAYER_APPID property: %s", ", ".join(map(str, rearranged)))
log.debug(
"Successfully set GAMESCOPECTRL_BASELAYER_APPID property: %s",
", ".join(map(str, rearranged)),
)
except Exception as e:
log.exception("Error setting GAMESCOPECTRL_BASELAYER_APPID property: %s", e)
log.exception(
"Error setting GAMESCOPECTRL_BASELAYER_APPID property: %s", e
)
finally:
d.close()

def window_setup(gamescope_baselayer_sequence: list[int]) -> None:

def window_setup(gamescope_baselayer_sequence: list[int]) -> None: # noqa
if gamescope_baselayer_sequence:
# Rearrange the sequence
rearranged_sequence, steam_assigned_layer_id = rearrange_gamescope_baselayer_order(gamescope_baselayer_sequence)
rearranged_sequence, steam_assigned_layer_id = (
rearrange_gamescope_baselayer_order(gamescope_baselayer_sequence)
)
# Assign our window a STEAM_GAME id
game_window_ids = get_window_client_ids()
if game_window_ids:
set_steam_game_property(game_window_ids,steam_assigned_layer_id)
set_steam_game_property(game_window_ids, steam_assigned_layer_id)

set_gamescope_baselayer_order(rearranged_sequence)

def monitor_layers(gamescope_baselayer_sequence: list[int], window_client_list: list[str]) -> None:

def monitor_layers( # noqa
gamescope_baselayer_sequence: list[int], window_client_list: list[str]
) -> None:
while True:
# Check if the window sequence has changed:
current_window_list = get_window_client_ids()
Expand All @@ -572,6 +592,7 @@ def monitor_layers(gamescope_baselayer_sequence: list[int], window_client_list:

time.sleep(5) # Check every 5 seconds


def run_command(command: list[AnyPath]) -> int:
"""Run the executable using Proton within the Steam Runtime."""
prctl: CFuncPtr
Expand Down Expand Up @@ -615,10 +636,15 @@ def run_command(command: list[AnyPath]) -> int:
gamescope_baselayer_sequence = get_gamescope_baselayer_order()

# Dont do window fuckery if we're not inside gamescope
if gamescope_baselayer_sequence and not os.environ.get("EXE", "").endswith("winetricks"):
if gamescope_baselayer_sequence and not os.environ.get("EXE", "").endswith(
"winetricks"
):
window_client_list = get_window_client_ids
window_setup(gamescope_baselayer_sequence)
monitor_thread = threading.Thread(target=monitor_layers, args=(gamescope_baselayer_sequence,window_client_list))
monitor_thread = threading.Thread(
target=monitor_layers,
args=(gamescope_baselayer_sequence, window_client_list),
)
monitor_thread.daemon = True
monitor_thread.start()

Expand Down
26 changes: 17 additions & 9 deletions umu/umu_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,9 @@ def test_run_command(self):
umu_run,
"Popen",
) as mock_popen,
patch.object(
umu_run, "get_gamescope_baselayer_order", return_value=None
),
):
mock_proc = MagicMock()
mock_proc.wait.return_value = 0
Expand Down Expand Up @@ -230,23 +233,28 @@ def test_run_command_nolibc(self):
"/home/foo/.local/share/Steam/compatibilitytools.d/GE-Proton9-7/proton",
mock_exe,
]
mock_proc = CompletedProcess(mock_command, 0)
mock = MagicMock()

os.environ["EXE"] = mock_exe
with (
patch.object(umu_run, "run", return_value=mock_proc),
patch.object(umu_run, "Popen", return_value=mock) as proc,
patch.object(umu_run, "get_libc", return_value=""),
patch.object(
umu_run, "get_gamescope_baselayer_order", return_value=None
),
):
result = umu_run.run_command(mock_command)
self.assertEqual(
result,
0,
"Expected 0 status code when libc could not be found",
)
# TODO: Mock the call
umu_run.run_command(mock_command)
proc.assert_called_once()

def test_run_command_none(self):
"""Test run_command when passed an empty list or None."""
with self.assertRaises(ValueError):
with (
self.assertRaises(ValueError),
patch.object(
umu_run, "get_gamescope_baselayer_order", return_value=None
),
):
umu_run.run_command([])
umu_run.run_command(None)

Expand Down