diff --git a/android/patches/gamescope/0054-A58-mailbox-hold-under-limiter.patch b/android/patches/gamescope/0054-A58-mailbox-hold-under-limiter.patch new file mode 100644 index 000000000..885b4cbb6 --- /dev/null +++ b/android/patches/gamescope/0054-A58-mailbox-hold-under-limiter.patch @@ -0,0 +1,384 @@ +A58 — under the FPS limiter, hold a second mailbox commit for the next vblank + +A mailbox client that paces itself just under the refresh rate (a guest at +~59.3 fps under a 60 cap on a 59.97 Hz panel) completes two commits inside one +composite window about once a second. The done-commit handling latches every +non-FIFO commit as it completes, so the second one replaces the first before +the paint and the panel repeats a frame (58.3-59.0 fps, 2.7-2.9 % doubled +intervals in gamenative-drm PR #46, B4-B6). The client cannot avoid it: the +latch point is gamescope's vblank wakeup (target vblank minus the rolling +draw-time estimate minus the red zone), which only gamescope knows. + +With gn_mailbox_hold on, while the FPS limiter is engaged for a window (a +target FPS is set, the window is not limiter-exempt, no VRR), the done-commit +handling for Xwayland and xdg windows does this for non-FIFO commits: + +- the first commit to complete for the coming paint is latched as today; +- a newer one is held in the done list instead of replacing it, and is latched + by the first done-commit pass of the next vblank cycle (the vblank pass + always runs one, so a held commit never waits for the client); +- a third replaces the held one; the replaced commit is dropped from the + window's queue, which releases its buffer and discards its presentation + feedback, as a mailbox replace does today. + +The queue is one deep, the client is never blocked, and a window that delivers +at or below the refresh rate never loses a frame. A held commit's earliest +present time is stamped by the pass that latches it. FIFO commits, unlimited +windows and VRR are unchanged. + +A held commit is shown one paint late. A client that keeps delivering one +commit per vblank never drains that: each new commit finds its paint taken by +the carried one and is carried in turn, a standing one-frame delay (seen with +in-game VSync on, where every commit was carried). So the carry is bounded: +after gn_mailbox_hold_max_carry paints in a row (default 8) have shown a +carried commit, the next fresh commit is latched at once and replaces the +carried one, a mailbox replace that drops one frame and puts the window back +on its own paint. A client below the refresh rate drains the carry by itself +at its next gap, as before. + +gn_mailbox_hold: -1 (default) = on when the gamenative-drm backend is in use +(GAMENATIVE_DRM_SOCKET set), else off; gamescope_gn_mailbox_hold=0/1 overrides +it; on Android the property debug.gamescope.gn_mailbox_hold=0/1 wins when set. +gn_mailbox_hold_max_carry (0 = unbounded) follows the same pattern, with the +property debug.gamescope.gn_mailbox_hold_max_carry. Both are resolved once, at +the first done-commit pass. + +Logs: "gn_mailbox_hold=<0|1> source= max_carry=N" once at +start, and about every 600 vblanks while the hold does anything: +"gn_mailbox_hold: deferred=N carried_paints=C max_run=R collapsed=X +replaced=M over K vblanks". deferred counts each held commit once; +carried_paints counts paints that showed a commit one paint late (the latency +the hold adds); max_run is the longest run of those; collapsed counts carries +ended by the bound; replaced counts held commits dropped for a newer one. + +diff --git a/src/steamcompmgr.cpp b/src/steamcompmgr.cpp +index 81f5cd8..df657d1 100644 +--- a/src/steamcompmgr.cpp ++++ b/src/steamcompmgr.cpp +@@ -101,6 +101,10 @@ + #include "vrclient_detect.h" + #include "Utils/Process.h" + ++#if defined(__ANDROID__) ++#include ++#endif ++ + #include "wlr_begin.hpp" + #include "wlr/types/wlr_pointer_constraints_v1.h" + #include "wlr_end.hpp" +@@ -8216,6 +8220,212 @@ bool handle_done_commit( steamcompmgr_win_t *w, xwayland_ctx_t *ctx, uint64_t co + return false; + } + ++// gamenative A58: mailbox hold under the FPS limiter. ++// ++// A mailbox client pacing itself just under the refresh rate (59.3 fps under a ++// 60 cap on a 59.97 Hz panel) completes two commits inside one composite window ++// about once a second. Collapsing them to the newest, as mailbox does, makes ++// the panel repeat a frame. Only gamescope knows where its latch point is, so ++// the hold lives here: once a window has had a non-FIFO commit latched for the ++// coming paint, a newer one is held and latched for the paint after it. The ++// hold is one deep: a third commit replaces the held one, which is released ++// (and its presentation feedback discarded) right away, as mailbox would. The ++// client is never blocked, and a window at or below the refresh rate never ++// loses a frame. A held commit is latched by the first done-commit pass of the ++// next vblank cycle; the vblank pass always runs one, so an idle client does ++// not leave it stuck. ++// ++// A held commit is shown one paint after the one it completed for. If the ++// client keeps delivering one commit per vblank, that one-paint delay never ++// drains: every new commit finds its paint taken by the carried one and is ++// carried in turn. So the carry is bounded: once gn_mailbox_hold_max_carry ++// paints in a row have shown a carried commit, the next fresh commit is ++// latched at once and replaces the carried one (a mailbox replace: one frame ++// dropped, the window back on its own paint). A client below the refresh rate ++// drains the carry by itself at its next gap. ++// ++// gn_mailbox_hold: -1 = auto (on when the gamenative-drm backend is in use), ++// 0 = off, 1 = on. Overridable with gamescope_gn_mailbox_hold=0/1 and, on ++// Android, the property debug.gamescope.gn_mailbox_hold=0/1 (wins when set). ++// gn_mailbox_hold_max_carry: paints in a row a carried commit may hold the ++// window before a fresh one displaces it (0 = unbounded); same env/property ++// pattern (debug.gamescope.gn_mailbox_hold_max_carry). Both are resolved ++// once, at the first done-commit pass. ++gamescope::ConVar cv_gn_mailbox_hold{ "gn_mailbox_hold", -1, "gamenative: hold one newer mailbox commit for the next vblank while the FPS limiter is engaged (-1 auto, 0 off, 1 on)." }; ++gamescope::ConVar cv_gn_mailbox_hold_max_carry{ "gn_mailbox_hold_max_carry", 8, "gamenative: paints in a row a held mailbox commit may be shown late before a fresh commit displaces it (0 = unbounded)." }; ++ ++#if defined(__ANDROID__) ++// An integer Android property, or nullopt when it is unset or not a number. ++static std::optional gn_int_property( const char *pszName ) ++{ ++ char szProp[ PROP_VALUE_MAX ] = {}; ++ if ( __system_property_get( pszName, szProp ) <= 0 ) ++ return std::nullopt; ++ char *pEnd = nullptr; ++ long lValue = strtol( szProp, &pEnd, 10 ); ++ if ( pEnd == szProp || *pEnd != '\0' ) ++ return std::nullopt; ++ return (int)lValue; ++} ++#endif ++ ++static int s_nGnMailboxMaxCarry = 0; ++ ++static bool gn_mailbox_hold_enabled() ++{ ++ static const bool s_bEnabled = []() ++ { ++ const char *pszSource = "default"; ++ bool bEnabled; ++ { ++ const char *pszSock = getenv( "GAMENATIVE_DRM_SOCKET" ); ++ bEnabled = pszSock && *pszSock; ++ } ++ if ( cv_gn_mailbox_hold != -1 ) ++ { ++ bEnabled = cv_gn_mailbox_hold != 0; ++ pszSource = "env"; ++ } ++ s_nGnMailboxMaxCarry = std::max( 0, (int)cv_gn_mailbox_hold_max_carry ); ++#if defined(__ANDROID__) ++ if ( std::optional oProp = gn_int_property( "debug.gamescope.gn_mailbox_hold" ); oProp && ( *oProp == 0 || *oProp == 1 ) ) ++ { ++ bEnabled = *oProp == 1; ++ pszSource = "prop"; ++ } ++ if ( std::optional oProp = gn_int_property( "debug.gamescope.gn_mailbox_hold_max_carry" ); oProp && *oProp >= 0 ) ++ s_nGnMailboxMaxCarry = *oProp; ++#endif ++ xwm_log.infof( "gn_mailbox_hold=%d source=%s max_carry=%d", bEnabled ? 1 : 0, pszSource, s_nGnMailboxMaxCarry ); ++ return bEnabled; ++ }(); ++ return s_bEnabled; ++} ++ ++static bool gn_mailbox_hold_applies( steamcompmgr_win_t *w ) ++{ ++ if ( !w || !gn_mailbox_hold_enabled() ) ++ return false; ++ if ( !g_nSteamCompMgrTargetFPS || !steamcompmgr_window_should_limit_fps( w ) ) ++ return false; ++ if ( GetBackend()->GetCurrentConnector() && GetBackend()->GetCurrentConnector()->IsVRRActive() ) ++ return false; ++ return true; ++} ++ ++// Counters for the report line, since start. ++static uint64_t s_ulGnDeferred = 0; // commits held for a later paint (each counted once) ++static uint64_t s_ulGnReplaced = 0; // held commits replaced by a newer one before their paint ++static uint64_t s_ulGnCarriedPaints = 0; // paints that showed a carried commit (one paint late) ++static uint64_t s_ulGnCollapsed = 0; // carries ended by a fresh commit (gn_mailbox_hold_max_carry) ++static uint32_t s_uGnMaxRun = 0; // longest run of carried paints since the last report ++ ++// vblank_idx is the cycle whose paint a done-commit pass feeds (it advances ++// after the vblank pass's done-commit handling); +1 keeps 0 as "none". A held ++// commit can therefore never be latched for the paint it was held from: the ++// paint id only changes when vblank_idx does. ++static inline uint64_t gn_mailbox_paint_id( uint64_t vblank_idx ) { return vblank_idx + 1; } ++ ++// Decide for a done commit. Returns true when the entry is held: it was put ++// (or replaced the window's held entry) in `held`, the list carried to the ++// next pass, marked gnHeld. `held_index` maps a window seq to its held entry ++// in `held` for this pass. Returns false when the entry is latched now. ++static bool gn_mailbox_hold_entry( steamcompmgr_win_t *w, const CommitDoneEntry_t &entry, uint64_t vblank_idx, ++ std::vector< CommitDoneEntry_t > &held, std::unordered_map< uint64_t, size_t > &held_index ) ++{ ++ if ( entry.fifo || !gn_mailbox_hold_applies( w ) ) ++ return false; ++ ++ // Nothing latched for the coming paint yet: latch this one. ++ if ( w->gnMailboxLatchedForPaint != gn_mailbox_paint_id( vblank_idx ) ) ++ return false; ++ ++ // The paint is taken by a carried commit and the carry has run its bound: ++ // a fresh commit is latched now and replaces it (gn_mailbox_hold_latched). ++ if ( !entry.gnHeld && w->gnMailboxLatchedCarried && s_nGnMailboxMaxCarry > 0 && ++ w->gnMailboxCarryRun >= (uint32_t)s_nGnMailboxMaxCarry ) ++ { ++ s_ulGnCollapsed++; ++ return false; ++ } ++ ++ CommitDoneEntry_t heldEntry = entry; ++ heldEntry.gnHeld = true; ++ if ( !entry.gnHeld ) ++ s_ulGnDeferred++; ++ ++ auto iter = held_index.find( entry.winSeq ); ++ if ( iter == held_index.end() ) ++ { ++ held_index[ entry.winSeq ] = held.size(); ++ held.push_back( heldEntry ); ++ return true; ++ } ++ ++ // A third commit in the window: it replaces the held one. Dropping the ++ // replaced commit from the queue releases its buffer and discards its ++ // presentation feedback (commit_t's destructor), as a mailbox replace does. ++ CommitDoneEntry_t &replaced = held[ iter->second ]; ++ for ( auto it = w->commit_queue.begin(); it != w->commit_queue.end(); ++it ) ++ { ++ if ( (*it)->commitID == replaced.commitID ) ++ { ++ w->commit_queue.erase( it ); ++ break; ++ } ++ } ++ replaced = heldEntry; ++ s_ulGnReplaced++; ++ return true; ++} ++ ++// A non-FIFO commit was latched for the coming paint: record it, and whether ++// it is a carried one (held from the paint before). ++static void gn_mailbox_hold_latched( steamcompmgr_win_t *w, const CommitDoneEntry_t &entry, uint64_t vblank_idx ) ++{ ++ const uint64_t ulPaint = gn_mailbox_paint_id( vblank_idx ); ++ if ( w->gnMailboxLatchedForPaint == ulPaint ) ++ { ++ // A second latch for the same paint: a fresh commit displacing a ++ // carried one, or a window the hold does not apply to. ++ if ( w->gnMailboxLatchedCarried && !entry.gnHeld && s_ulGnCarriedPaints ) ++ s_ulGnCarriedPaints--; ++ } ++ else if ( entry.gnHeld ) ++ { ++ w->gnMailboxCarryRun++; ++ s_ulGnCarriedPaints++; ++ s_uGnMaxRun = std::max( s_uGnMaxRun, w->gnMailboxCarryRun ); ++ } ++ if ( !entry.gnHeld ) ++ w->gnMailboxCarryRun = 0; ++ w->gnMailboxLatchedForPaint = ulPaint; ++ w->gnMailboxLatchedCarried = entry.gnHeld; ++} ++ ++// One line per ~600 cycles while the hold does anything, to confirm it is ++// active and show what it costs: deferred = commits held for a later paint, ++// carried_paints = paints that showed a commit one paint late, max_run = the ++// longest run of those in a row, collapsed = carries ended by the bound, ++// replaced = held commits dropped for a newer one. ++static void gn_mailbox_hold_report( uint64_t vblank_idx ) ++{ ++ static uint64_t s_ulLastReportIdx = 0; ++ static uint64_t s_ulLast[ 4 ] = {}; ++ if ( vblank_idx < s_ulLastReportIdx + 600 ) ++ return; ++ const uint64_t ulNow[ 4 ] = { s_ulGnDeferred, s_ulGnCarriedPaints, s_ulGnCollapsed, s_ulGnReplaced }; ++ if ( memcmp( ulNow, s_ulLast, sizeof( ulNow ) ) != 0 ) ++ { ++ xwm_log.infof( "gn_mailbox_hold: deferred=%" PRIu64 " carried_paints=%" PRIu64 " max_run=%u collapsed=%" PRIu64 " replaced=%" PRIu64 " over %" PRIu64 " vblanks", ++ ulNow[ 0 ] - s_ulLast[ 0 ], ulNow[ 1 ] - s_ulLast[ 1 ], s_uGnMaxRun, ++ ulNow[ 2 ] - s_ulLast[ 2 ], ulNow[ 3 ] - s_ulLast[ 3 ], vblank_idx - s_ulLastReportIdx ); ++ } ++ s_ulLastReportIdx = vblank_idx; ++ memcpy( s_ulLast, ulNow, sizeof( ulNow ) ); ++ s_uGnMaxRun = 0; ++} ++ + // TODO: Merge these two functions. + void handle_done_commits_xwayland( xwayland_ctx_t *ctx, bool vblank, uint64_t vblank_idx ) + { +@@ -8233,6 +8443,10 @@ void handle_done_commits_xwayland( xwayland_ctx_t *ctx, bool vblank, uint64_t vb + fifo_win_seqs.clear(); + fifo_win_seqs.reserve( 32 ); + ++ // gamenative A58: this pass's held mailbox entry per window seq. ++ static std::unordered_map< uint64_t, size_t > gn_held_index; ++ gn_held_index.clear(); ++ + uint64_t now = get_time_in_nanos(); + + // very fast loop yes +@@ -8257,6 +8471,11 @@ void handle_done_commits_xwayland( xwayland_ctx_t *ctx, bool vblank, uint64_t vb + continue; + } + ++ // gamenative A58: before earliestPresentTime is stamped, so a held ++ // commit is stamped by the pass that latches it. ++ if ( entry_win && gn_mailbox_hold_entry( entry_win, entry, vblank_idx, commits_before_their_time, gn_held_index ) ) ++ continue; ++ + if (!entry.earliestPresentTime) + { + entry.earliestPresentTime = next_refresh_time; +@@ -8273,10 +8492,15 @@ void handle_done_commits_xwayland( xwayland_ctx_t *ctx, bool vblank, uint64_t vb + { + if (entry.fifo) + fifo_win_seqs.insert(entry.winSeq); ++ else // gamenative A58: this window has its commit for the coming paint. ++ gn_mailbox_hold_latched( entry_win, entry, vblank_idx ); + } + } + + ctx->doneCommits.listCommitsDone.swap( commits_before_their_time ); ++ ++ if ( vblank ) ++ gn_mailbox_hold_report( vblank_idx ); + } + + void handle_done_commits_xdg( bool vblank, uint64_t vblank_idx ) +@@ -8295,6 +8519,10 @@ void handle_done_commits_xdg( bool vblank, uint64_t vblank_idx ) + fifo_win_seqs.clear(); + fifo_win_seqs.reserve( 32 ); + ++ // gamenative A58: this pass's held mailbox entry per window seq. ++ static std::unordered_map< uint64_t, size_t > gn_held_index; ++ gn_held_index.clear(); ++ + uint64_t now = get_time_in_nanos(); + + // very fast loop yes +@@ -8318,6 +8546,11 @@ void handle_done_commits_xdg( bool vblank, uint64_t vblank_idx ) + commits_before_their_time.push_back( entry ); + continue; + } ++ ++ // gamenative A58: before earliestPresentTime is stamped, so a held ++ // commit is stamped by the pass that latches it. ++ if ( entry_win && gn_mailbox_hold_entry( entry_win, entry, vblank_idx, commits_before_their_time, gn_held_index ) ) ++ continue; + + if (!entry.earliestPresentTime) + { +@@ -8335,6 +8568,8 @@ void handle_done_commits_xdg( bool vblank, uint64_t vblank_idx ) + { + if (entry.fifo) + fifo_win_seqs.insert(entry.winSeq); ++ else // gamenative A58: this window has its commit for the coming paint. ++ gn_mailbox_hold_latched( entry_win, entry, vblank_idx ); + } + } + +diff --git a/src/steamcompmgr_shared.hpp b/src/steamcompmgr_shared.hpp +index aea260b..5da643d 100644 +--- a/src/steamcompmgr_shared.hpp ++++ b/src/steamcompmgr_shared.hpp +@@ -164,6 +164,14 @@ struct steamcompmgr_win_t { + bool unlockedForFrameCallback = false; + bool receivedDoneCommit = false; + ++ // gamenative A58: vblank_idx + 1 of the paint this window's last non-FIFO ++ // commit was latched for (0 = none), whether that commit was a carried one ++ // (held from the paint before), and how many paints in a row have shown a ++ // carried commit; see gn_mailbox_hold_entry. ++ uint64_t gnMailboxLatchedForPaint = 0; ++ bool gnMailboxLatchedCarried = false; ++ uint32_t gnMailboxCarryRun = 0; ++ + // gamenative: set once the guest's own content (the WSI shim's override + // surface) has been bound on this window, i.e. this session has had a game + // window at some point. Only ever set by that coming into existence, so a +diff --git a/src/xwayland_ctx.hpp b/src/xwayland_ctx.hpp +index 7b39ecb..221a95b 100644 +--- a/src/xwayland_ctx.hpp ++++ b/src/xwayland_ctx.hpp +@@ -55,6 +55,8 @@ struct CommitDoneEntry_t + uint64_t earliestPresentTime; + uint64_t earliestLatchTime; + bool fifo; ++ // gamenative A58: held by the mailbox hold for a later paint. ++ bool gnHeld = false; + }; + + struct CommitDoneList_t