diff --git a/tools/gamescope/PATCHES.md b/tools/gamescope/PATCHES.md index 611af30fb..fc3b1b481 100644 --- a/tools/gamescope/PATCHES.md +++ b/tools/gamescope/PATCHES.md @@ -260,3 +260,9 @@ the X pointer stays far from the seat position while input moves, gamescope take each frame's motion to the X pointer itself until the game stops warping. Absolute motion also sends relative motion, as a real compositor does, so XInput2 raw motion reaches games that read only that. A touch moves the pointer without relative motion, so tapping the screen does not turn the camera. + +## 0132 sdl, steamcompmgr: follow a game's window mode +With `--follow-game-window-mode`, the outer window of a nested SDL gamescope follows the focused game window. A window +asking for `_NET_WM_STATE_FULLSCREEN`, or a Steam game covering the X screen without minimise or maximise functions or +decorations in `_MOTIF_WM_HINTS`, makes the outer window fullscreen on the desktop. Anything else makes it a normal window of the +game window's size, or a maximised one when that size does not fit the desktop with its title bar and panel. A change is applied only when the wanted mode changes, so a manual resize or Super+F stays. diff --git a/tools/gamescope/patches/0132-follow-game-window-mode.patch b/tools/gamescope/patches/0132-follow-game-window-mode.patch new file mode 100644 index 000000000..c0fb8e2c1 --- /dev/null +++ b/tools/gamescope/patches/0132-follow-game-window-mode.patch @@ -0,0 +1,254 @@ +diff --git a/src/Backends/SDLBackend.cpp b/src/Backends/SDLBackend.cpp +index 58af150..b050183 100644 +--- a/src/Backends/SDLBackend.cpp ++++ b/src/Backends/SDLBackend.cpp +@@ -51,6 +51,7 @@ namespace gamescope + GAMESCOPE_SDL_EVENT_VISIBLE, + GAMESCOPE_SDL_EVENT_GRAB, + GAMESCOPE_SDL_EVENT_CURSOR, ++ GAMESCOPE_SDL_EVENT_WINDOW_MODE, + + GAMESCOPE_SDL_EVENT_COUNT, + }; +@@ -114,6 +115,7 @@ namespace gamescope + virtual void SetCursorImage( std::shared_ptr info ) override; + virtual void SetRelativeMouseMode( bool bRelative ) override; + virtual void SetVisible( bool bVisible ) override; ++ virtual void SetWindowMode( bool bFullscreen, int nWidth, int nHeight ) override; + virtual void SetTitle( std::shared_ptr szTitle ) override; + virtual void SetIcon( std::shared_ptr> uIconPixels ) override; + virtual void SetSelection( std::shared_ptr szContents, GamescopeSelection eSelection ) override; +@@ -179,6 +181,7 @@ namespace gamescope + void SetCursorImage( std::shared_ptr info ); + void SetRelativeMouseMode( bool bRelative ); + void SetVisible( bool bVisible ); ++ void SetWindowMode( bool bFullscreen, int nWidth, int nHeight ); + void SetTitle( std::shared_ptr szTitle ); + void SetIcon( std::shared_ptr> uIconPixels ); + void SetSelection( std::shared_ptr szContents, GamescopeSelection eSelection ); +@@ -193,6 +196,12 @@ namespace gamescope + bool m_bShown = false; + CSDLConnector m_Connector; // Window. + uint32_t m_uUserEventIdBase = 0u; ++ std::atomic m_bWantFullscreen = { false }; ++ std::atomic m_nWantWidth = { 0 }; ++ std::atomic m_nWantHeight = { 0 }; ++ bool m_bAppliedFullscreen = false; ++ int m_nAppliedWidth = 0; ++ int m_nAppliedHeight = 0; + std::vector m_pszInstanceExtensions; + + std::thread m_SDLThread; +@@ -372,6 +381,10 @@ namespace gamescope + { + m_pBackend->SetVisible( bVisible ); + } ++ void CSDLConnector::SetWindowMode( bool bFullscreen, int nWidth, int nHeight ) ++ { ++ m_pBackend->SetWindowMode( bFullscreen, nWidth, nHeight ); ++ } + void CSDLConnector::SetTitle( std::shared_ptr szTitle ) + { + m_pBackend->SetTitle( std::move( szTitle ) ); +@@ -544,6 +557,13 @@ namespace gamescope + m_bApplicationVisible = bVisible; + PushUserEvent( GAMESCOPE_SDL_EVENT_VISIBLE ); + } ++ void CSDLBackend::SetWindowMode( bool bFullscreen, int nWidth, int nHeight ) ++ { ++ m_bWantFullscreen = bFullscreen; ++ m_nWantWidth = nWidth; ++ m_nWantHeight = nHeight; ++ PushUserEvent( GAMESCOPE_SDL_EVENT_WINDOW_MODE ); ++ } + void CSDLBackend::SetTitle( std::shared_ptr szTitle ) + { + m_pApplicationTitle = szTitle; +@@ -894,6 +914,38 @@ namespace gamescope + } + } + } ++ else if ( event.type == GetUserEventIndex( GAMESCOPE_SDL_EVENT_WINDOW_MODE ) ) ++ { ++ const bool bFullscreen = m_bWantFullscreen; ++ const int nWidth = bFullscreen ? 0 : m_nWantWidth.load(); ++ const int nHeight = bFullscreen ? 0 : m_nWantHeight.load(); ++ ++ if ( bFullscreen != m_bAppliedFullscreen || nWidth != m_nAppliedWidth || nHeight != m_nAppliedHeight ) ++ { ++ m_bAppliedFullscreen = bFullscreen; ++ m_nAppliedWidth = nWidth; ++ m_nAppliedHeight = nHeight; ++ g_bFullscreen = bFullscreen; ++ SDL_SetWindowFullscreen( m_Connector.GetSDLWindow(), bFullscreen ? SDL_WINDOW_FULLSCREEN_DESKTOP : 0 ); ++ if ( !bFullscreen ) ++ { ++ static constexpr int k_nDesktopChromeHeight = 96; ++ ++ SDL_Rect usable = {}; ++ const int nDisplay = SDL_GetWindowDisplayIndex( m_Connector.GetSDLWindow() ); ++ const bool bKnown = SDL_GetDisplayUsableBounds( nDisplay, &usable ) == 0; ++ if ( bKnown && ( nWidth > usable.w || nHeight + k_nDesktopChromeHeight > usable.h ) ) ++ { ++ SDL_MaximizeWindow( m_Connector.GetSDLWindow() ); ++ } ++ else ++ { ++ SDL_RestoreWindow( m_Connector.GetSDLWindow() ); ++ SDL_SetWindowSize( m_Connector.GetSDLWindow(), nWidth, nHeight ); ++ } ++ } ++ } ++ } + else if ( event.type == GetUserEventIndex( GAMESCOPE_SDL_EVENT_TITLE ) ) + { + std::shared_ptr pAppTitle = m_pApplicationTitle; +diff --git a/src/backend.h b/src/backend.h +index 37b936e..fbfa7c7 100644 +--- a/src/backend.h ++++ b/src/backend.h +@@ -273,6 +273,7 @@ namespace gamescope + virtual void SetRelativeMouseMode( bool bRelative ) = 0; + virtual void SetCursorPositionHint( double flX, double flY ) {} + virtual void SetVisible( bool bVisible ) = 0; ++ virtual void SetWindowMode( bool bFullscreen, int nWidth, int nHeight ) {} + virtual void SetTitle( std::shared_ptr szTitle ) = 0; + virtual void SetIcon( std::shared_ptr> uIconPixels ) = 0; + virtual void SetSelection( std::shared_ptr szContents, GamescopeSelection eSelection ) = 0; +diff --git a/src/main.cpp b/src/main.cpp +index eef4264..8f0ddd3 100644 +--- a/src/main.cpp ++++ b/src/main.cpp +@@ -73,6 +73,7 @@ const struct option *gamescope_options = (struct option[]){ + { "rt", no_argument, nullptr, 0 }, + { "prefer-vk-device", required_argument, 0 }, + { "expose-wayland", no_argument, 0 }, ++ { "follow-game-window-mode", no_argument, 0 }, + { "mouse-sensitivity", required_argument, nullptr, 's' }, + { "mangoapp", no_argument, nullptr, 0 }, + { "adaptive-sync", no_argument, nullptr, 0 }, +@@ -700,6 +701,7 @@ static void UpdateCompatEnvVars() + int g_nPreferredOutputWidth = 0; + int g_nPreferredOutputHeight = 0; + bool g_bExposeWayland = false; ++bool g_bFollowGameWindowMode = false; + const char *g_sOutputName = nullptr; + bool g_bDebugLayers = false; + bool g_bForceDisableColorMgmt = false; +@@ -838,6 +840,8 @@ int main(int argc, char **argv) + cv_adaptive_sync = true; + } else if (strcmp(opt_name, "expose-wayland") == 0) { + g_bExposeWayland = true; ++ } else if (strcmp(opt_name, "follow-game-window-mode") == 0) { ++ g_bFollowGameWindowMode = true; + } else if (strcmp(opt_name, "backend") == 0) { + eCurrentBackend = parse_backend_name( optarg ); + } else if (strcmp(opt_name, "cursor-scale-height") == 0) { +diff --git a/src/main.hpp b/src/main.hpp +index b2a169f..48929f2 100644 +--- a/src/main.hpp ++++ b/src/main.hpp +@@ -134,6 +134,7 @@ extern int g_upscaleFilterSharpness; + extern bool g_bBorderlessOutputWindow; + + extern bool g_bExposeWayland; ++extern bool g_bFollowGameWindowMode; + + extern bool g_bRt; + +diff --git a/src/steamcompmgr.cpp b/src/steamcompmgr.cpp +index c468fb3..3188b74 100644 +--- a/src/steamcompmgr.cpp ++++ b/src/steamcompmgr.cpp +@@ -4741,6 +4741,54 @@ static void set_wm_state( xwayland_ctx_t *ctx, Window win, uint32_t state ) + sizeof(wmState) / sizeof(wmState[0])); + } + ++static bool ++window_offers_frame_controls( xwayland_ctx_t *ctx, steamcompmgr_win_t *w ) ++{ ++ static constexpr unsigned long k_ulMotifFunctions = 1ul << 0; ++ static constexpr unsigned long k_ulMotifMinimize = 1ul << 3; ++ static constexpr unsigned long k_ulMotifMaximize = 1ul << 4; ++ static constexpr unsigned long k_ulMotifDecorations = 1ul << 1; ++ ++ Atom type; ++ int format; ++ unsigned long nitems; ++ unsigned long bytesAfter; ++ unsigned char *data = nullptr; ++ if ( XGetWindowProperty( ctx->dpy, w->xwayland().id, ctx->atoms.motifWMHintsAtom, 0, 3, false, ++ AnyPropertyType, &type, &format, &nitems, &bytesAfter, &data ) != Success ) ++ return false; ++ ++ bool bControls = false; ++ if ( data && format == 32 && nitems >= 3 ) ++ { ++ const long *hints = (const long *)data; ++ bControls = ( ( hints[0] & k_ulMotifFunctions ) && ( hints[1] & ( k_ulMotifMinimize | k_ulMotifMaximize ) ) ) || ++ ( ( hints[0] & k_ulMotifDecorations ) && hints[2] ); ++ } ++ if ( data ) ++ XFree( data ); ++ return bControls; ++} ++ ++static void ++apply_game_window_mode( global_focus_t *pFocus ) ++{ ++ static constexpr int k_nSmallestFollowedWindow = 64; ++ ++ steamcompmgr_win_t *w = pFocus->focusWindow; ++ if ( !g_bFollowGameWindowMode || !pFocus->GetNestedHints() || !w || w->type != steamcompmgr_win_type_t::XWAYLAND ) ++ return; ++ ++ xwayland_ctx_t *ctx = w->xwayland().ctx; ++ const XWindowAttributes &a = w->xwayland().a; ++ if ( a.width < k_nSmallestFollowedWindow || a.height < k_nSmallestFollowedWindow ) ++ return; ++ ++ const bool bCoversScreen = a.x == 0 && a.y == 0 && a.width >= ctx->root_width && a.height >= ctx->root_height; ++ const bool bFullscreen = w->isFullscreen || ( w->appID && bCoversScreen && !window_offers_frame_controls( ctx, w ) ); ++ pFocus->GetNestedHints()->SetWindowMode( bFullscreen, a.width, a.height ); ++} ++ + static void + fit_root_to_focus_window( xwayland_ctx_t *ctx ) + { +@@ -5739,6 +5787,7 @@ determine_and_apply_focus( global_focus_t *pFocus ) + if ( pFocus->focusWindow ) + { + pFocus->GetNestedHints()->SetVisible( true ); ++ apply_game_window_mode( pFocus ); + if ( previousLocalFocus.focusWindow != pFocus->focusWindow ) + { + pFocus->GetNestedHints()->SetTitle( pFocus->focusWindow->title ); +@@ -7083,6 +7132,8 @@ static inline float santitize_float( float f ) + static void + handle_property_notify(xwayland_ctx_t *ctx, XPropertyEvent *ev) + { ++ if (ev->atom == ctx->atoms.motifWMHintsAtom) ++ MakeFocusDirty(); + /* check if Trans property was changed */ + if (ev->atom == ctx->atoms.opacityAtom) + { +@@ -9216,6 +9267,7 @@ void init_xwayland_ctx(uint32_t serverId, gamescope_xwayland_server_t *xwayland_ + ctx->atoms.winNormalAtom = XInternAtom(ctx->dpy, "_NET_WM_WINDOW_TYPE_NORMAL", false); + ctx->atoms.sizeHintsAtom = XInternAtom(ctx->dpy, "WM_NORMAL_HINTS", false); + ctx->atoms.netWMStateFullscreenAtom = XInternAtom(ctx->dpy, "_NET_WM_STATE_FULLSCREEN", false); ++ ctx->atoms.motifWMHintsAtom = XInternAtom(ctx->dpy, "_MOTIF_WM_HINTS", false); + ctx->atoms.netActiveWindowAtom = XInternAtom(ctx->dpy, "_NET_ACTIVE_WINDOW", false); + ctx->atoms.netWMStateAtom = XInternAtom(ctx->dpy, "_NET_WM_STATE", false); + ctx->atoms.WMTransientForAtom = XInternAtom(ctx->dpy, "WM_TRANSIENT_FOR", false); +diff --git a/src/xwayland_ctx.hpp b/src/xwayland_ctx.hpp +index 15c2e6b..525b155 100644 +--- a/src/xwayland_ctx.hpp ++++ b/src/xwayland_ctx.hpp +@@ -128,6 +128,7 @@ struct xwayland_ctx_t final : public gamescope::IWaitable + Atom winNormalAtom; + Atom sizeHintsAtom; + Atom netWMStateFullscreenAtom; ++ Atom motifWMHintsAtom; + Atom netActiveWindowAtom; + Atom netWMStateAtom; + Atom WMTransientForAtom; diff --git a/tools/gamescope/release.env b/tools/gamescope/release.env index cf61f9453..a5633d960 100644 --- a/tools/gamescope/release.env +++ b/tools/gamescope/release.env @@ -1,3 +1,3 @@ GAMESCOPE_REPO=Droid-Deck/DroidDeck-Components -GAMESCOPE_TAG=gamescope-3.16.29-p20 -GAMESCOPE_SHA256=30a94becf21f3de4d1d7239c47b5168aab973f4e95981402a550c228929ba827 +GAMESCOPE_TAG=gamescope-3.16.29-p21 +GAMESCOPE_SHA256=c34d2728eb31a5ded35e5cb874e68dd4932a7fba4bcce5f99edf654108f97018 diff --git a/tools/linuxfs/desktop/droiddeck-gpu b/tools/linuxfs/desktop/droiddeck-gpu index f759d3ad0..886f0d0a0 100644 --- a/tools/linuxfs/desktop/droiddeck-gpu +++ b/tools/linuxfs/desktop/droiddeck-gpu @@ -10,7 +10,7 @@ window=0 if [ "${BL_DESKTOP_GPU_WINDOW:-1}" != 0 ] && [ -n "${WAYLAND_DISPLAY:-}" ] && [ "$WAYLAND_DISPLAY" != wayland-0 ]; then window=1 parent=$WAYLAND_DISPLAY - args=(--backend sdl --expose-wayland -W "$width" -H "$height" -w "$width" -h "$height") + args=(--backend sdl --expose-wayland --follow-game-window-mode -W "$width" -H "$height" -w "$width" -h "$height") else args=(--backend wayland --expose-wayland -f -W "$width" -H "$height" -w "$width" -h "$height") fi diff --git a/tools/tests/test_desktop_gpu_window.py b/tools/tests/test_desktop_gpu_window.py index ba3bfc549..5a1ccb5ff 100644 --- a/tools/tests/test_desktop_gpu_window.py +++ b/tools/tests/test_desktop_gpu_window.py @@ -32,6 +32,7 @@ class DesktopGpuWindowTest(unittest.TestCase): def test_on_the_desktop_the_game_gets_a_window_of_its_own(self): run = self.run_gpu(WAYLAND_DISPLAY="wayland-1", MESA_VK_WSI_DEBUG="sw,linear") self.assertEqual(run["args"][:2], ["--backend", "sdl"]) + self.assertIn("--follow-game-window-mode", run["args"]) self.assertNotIn("-f", run["args"]) self.assertEqual(run["args"][-2:], ["/usr/local/bin/droiddeck-gpu-window", "game.exe"]) self.assertEqual(run["env"]["WAYLAND_DISPLAY"], "wayland-1") @@ -44,6 +45,7 @@ class DesktopGpuWindowTest(unittest.TestCase): for env in ({"WAYLAND_DISPLAY": "wayland-1", "BL_DESKTOP_GPU_WINDOW": "0"}, {"WAYLAND_DISPLAY": "wayland-0"}, {}): run = self.run_gpu(**env) self.assertEqual(run["args"][:2], ["--backend", "wayland"]) + self.assertNotIn("--follow-game-window-mode", run["args"]) self.assertIn("-f", run["args"]) self.assertEqual(run["env"]["WAYLAND_DISPLAY"], "wayland-0") self.assertIsNone(run["env"]["MESA_VK_WSI_DEBUG"])