From 2fd6ac7ea1508860fe7659b041f05da88a3487c3 Mon Sep 17 00:00:00 2001
From: Timothee 'TTimo' Besset <ttimo@valvesoftware.com>
Date: Wed, 8 Nov 2023 10:26:52 -0600
Subject: [PATCH] add support for taking screenshots using the xprop interface
 to gamescope, expose additional screenshot mode settings. enabled based on
 gamescope version detection, force with --screenshot-force-xprop

---
 client/devkit-utils/steamos-get-status | 19 ++++++++++++
 client/devkit_client/__init__.py       |  7 +++--
 client/devkit_client/gui2/gui2.py      | 42 +++++++++++++++++++++++---
 3 files changed, 62 insertions(+), 6 deletions(-)

diff --git a/client/devkit-utils/steamos-get-status b/client/devkit-utils/steamos-get-status
index 0169a0a..b2d00aa 100755
--- a/client/devkit-utils/steamos-get-status
+++ b/client/devkit-utils/steamos-get-status
@@ -347,6 +347,23 @@ if __name__ == '__main__':
 
     _user_password_is_set = user_password_is_set()
 
+    # optimistic - if the test fails assume this has evolved beyond our ability to check and it's fine
+    gamescope_supports_xprop = True
+    try:
+        output = subprocess.check_output('pacman -Q gamescope',
+                                         shell=True,
+                                         universal_newlines=True)
+        logger.debug(output)
+        (_, major, minor, _) = re.split('gamescope (.*?)\.(.*?)\..*', output)
+        major = int(major)
+        minor = int(minor)
+        if major < 3:
+            gamescope_supports_xprop = False
+        elif major == 3 and minor <= 12:
+            gamescope_supports_xprop = False
+    except Exception as e:
+        logger.warning(f'could not check gamescope version for xprop: {e!r}')
+
     if conf.json:
         ret = {
             'steamdeck': steamdeck,
@@ -368,6 +385,7 @@ if __name__ == '__main__':
             'foxnet_hostname': foxnet_hostname,
             'user_password_is_set': _user_password_is_set,
             'steam_launch_flags': _steam_launch_flags,
+            'gamescope_supports_xprop': gamescope_supports_xprop,
         }
         json.dump(ret, sys.stdout, sort_keys=True, indent=4)
     else:
@@ -386,4 +404,5 @@ if __name__ == '__main__':
         logger.info(f"Foxnet hostname        : {foxnet_hostname}")
         logger.info(f"User password is set   : {'yes' if _user_password_is_set else 'no'}")
         logger.info(f"Steam launch flags     : {_steam_launch_flags}")
+        logger.info(f"Screenshot with xprop  : {gamescope_supports_xprop}")
 
diff --git a/client/devkit_client/__init__.py b/client/devkit_client/__init__.py
index 0b163a2..bb689d6 100644
--- a/client/devkit_client/__init__.py
+++ b/client/devkit_client/__init__.py
@@ -1568,8 +1568,11 @@ def screenshot(args):
     # - so they don't accumulate in tmpfs
     # - so we can wait and confirm a new screenshot was delivered
     _simple_ssh(ssh, 'rm /tmp/gamescope_*.png', silent=True)
-    _simple_ssh(ssh, 'kill -USR2 `pidof gamescope`', silent=True, check_status=True)
-    attempts = 50
+    if args.xprop is None:
+        _simple_ssh(ssh, 'kill -USR2 `pidof gamescope`', silent=True, check_status=True)
+    else:
+        _simple_ssh(ssh, f'DISPLAY=:0 xprop -root -f GAMESCOPECTRL_DEBUG_REQUEST_SCREENSHOT 32c -set GAMESCOPECTRL_DEBUG_REQUEST_SCREENSHOT {args.xprop}', silent=True, check_status=True)
+    attempts = 100
     while attempts > 0:
         time.sleep(.1)
         attempts -= 1
diff --git a/client/devkit_client/gui2/gui2.py b/client/devkit_client/gui2/gui2.py
index bcab326..e837e49 100644
--- a/client/devkit_client/gui2/gui2.py
+++ b/client/devkit_client/gui2/gui2.py
@@ -351,13 +351,14 @@ class DevkitCommands:
     def _screenshot(self, *args):
 
         class ScreenshotArgs:
-            def __init__(self, devkit, folder, filename, do_timestamp):
+            def __init__(self, devkit, folder, filename, do_timestamp, xprop):
                 self.machine, self.machine_name_type = devkit.machine_command_args
                 self.http_port = devkit.http_port
                 self.login = None
                 self.folder = folder
                 self.filename = filename
                 self.do_timestamp = do_timestamp
+                self.xprop = xprop
         screenshot_args = ScreenshotArgs(*args)
         devkit_client.screenshot(screenshot_args)
 
@@ -719,6 +720,10 @@ class Devkit:
         elif 'PROTON_LOG' in status_flags:
             del status_flags['PROTON_LOG']
 
+    @property
+    def gamescope_supports_xprop(self):
+        return self.steamos_status.get('gamescope_supports_xprop', False)
+
     def has_mdns_service(self):
         return self.service_name is not None
 
@@ -2377,17 +2382,24 @@ class Screenshot(SubTool):
     FOLDER_KEY = 'Screenshot.folder'
     FILENAME_KEY = 'Screenshot.filename'
     TIMESTAMP_KEY = 'Screenshot.timestamp'
+    MODE_KEY = 'Screenshot.mode'
 
-    def setup(self):
+    def setup(self, force_xprop):
+        self.force_xprop = force_xprop
         if not self.FOLDER_KEY in self.settings:
             self.settings[self.FOLDER_KEY] = str(pathlib.Path(os.path.expanduser('~/Pictures')))
         if not self.FILENAME_KEY in self.settings:
             self.settings[self.FILENAME_KEY] = ''
         if not self.TIMESTAMP_KEY in self.settings:
             self.settings[self.TIMESTAMP_KEY] = True
+        if not self.MODE_KEY in self.settings:
+            self.settings[self.MODE_KEY] = 1
         super(Screenshot, self).setup()
 
     def devkits_window_draw(self, selected_devkit):
+        xprop = None
+        use_xprop = selected_devkit.gamescope_supports_xprop or self.force_xprop
+
         imgui.text('Folder:')
         imgui.same_line()
         imgui.set_next_item_width(48*CHARACTER_WIDTH)
@@ -2398,7 +2410,7 @@ class Screenshot(SubTool):
         imgui.same_line()
         imgui.text('Filename (optional):')
         imgui.same_line()
-        imgui.set_next_item_width(32*CHARACTER_WIDTH)
+        imgui.set_next_item_width(18*CHARACTER_WIDTH)
         changed, s = imgui.input_text('##screenshot_filename', self.settings[self.FILENAME_KEY], 128)
         if changed:
             self.settings[self.FILENAME_KEY] = s
@@ -2408,6 +2420,23 @@ class Screenshot(SubTool):
         if clicked:
             self.settings[self.TIMESTAMP_KEY] = v
 
+        if use_xprop:
+            imgui.same_line()
+            mode_choices = [
+                'baseplane only',
+                'all real layers',
+                'full composition',
+                'screen buffer'
+                ]
+            imgui.push_item_width(16*CHARACTER_WIDTH)
+            clicked, selected_index = imgui.combo(
+                '##ScreenshotMode', self.settings[self.MODE_KEY] - 1, mode_choices
+            )
+            imgui.pop_item_width()
+            if clicked:
+                self.settings[self.MODE_KEY] = selected_index + 1
+            xprop = self.settings[self.MODE_KEY]
+
         imgui.same_line()
         # gross - plz halp with layout
         imgui.set_cursor_pos_x(1100)
@@ -2417,6 +2446,7 @@ class Screenshot(SubTool):
                 self.settings[self.FOLDER_KEY],
                 self.settings[self.FILENAME_KEY],
                 self.settings[self.TIMESTAMP_KEY],
+                xprop
                 )
             self.modal_wait = ModalWait(
                 self.viewport,
@@ -3325,6 +3355,10 @@ def main():
             '--with-cmder', required=False, action='store',
             help='Configure run in terminal to use given exe with cmder syntax. Set to blank to clear.'
         )
+    parser.add_argument(
+        '--screenshot-force-xprop', required=False, action='store_true',
+        help='Force using xprop for screenshots, overriding the gamescope version check.'
+    )
 
     conf = parser.parse_args()
 
@@ -3372,7 +3406,7 @@ def main():
     toolbar.setup()
 
     screenshot = Screenshot(devkit_commands, viewport, toolbar, settings)
-    screenshot.setup()
+    screenshot.setup(conf.screenshot_force_xprop)
     perf_overlay = PerfOverlay(devkit_commands, viewport, toolbar, settings)
     perf_overlay.setup()
     gpu_trace = GPUTrace(devkit_commands, viewport, toolbar, settings)
-- 
GitLab