From 2df58136c6421a1534dd5be10a528551e702bef0 Mon Sep 17 00:00:00 2001 From: Utkarsh Dalal Date: Fri, 23 Oct 2026 13:20:17 +0530 Subject: [PATCH] android: send the game window note on the first top-level map (A43) Under X11 the app hides the booting splash on the first mapped window. Under gamescope, A43 sent the game window note only once wine's desktop window committed at least 20 frames a second with a game child mapped, which a static dialog never reaches, so the dialog stayed hidden behind the splash. A43 now sends the note on the first top-level map, on a fresh GAMENATIVE_DRM_SOCKET connection, and again on later commits at most once a second, three notes in all, in case the daemon drops one to a racing connection. The commit-rate gate and its game-child scan are gone, and the change is folded into A43 so the train still re-applies. --- .../0043-A43-x11-game-window-note.patch | 189 +++++++----------- 1 file changed, 74 insertions(+), 115 deletions(-) diff --git a/android/patches/gamescope/0043-A43-x11-game-window-note.patch b/android/patches/gamescope/0043-A43-x11-game-window-note.patch index 7b1cb7848..5c11d9082 100644 --- a/android/patches/gamescope/0043-A43-x11-game-window-note.patch +++ b/android/patches/gamescope/0043-A43-x11-game-window-note.patch @@ -1,25 +1,25 @@ -A43 — report an X11-drawn game window to the daemon +A43 — send the game window note on the first top-level map The daemon clears the app's booting splash when it receives a GameWindowNote on the scan-out channel. Only the guest ICD shim sends one, from its swapchain present path, so a game that never presents through the shim's WSI proxy keeps the splash up while it runs behind it: an OpenGL game whose zink output reaches -X as plain drawing (Balatro), or a GDI game. +X as plain drawing (Balatro), a GDI game, or a static dialog such as an assert +MessageBox. -Such a game draws into a child of wine's desktop window, and Xwayland commits -the desktop window's own surface for every frame. So while no override surface -is bound on the focused window, its main-surface commits are counted per -second; when a second carries at least 20 of them and the desktop window has a -game child (the shim's own rule: viewable, InputOutput, larger than 1x1, a -readable WM_CLASS instance that differs from the desktop's), the compositor -sends the note itself on a fresh GAMENATIVE_DRM_SOCKET connection. At most three -notes are sent, one per qualifying second, so a note dropped by a racing -connection is sent again; the daemon latches the first. +The note was first sent on a commit-rate gate: while no override surface was +bound, the focused wine desktop window's main-surface commits were counted, and +a second with at least 20 of them and a game child mapped sent it. A static +dialog never reaches that rate, so it stayed hidden behind the splash. The note +now follows the X11 server's rule, where the app hides the splash on the first +mapped window, whatever it is: the compositor sends it on the first map of a +top-level window (in practice wine's desktop window), on a fresh +GAMENATIVE_DRM_SOCKET connection. -A game that presents through the shim is not affected: once its override -surface is bound its frames no longer land on the main surface and the count is -reset, and before its first present a mapped, still window commits far below -the threshold, so the splash is not lifted over it early. +The daemon serves only the newest scan-out connection, so a note can be dropped +when another client connects right after it. It is sent again on a later +commit, at most once a second and at most three times in all; the daemon +latches the first. --- a/src/steamcompmgr.cpp +++ b/src/steamcompmgr.cpp @@ -31,81 +31,46 @@ the threshold, so the splash is not lifted over it early. #include #include #include -@@ -8710,6 +8711,161 @@ void update_wayland_res(CommitDoneList_t *doneCommits, steamcompmgr_win_t *w, Re - w->commit_queue.push_back( std::move(newCommit) ); +@@ -5918,6 +5919,9 @@ + } + } } ++ ++static void ++x11_top_level_mapped( steamcompmgr_win_t *w ); + + static void + map_win(xwayland_ctx_t* ctx, Window id, unsigned long sequence) +@@ -6025,6 +6029,8 @@ + } + + handle_desktop_window( w ); ++ ++ x11_top_level_mapped( w ); + + MakeFocusDirty(); + +@@ -8708,8 +8714,99 @@ + } -+// gamenative A43: the booting splash is cleared by the daemon when it hears the -+// game's window is up. The ICD shim reports that from its swapchain present, so a -+// game that never presents through the shim (OpenGL drawn through X, GDI) never -+// clears it. While no override surface is bound, a wine desktop window that -+// keeps committing its own surface with a game child mapped is that game -+// drawing through X, and the compositor sends the same note itself. -+static LogScope s_GameWindowLogScope( "game-window" ); + w->commit_queue.push_back( std::move(newCommit) ); ++} + -+static constexpr uint64_t k_ulX11GameWindowSampleNs = 1'000'000'000ull; -+static constexpr uint32_t k_unX11GameWindowMinCommits = 20; ++// gamenative A43/A44: the booting splash is cleared by the daemon when it hears ++// the game's window is up. The ICD shim reports that from its swapchain present, ++// which a game drawn through X (OpenGL, GDI) or a static dialog never reaches. ++// So the compositor follows the X11 server's rule and sends the same note ++// itself on the first top-level map. ++static constexpr uint64_t k_ulX11GameWindowResendNs = 1'000'000'000ull; +static constexpr uint32_t k_unX11GameWindowMaxNotes = 3; + +static struct +{ -+ uint64_t ulSampleStart = 0; -+ uint32_t unCommits = 0; ++ Window firstMapped = None; ++ uint64_t ulLastAttempt = 0; + uint32_t unNotes = 0; +} s_X11GameWindow; + -+static std::string -+x11_window_instance( Display *dpy, Window win ) -+{ -+ std::string instance; -+ XClassHint hint = {}; -+ if ( !XGetClassHint( dpy, win, &hint ) ) -+ return instance; -+ if ( hint.res_name ) -+ { -+ instance = hint.res_name; -+ XFree( hint.res_name ); -+ } -+ if ( hint.res_class ) -+ XFree( hint.res_class ); -+ return instance; -+} -+ -+// Same rule as the shim's child scan: a viewable, InputOutput, non-degenerate -+// child of the desktop window whose WM_CLASS instance is readable and differs -+// from the desktop's own. -+static bool -+x11_game_child_present( Display *dpy, Window desktop ) -+{ -+ std::string desktopInstance = x11_window_instance( dpy, desktop ); -+ if ( desktopInstance.empty() ) -+ return false; -+ -+ Window root = None, parent = None; -+ Window *children = nullptr; -+ unsigned int nchildren = 0; -+ if ( !XQueryTree( dpy, desktop, &root, &parent, &children, &nchildren ) ) -+ return false; -+ -+ bool bFound = false; -+ for ( unsigned int i = 0; i < nchildren && !bFound; i++ ) -+ { -+ XWindowAttributes attribs; -+ if ( !XGetWindowAttributes( dpy, children[ i ], &attribs ) ) -+ continue; -+ if ( attribs.map_state != IsViewable || attribs.c_class != InputOutput ) -+ continue; -+ if ( attribs.width <= 1 || attribs.height <= 1 ) -+ continue; -+ std::string childInstance = x11_window_instance( dpy, children[ i ] ); -+ bFound = !childInstance.empty() && childInstance != desktopInstance; -+ } -+ -+ if ( children ) -+ XFree( children ); -+ return bFound; -+} -+ +// One GameWindowNote (gndrm-uapi wire.rs: "GNW1", present = 1, 16 reserved +// bytes) on a fresh scan-out connection. The daemon serves the newest +// connection, reads the note and drops the channel when it closes. @@ -144,60 +109,54 @@ the threshold, so the splash is not lifted over it early. + } + close( fd ); + return bSent; -+} -+ + } + ++// The daemon serves only the newest scan-out connection, so a note can be ++// dropped when another client connects right after it. Notes are sent at most ++// once a second and at most three in all; the daemon latches the first. +static void -+x11_game_window_commit( xwayland_ctx_t *ctx, steamcompmgr_win_t *w, struct wlr_surface *surf ) ++x11_game_window_note( const char *pszReason ) +{ + auto &s = s_X11GameWindow; + if ( s.unNotes >= k_unX11GameWindowMaxNotes ) + return; -+ if ( w == nullptr || w->type != steamcompmgr_win_type_t::XWAYLAND ) -+ return; + -+ global_focus_t *pFocus = GetCurrentFocus(); -+ if ( !pFocus || pFocus->focusWindow != w ) ++ uint64_t ulNow = get_time_in_nanos(); ++ if ( s.ulLastAttempt != 0 && ulNow - s.ulLastAttempt < k_ulX11GameWindowResendNs ) + return; ++ s.ulLastAttempt = ulNow; + -+ if ( w->xwayland().surface.override_surface != nullptr || w->bSessionSawGameWindow ) ++ bool bSent = send_game_window_note(); ++ if ( bSent ) + { -+ s.ulSampleStart = 0; -+ s.unCommits = 0; -+ return; ++ s.unNotes++; ++ hasRepaint = true; + } ++ xwm_log.infof( "%s 0x%lx: game window note %s (%u/%u)", ++ pszReason, s.firstMapped, bSent ? "sent" : "failed", s.unNotes, k_unX11GameWindowMaxNotes ); ++} + -+ if ( surf != w->xwayland().surface.main_surface ) -+ return; -+ -+ uint64_t ulNow = get_time_in_nanos(); -+ if ( s.ulSampleStart == 0 ) -+ s.ulSampleStart = ulNow; -+ s.unCommits++; -+ if ( ulNow - s.ulSampleStart < k_ulX11GameWindowSampleNs ) -+ return; -+ -+ bool bRendering = s.unCommits >= k_unX11GameWindowMinCommits; -+ uint32_t unCommits = s.unCommits; -+ s.ulSampleStart = ulNow; -+ s.unCommits = 0; -+ if ( !bRendering || !x11_game_child_present( ctx->dpy, w->xwayland().id ) ) ++static void ++x11_top_level_mapped( steamcompmgr_win_t *w ) ++{ ++ auto &s = s_X11GameWindow; ++ if ( s.firstMapped != None ) + return; + -+ bool bSent = send_game_window_note(); -+ if ( bSent ) -+ s.unNotes++; -+ s_GameWindowLogScope.infof( "X11 game window drawing on 0x%lx (%u commits/s): note %s (%u/%u)", -+ w->xwayland().id, unCommits, bSent ? "sent" : "failed", s.unNotes, k_unX11GameWindowMaxNotes ); ++ s.firstMapped = w->xwayland().id; ++ x11_game_window_note( "first top-level map" ); +} + void check_new_xwayland_res(xwayland_ctx_t *ctx) { // When importing buffer, we'll potentially need to perform operations with -@@ -8720,6 +8876,7 @@ void check_new_xwayland_res(xwayland_ctx_t *ctx) - for ( uint32_t i = 0; i < tmp_queue.size(); i++ ) - { +@@ -8722,6 +8819,9 @@ steamcompmgr_win_t *w = find_win( ctx, tmp_queue[ i ].surf ); -+ x11_game_window_commit( ctx, w, tmp_queue[ i ].surf ); update_wayland_res( &ctx->doneCommits, w, tmp_queue[ i ]); } ++ ++ if ( !tmp_queue.empty() && s_X11GameWindow.firstMapped != None ) ++ x11_game_window_note( "resend after first map of" ); } + + void check_new_xdg_res()