From 25f490c8b895ae6f6cd71c416c4c04c62989ce3d Mon Sep 17 00:00:00 2001 From: m4ko Date: Tue, 1 Sep 2026 20:51:45 +0200 Subject: [PATCH] Hotfixed a issue with resolution in borderless mode. --- ptlinux/gamebox_runtime.py | 137 ++++++++++++++++++++++++++++++++++++- 1 file changed, 135 insertions(+), 2 deletions(-) diff --git a/ptlinux/gamebox_runtime.py b/ptlinux/gamebox_runtime.py index 2da2e16..8fb3b13 100644 --- a/ptlinux/gamebox_runtime.py +++ b/ptlinux/gamebox_runtime.py @@ -19,6 +19,8 @@ from .desktop_identity import ( WINDOW_TITLE = APPLICATION_NAME WINDOW_DISCOVERY_TIMEOUT_SECONDS = 60.0 WINDOW_READY_DELAY_SECONDS = 0.5 +WINDOW_RESTORE_DELAY_SECONDS = 0.15 +WINDOW_REMAP_DELAY_SECONDS = 0.05 _X_ERROR_HANDLER = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_void_p, ctypes.c_void_p) @@ -117,17 +119,24 @@ class _X11: title = self._window_title(window) if title is not None: windows.append((window, title)) - return _select_game_window(windows, title_fragment, excluded_window_ids) + selected = _select_game_window(windows, title_fragment, excluded_window_ids) + if selected is None: + return None + window, title = selected + return self._client_window(window), title def _root_windows(self) -> list[int]: root = self.library.XDefaultRootWindow(self.display) + return self._child_windows(root) + + def _child_windows(self, window: int) -> list[int]: root_return = ctypes.c_ulong() parent_return = ctypes.c_ulong() children = ctypes.POINTER(ctypes.c_ulong)() child_count = ctypes.c_uint() status = self.library.XQueryTree( self.display, - root, + window, ctypes.byref(root_return), ctypes.byref(parent_return), ctypes.byref(children), @@ -141,12 +150,86 @@ class _X11: if children: self.library.XFree(ctypes.cast(children, ctypes.c_void_p)) + def _client_window(self, window: int) -> int: + """Resolve a window-manager frame to the ICCCM client it contains.""" + + queue = [window] + visited: set[int] = set() + while queue and len(visited) < 64: + candidate = queue.pop(0) + if candidate in visited: + continue + visited.add(candidate) + if self._has_wm_state(candidate): + return candidate + queue.extend(self._child_windows(candidate)) + return window + + def _has_wm_state(self, window: int) -> bool: + wm_state = self.library.XInternAtom(self.display, b"WM_STATE", 1) + if wm_state == 0: + return False + actual_type = ctypes.c_ulong() + actual_format = ctypes.c_int() + item_count = ctypes.c_ulong() + bytes_after = ctypes.c_ulong() + value = ctypes.POINTER(ctypes.c_ubyte)() + status = self.library.XGetWindowProperty( + self.display, + window, + wm_state, + 0, + 0, + 0, + 0, + ctypes.byref(actual_type), + ctypes.byref(actual_format), + ctypes.byref(item_count), + ctypes.byref(bytes_after), + ctypes.byref(value), + ) + try: + return status == 0 and actual_type.value != 0 + finally: + if value: + self.library.XFree(ctypes.cast(value, ctypes.c_void_p)) + def map_window(self, window: int, *, borderless: bool) -> None: if borderless: self._remove_decorations(window) self.library.XMapRaised(self.display, window) self.library.XFlush(self.display) + def refresh_render_surface(self, window: int) -> bool: + """Recreate the minimize/restore event sequence after borderless styling. + + Some Wine/DXVK combinations retain the host desktop-sized render surface + after the top-level game window becomes borderless. A normal user-driven + minimize and restore makes Wine reconcile the surface with the real window. + Request that same standard X11 lifecycle once without changing geometry. + """ + + screen = self.library.XDefaultScreen(self.display) + if self.library.XIconifyWindow(self.display, window, screen) == 0: + return False + self.library.XSync(self.display, 0) + try: + time.sleep(WINDOW_RESTORE_DELAY_SECONDS) + finally: + # Mutter keeps an iconified client's own X window mapped beneath an + # unmapped frame, so XMapRaised alone cannot deiconify it. An explicit + # client unmap/remap generates a fresh MapRequest for the window manager. + # Always finish the remap if either short wait is interrupted, so the + # game cannot be stranded as a hidden window. + self.library.XUnmapWindow(self.display, window) + self.library.XSync(self.display, 0) + try: + time.sleep(WINDOW_REMAP_DELAY_SECONDS) + finally: + self.library.XMapRaised(self.display, window) + self.library.XSync(self.display, 0) + return True + def set_window_identity( self, window: int, @@ -270,6 +353,8 @@ class _X11: self.library.XSetErrorHandler.restype = ctypes.c_void_p self.library.XOpenDisplay.argtypes = [ctypes.c_char_p] self.library.XOpenDisplay.restype = ctypes.c_void_p + self.library.XDefaultScreen.argtypes = [ctypes.c_void_p] + self.library.XDefaultScreen.restype = ctypes.c_int self.library.XDefaultRootWindow.argtypes = [ctypes.c_void_p] self.library.XDefaultRootWindow.restype = ctypes.c_ulong self.library.XQueryTree.argtypes = [ @@ -304,6 +389,21 @@ class _X11: ctypes.c_int, ] self.library.XChangeProperty.restype = ctypes.c_int + self.library.XGetWindowProperty.argtypes = [ + ctypes.c_void_p, + ctypes.c_ulong, + ctypes.c_ulong, + ctypes.c_long, + ctypes.c_long, + ctypes.c_int, + ctypes.c_ulong, + ctypes.POINTER(ctypes.c_ulong), + ctypes.POINTER(ctypes.c_int), + ctypes.POINTER(ctypes.c_ulong), + ctypes.POINTER(ctypes.c_ulong), + ctypes.POINTER(ctypes.POINTER(ctypes.c_ubyte)), + ] + self.library.XGetWindowProperty.restype = ctypes.c_int self.library.XSetClassHint.argtypes = [ ctypes.c_void_p, ctypes.c_ulong, @@ -326,6 +426,14 @@ class _X11: self.library.XSetWMProtocols.restype = ctypes.c_int self.library.XMapRaised.argtypes = [ctypes.c_void_p, ctypes.c_ulong] self.library.XMapRaised.restype = ctypes.c_int + self.library.XUnmapWindow.argtypes = [ctypes.c_void_p, ctypes.c_ulong] + self.library.XUnmapWindow.restype = ctypes.c_int + self.library.XIconifyWindow.argtypes = [ + ctypes.c_void_p, + ctypes.c_ulong, + ctypes.c_int, + ] + self.library.XIconifyWindow.restype = ctypes.c_int self.library.XSync.argtypes = [ctypes.c_void_p, ctypes.c_int] self.library.XSync.restype = ctypes.c_int self.library.XFlush.argtypes = [ctypes.c_void_p] @@ -368,6 +476,31 @@ def _prepare_game_window( flush=True, ) mapper.map_window(window, borderless=borderless) + if not borderless: + return + try: + refreshed = mapper.refresh_render_surface(window) + except Exception as exc: + print( + f"Warning: could not synchronize the borderless game surface ({exc}); " + "continuing normally", + file=sys.stderr, + flush=True, + ) + else: + if refreshed: + print( + "Borderless game surface synchronized with its window.", + file=sys.stderr, + flush=True, + ) + else: + print( + "Warning: the window manager declined the borderless surface " + "refresh; continuing normally", + file=sys.stderr, + flush=True, + ) def _apply_game_window_identity(mapper: _X11, window: int) -> None: