├── .gitmodules ├── LICENSE ├── README.md ├── extras ├── app-list-page.jpg ├── dial-page.jpg └── settings-page.jpg ├── gamescope-mode-change.py ├── gamescope-refresh.py ├── main.py ├── main_view.html ├── plugin.json ├── setup.py └── tile_view.html /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "third-party/python-xlib"] 2 | path = third-party/python-xlib 3 | url = https://github.com/python-xlib/python-xlib.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Stijn Jacobs 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [Steam Deck Plugin Loader](https://github.com/SteamDeckHomebrew/PluginLoader) to modify the nested resolution as seen by your games. 2 | 3 | ## Installation: 4 | 1. Ensure you have [Steam Deck Plugin Loader](https://github.com/SteamDeckHomebrew/PluginLoader) installed 5 | 2. git clone https://github.com/loki-47-6F-64/gamescope-mode-change.git 6 | 3. cd gamescope-mode-change 7 | 4. git submodule update --init 8 | 5. Copy gamescope-mode-change to `/path/to/homebrew/plugins/gamescope-mode-change` 9 | 10 | ## Main page: 11 | 12 | ![app-list-page](extras/app-list-page.jpg "Non-Steam Apps list") 13 | 14 | * This is a complete list of all Non-Steam games in your Steam Library 15 | * By clicking on an app, you'll open the settings page for that particular app 16 | * By clicking on `Save settings`, Gamescope-mode-change will remember your settings even if the toggle has been set to off. 17 | 18 | ## Settings page: 19 | ![settings-page](extras/settings-page.jpg "Settings") 20 | 21 | * By default, Gamescope-mode-change will set the resolution to the native resolution of your current monitor. 22 | * If Super Resolution is toggled off, the resolution will be set to Native or Width/Height, whichever is lower, otherwise it will force your configures Width/Height 23 | * If either Width or Height is set to Native, Super Resolution will effectively be set to off. 24 | * By clicking on either Width or Height, you'll be able to set the Width or Height respectively. 25 | 26 | 27 | ## Dial page: 28 | ![dial-page](extras/dial-page.jpg "Input Number") 29 | 30 | * Should be pretty straightforward 31 | 32 | 33 | Gamescope-mode-change works by modifying the launch options of your game, wich will make Steam run a script right before starting the game. 34 | ``` 35 | --fullscreen 36 | ``` 37 | becomes 38 | ``` 39 | /path/to/homebrew/plugins/gamescope-mode-change/gamescope-mode-change.py --id=1 --nestedWidth=${width} --nestedHeight=${height} xX-gamescope-Xx && %command% --fullscreen 40 | ``` 41 | 42 | and 43 | ``` 44 | ENVIRONMENT_VAR=VALUE %command% --fullscreen 45 | ``` 46 | becomes 47 | ``` 48 | /path/to/homebrew/plugins/gamescope-mode-change/gamescope-mode-change.py --id=1 --nestedWidth=${width} --nestedHeight=${height} xX-gamescope-Xx && ENVIRONMENT_VAR=VALUE %command% --fullscreen 49 | ``` 50 | 51 | 52 | If you wish to change launch options for your game while the script is active, be sure to do so after `xX-gamescope-Xx && `. <- please do not forget the extra space after `&&` 53 | 54 | 55 | Good luck :) -------------------------------------------------------------------------------- /extras/app-list-page.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loki-47-6F-64/gamescope-mode-change/4a9bd8b15f7fc577afbeaaf6305c170c0680561e/extras/app-list-page.jpg -------------------------------------------------------------------------------- /extras/dial-page.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loki-47-6F-64/gamescope-mode-change/4a9bd8b15f7fc577afbeaaf6305c170c0680561e/extras/dial-page.jpg -------------------------------------------------------------------------------- /extras/settings-page.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loki-47-6F-64/gamescope-mode-change/4a9bd8b15f7fc577afbeaaf6305c170c0680561e/extras/settings-page.jpg -------------------------------------------------------------------------------- /gamescope-mode-change.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sys 4 | import os 5 | 6 | # Change path so we find Xlib 7 | sys.path.append(os.path.join(os.path.dirname(__file__), 'third-party/python-xlib')) 8 | 9 | atomName = "GAMESCOPE_XWAYLAND_MODE_CONTROL" 10 | 11 | from optparse import OptionParser 12 | from Xlib import display, Xatom, X 13 | 14 | from Xlib.protocol import event 15 | from Xlib.xobject import drawable 16 | 17 | parser = OptionParser() 18 | parser.add_option("-d", "--display", dest="display", help="This option specifies the X server to which to connect", metavar="dpy", default=":1") 19 | parser.add_option("-x", "--nestedWidth", dest="width", help="The width as seen by the Game, if width is 0, then Gamescope's output resolution will be used", metavar="nested_width", default="1280") 20 | parser.add_option("-y", "--nestedHeight", dest="height", help="The height as seen by the Game, if height is 0, then Gamescope's output resolution will be used", metavar="nested_height", default="800") 21 | parser.add_option("-i", "--id", dest="serverID", help="The xwayland server to modify: [0 .. n) where n is the number of xwayland servers setup by Gamescope, e.g. --id=0 will modify the first xwayland server", metavar="serverID", default="0") 22 | parser.add_option("-f", "--force", action="store_true", dest="force", help="Force resolution even if the width and/or the height exceed the Gamescope output width/height respectively", default=False) 23 | 24 | (options, args) = parser.parse_args() 25 | 26 | width = int(options.width) 27 | height = int(options.height) 28 | serverID = int(options.serverID) 29 | 30 | MAX_INT = 2**30 + (2**30-1) 31 | superRes = 1 if options.force else 0 32 | 33 | if(height == 0 or width == 0): 34 | height = MAX_INT 35 | width = MAX_INT 36 | superRes = 0 37 | 38 | d = display.Display(options.display) 39 | 40 | atom = d.intern_atom(atomName, only_if_exists=1) 41 | if atom == X.NONE: 42 | sys.stderr.write('xwayland: no atom named "%s" on server "%s"\n'%(atomName, d.get_display_name())) 43 | sys.exit(1) 44 | 45 | print(d.screen().root.id) 46 | 47 | 48 | def _sendModeChanged(disp : display.Display, atom : int, win : drawable.Window): 49 | event_ = event.PropertyNotify( 50 | window = win.id, 51 | display = disp, 52 | atom = atom, 53 | time = 0, 54 | state = X.PropertyNewValue 55 | ) 56 | 57 | win.send_event(event_) 58 | disp.flush() 59 | 60 | def _changeMode(disp : display.Display, atom : int, win : drawable.Window, width : int, height : int, serverID = 1, superRes = 1): 61 | print(f"changeMode({win.id})") 62 | win.change_property(atom, Xatom.CARDINAL, 32, [serverID, width, height, superRes]) 63 | 64 | def x11(disp : display.Display, func, *args): 65 | ret = func(disp, *args) 66 | 67 | disp.flush() 68 | 69 | return ret 70 | 71 | def apply(f, *args1): 72 | def _apply(*args2): 73 | f(*args1, *args2) 74 | return _apply 75 | 76 | 77 | sendModeChanged = apply(x11, d, _sendModeChanged, atom) 78 | changeMode = apply(x11, d, _changeMode, atom) 79 | 80 | changeMode(d.screen().root, width, height, serverID, superRes) 81 | sendModeChanged(d.screen().root) -------------------------------------------------------------------------------- /gamescope-refresh.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import sys 4 | import os 5 | 6 | # Change path so we find Xlib 7 | sys.path.append(os.path.join(os.path.dirname(__file__), 'third-party/python-xlib')) 8 | 9 | from optparse import OptionParser 10 | from Xlib import display, Xatom, X 11 | 12 | from Xlib.protocol import event 13 | from Xlib.xobject import drawable 14 | 15 | parser = OptionParser() 16 | parser.add_option("-d", "--display", dest="display", help="This option specifies the X server to which to connect", metavar="dpy", default=":1") 17 | parser.add_option("-r", "--refresh-rate", dest="refreshRate", help="The refresh rate in FPS", metavar="fps", default="0") 18 | parser.add_option("-x", "--dynamic", action="store_true", dest="dynamic", help="Adjust dynamic refresh rate rather than the FPS limit", default=False) 19 | 20 | (options, args) = parser.parse_args() 21 | 22 | atomName = "GAMESCOPE_DYNAMIC_REFRESH" if options.dynamic else "GAMESCOPE_FPS_LIMIT" 23 | refreshRate = int(options.refreshRate) 24 | 25 | d = display.Display(options.display) 26 | 27 | atom = d.intern_atom(atomName, only_if_exists=1) 28 | if atom == X.NONE: 29 | sys.stderr.write('xwayland: no atom named "%s" on server "%s"\n'%(atomName, d.get_display_name())) 30 | sys.exit(1) 31 | 32 | 33 | def _sendModeChanged(disp : display.Display, atom : int, win : drawable.Window): 34 | event_ = event.PropertyNotify( 35 | window = win.id, 36 | display = disp, 37 | atom = atom, 38 | time = 0, 39 | state = X.PropertyNewValue 40 | ) 41 | 42 | win.send_event(event_) 43 | disp.flush() 44 | 45 | def _changeMode(disp : display.Display, atom : int, win : drawable.Window, fps : int): 46 | win.change_property(atom, Xatom.CARDINAL, 32, [fps]) 47 | 48 | def x11(disp : display.Display, func, *args): 49 | ret = func(disp, *args) 50 | 51 | disp.flush() 52 | 53 | return ret 54 | 55 | def apply(f, *args1): 56 | def _apply(*args2): 57 | f(*args1, *args2) 58 | return _apply 59 | 60 | 61 | sendModeChanged = apply(x11, d, _sendModeChanged, atom) 62 | changeMode = apply(x11, d, _changeMode, atom) 63 | 64 | changeMode(d.screen().root, refreshRate) 65 | sendModeChanged(d.screen().root) -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pathlib 4 | import logging 5 | import sys 6 | 7 | import json 8 | 9 | # Get the home directory based on the current working directory 10 | def homedir() -> pathlib.Path: 11 | cwd = pathlib.Path(os.getcwd()).parts 12 | if(len(cwd) >= 3): 13 | return pathlib.Path(f"{cwd[0]}{'/'.join(cwd[1:3])}") 14 | else: 15 | return pathlib.Path.home() 16 | 17 | VERSION = "0.1.0" 18 | HOME_DIR = str(homedir()) 19 | PLUGIN_DIR = pathlib.Path(os.getcwd()).parent.resolve() / "plugins" 20 | DEFAULT_SETTINGS_LOCATION = f"{HOME_DIR}/.config/GamescopeModeChange/settings.json" 21 | LOG_LOCATION = "/tmp/GamescopeModeChange.log" 22 | 23 | logging.basicConfig( 24 | filename = LOG_LOCATION, 25 | format = '%(asctime)s %(levelname)s %(message)s', 26 | filemode = 'w', 27 | force = True) 28 | 29 | logger = logging.getLogger() 30 | logger.setLevel(logging.DEBUG) 31 | logging.info(f"PluginLoader GamescopeModeChange v{VERSION} https://github.com/Loki-47-6F-64/gamescope-mode-change") 32 | logging.debug(f"Current Working Directory: {os.getcwd()}") 33 | logging.debug(f"Plugin Dir: {PLUGIN_DIR}") 34 | logging.debug(f"Home Directory:{HOME_DIR}") 35 | 36 | sys.path.append(str(PLUGIN_DIR / "gamescope-mode-change")) 37 | logging.info(sys.path) 38 | 39 | sys.path = sys.path[:-1] 40 | 41 | class Plugin: 42 | SETTINGS_PATH = pathlib.Path(DEFAULT_SETTINGS_LOCATION) 43 | 44 | async def get_version(self) -> str: 45 | return VERSION 46 | 47 | async def mode_change_script_path(self) -> str: 48 | logging.debug("mode_change_script_path") 49 | return str(PLUGIN_DIR / "gamescope-mode-change/gamescope-mode-change.py") 50 | 51 | async def read_settings(self) -> str: 52 | # If the config file doesn't exist yet, send an empty list back 53 | if(not Plugin.SETTINGS_PATH.exists()): 54 | return "[]" 55 | 56 | with open(DEFAULT_SETTINGS_LOCATION, mode="r") as f: 57 | return f.read() 58 | 59 | async def write_settings(self, contents : str): 60 | if(not Plugin.SETTINGS_PATH.exists()): 61 | Plugin.SETTINGS_PATH.parent.resolve().mkdir(parents=True, exist_ok=True) 62 | 63 | t = json.loads(contents) 64 | 65 | with open(DEFAULT_SETTINGS_LOCATION, "w") as f: 66 | f.write(json.dumps(t, indent=4)) -------------------------------------------------------------------------------- /main_view.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 119 | 120 | 121 | 122 | 123 | 126 |
127 |
128 |
129 | Save settings 130 |
131 |
132 | Go back 133 |
134 |
135 |
136 | 137 | 650 | 651 | 652 | -------------------------------------------------------------------------------- /plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Gamescope-mode-change", 3 | "author": "Loki-47-6F-64", 4 | "main_view_html": "main_view.html", 5 | "tile_view_html": "", 6 | "flags": ["refresh"] 7 | } 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | with open("README.md", "r") as fh: 3 | long_description = fh.read() 4 | 5 | setuptools.setup( 6 | name="gamescope-mode-changer", 7 | version="0.1.0", 8 | author="Loik-47-6F-64", 9 | author_email="", 10 | description="Modify nested resolution for Gamescope", 11 | long_description=long_description, 12 | long_description_content_type="text/markdown", 13 | url="", 14 | packages=setuptools.find_packages(), 15 | classifiers=[ 16 | "Programming Language :: Python :: 3", 17 | "License :: OSI Approved :: MIT License", 18 | "Operating System :: OS Independent", 19 | ], 20 | python_requires='>=3.6', 21 | ) 22 | -------------------------------------------------------------------------------- /tile_view.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loki-47-6F-64/gamescope-mode-change/4a9bd8b15f7fc577afbeaaf6305c170c0680561e/tile_view.html --------------------------------------------------------------------------------