From 52e1b1ff62d09f9afe119e3554c4bbb454a052c7 Mon Sep 17 00:00:00 2001 From: Utkarsh Dalal Date: Tue, 3 Nov 2026 07:37:01 +0530 Subject: [PATCH 1/2] =?UTF-8?q?android:=20A58=20=E2=80=94=20under=20the=20?= =?UTF-8?q?FPS=20limiter,=20hold=20a=20second=20mailbox=20commit=20for=20t?= =?UTF-8?q?he=20next=20vblank?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A mailbox client pacing itself just under the refresh rate gets two commits into one composite window about once a second, and the newer one replaces the older before the paint, so the panel repeats a frame. Under the limiter, with gn_mailbox_hold on (default on gamenative-drm), the second commit is held and latched at the next vblank; a third replaces the held one. Test build for the mailbox-at-display-rate study; see the patch header. --- .../0054-A58-mailbox-hold-under-limiter.patch | 272 ++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 android/patches/gamescope/0054-A58-mailbox-hold-under-limiter.patch 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..acb72f964 --- /dev/null +++ b/android/patches/gamescope/0054-A58-mailbox-hold-under-limiter.patch @@ -0,0 +1,272 @@ +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. + +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. +It is resolved once, at the first done-commit pass. + +Logs: "gn_mailbox_hold=<0|1> source=" once at start, and +"gn_mailbox_hold: held=N replaced=M over K vblanks (total ...)" about every +600 vblanks while holds happen. + +diff --git a/src/steamcompmgr.cpp b/src/steamcompmgr.cpp +index 81f5cd8..987d54c 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,133 @@ 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. ++// ++// 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). ++// 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)." }; ++ ++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"; ++ } ++#if defined(__ANDROID__) ++ char szProp[ PROP_VALUE_MAX ] = {}; ++ if ( __system_property_get( "debug.gamescope.gn_mailbox_hold", szProp ) > 0 && ++ ( szProp[0] == '0' || szProp[0] == '1' ) ) ++ { ++ bEnabled = szProp[0] == '1'; ++ pszSource = "prop"; ++ } ++#endif ++ xwm_log.infof( "gn_mailbox_hold=%d source=%s", bEnabled ? 1 : 0, pszSource ); ++ 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; ++} ++ ++static uint64_t s_ulGnHoldCount = 0; ++static uint64_t s_ulGnHoldReplacedCount = 0; ++ ++// 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". ++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. `held_index` maps a window seq to its held entry in `held` for ++// this pass. Returns false when the entry goes on as today. ++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; ++ ++ auto iter = held_index.find( entry.winSeq ); ++ if ( iter == held_index.end() ) ++ { ++ held_index[ entry.winSeq ] = held.size(); ++ held.push_back( entry ); ++ s_ulGnHoldCount++; ++ 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 = entry; ++ s_ulGnHoldReplacedCount++; ++ return true; ++} ++ ++// One line per ~600 cycles while holds happen, to confirm the hold is active. ++static void gn_mailbox_hold_report( uint64_t vblank_idx ) ++{ ++ static uint64_t s_ulLastReportIdx = 0; ++ static uint64_t s_ulLastHold = 0, s_ulLastReplaced = 0; ++ if ( vblank_idx < s_ulLastReportIdx + 600 ) ++ return; ++ if ( s_ulGnHoldCount != s_ulLastHold || s_ulGnHoldReplacedCount != s_ulLastReplaced ) ++ { ++ xwm_log.infof( "gn_mailbox_hold: held=%" PRIu64 " replaced=%" PRIu64 " over %" PRIu64 " vblanks (total held=%" PRIu64 " replaced=%" PRIu64 ")", ++ s_ulGnHoldCount - s_ulLastHold, s_ulGnHoldReplacedCount - s_ulLastReplaced, ++ vblank_idx - s_ulLastReportIdx, s_ulGnHoldCount, s_ulGnHoldReplacedCount ); ++ } ++ s_ulLastReportIdx = vblank_idx; ++ s_ulLastHold = s_ulGnHoldCount; ++ s_ulLastReplaced = s_ulGnHoldReplacedCount; ++} ++ + // TODO: Merge these two functions. + void handle_done_commits_xwayland( xwayland_ctx_t *ctx, bool vblank, uint64_t vblank_idx ) + { +@@ -8233,6 +8364,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 +8392,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 +8413,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. ++ entry_win->gnMailboxLatchedForPaint = gn_mailbox_paint_id( 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 +8440,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 +8467,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 +8489,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. ++ entry_win->gnMailboxLatchedForPaint = gn_mailbox_paint_id( vblank_idx ); + } + } + +diff --git a/src/steamcompmgr_shared.hpp b/src/steamcompmgr_shared.hpp +index aea260b..6bdfb54 100644 +--- a/src/steamcompmgr_shared.hpp ++++ b/src/steamcompmgr_shared.hpp +@@ -164,6 +164,10 @@ 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); see gn_mailbox_hold_entry. ++ uint64_t gnMailboxLatchedForPaint = 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 From 9fad0704553d57cedaab575a1351d717fdecb034 Mon Sep 17 00:00:00 2001 From: Utkarsh Dalal Date: Tue, 3 Nov 2026 08:12:45 +0530 Subject: [PATCH 2/2] =?UTF-8?q?android:=20A58=20=E2=80=94=20bound=20the=20?= =?UTF-8?q?carry:=20a=20standing=20one-paint=20delay=20ends=20after=20gn?= =?UTF-8?q?=5Fmailbox=5Fhold=5Fmax=5Fcarry=20paints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With in-game VSync on the client delivers one commit per vblank, so once a commit was held every later one found its paint taken by the carried commit and was carried in turn: a standing one-frame delay that frame rate cannot show. After gn_mailbox_hold_max_carry paints in a row (default 8, 0 = unbounded, property debug.gamescope.gn_mailbox_hold_max_carry) a fresh commit is latched at once and replaces the carried one. The report counted a held commit once per compositor pass it stayed held (twice per vblank); it now counts each deferral once and reports carried_paints, max_run and collapsed alongside replaced. --- .../0054-A58-mailbox-hold-under-limiter.patch | 192 ++++++++++++++---- 1 file changed, 152 insertions(+), 40 deletions(-) diff --git a/android/patches/gamescope/0054-A58-mailbox-hold-under-limiter.patch b/android/patches/gamescope/0054-A58-mailbox-hold-under-limiter.patch index acb72f964..885b4cbb6 100644 --- a/android/patches/gamescope/0054-A58-mailbox-hold-under-limiter.patch +++ b/android/patches/gamescope/0054-A58-mailbox-hold-under-limiter.patch @@ -26,17 +26,33 @@ 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. -It is resolved once, at the first done-commit pass. +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=" once at start, and -"gn_mailbox_hold: held=N replaced=M over K vblanks (total ...)" about every -600 vblanks while holds happen. +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..987d54c 100644 +index 81f5cd8..df657d1 100644 --- a/src/steamcompmgr.cpp +++ b/src/steamcompmgr.cpp @@ -101,6 +101,10 @@ @@ -50,7 +66,7 @@ index 81f5cd8..987d54c 100644 #include "wlr_begin.hpp" #include "wlr/types/wlr_pointer_constraints_v1.h" #include "wlr_end.hpp" -@@ -8216,6 +8220,133 @@ bool handle_done_commit( steamcompmgr_win_t *w, xwayland_ctx_t *ctx, uint64_t co +@@ -8216,6 +8220,212 @@ bool handle_done_commit( steamcompmgr_win_t *w, xwayland_ctx_t *ctx, uint64_t co return false; } @@ -69,11 +85,41 @@ index 81f5cd8..987d54c 100644 +// 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). -+// Resolved once, at the first done-commit pass. ++// 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() +{ @@ -90,16 +136,17 @@ index 81f5cd8..987d54c 100644 + bEnabled = cv_gn_mailbox_hold != 0; + pszSource = "env"; + } ++ s_nGnMailboxMaxCarry = std::max( 0, (int)cv_gn_mailbox_hold_max_carry ); +#if defined(__ANDROID__) -+ char szProp[ PROP_VALUE_MAX ] = {}; -+ if ( __system_property_get( "debug.gamescope.gn_mailbox_hold", szProp ) > 0 && -+ ( szProp[0] == '0' || szProp[0] == '1' ) ) ++ if ( std::optional oProp = gn_int_property( "debug.gamescope.gn_mailbox_hold" ); oProp && ( *oProp == 0 || *oProp == 1 ) ) + { -+ bEnabled = szProp[0] == '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", bEnabled ? 1 : 0, pszSource ); ++ xwm_log.infof( "gn_mailbox_hold=%d source=%s max_carry=%d", bEnabled ? 1 : 0, pszSource, s_nGnMailboxMaxCarry ); + return bEnabled; + }(); + return s_bEnabled; @@ -116,17 +163,23 @@ index 81f5cd8..987d54c 100644 + return true; +} + -+static uint64_t s_ulGnHoldCount = 0; -+static uint64_t s_ulGnHoldReplacedCount = 0; ++// 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". ++// 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. `held_index` maps a window seq to its held entry in `held` for -+// this pass. Returns false when the entry goes on as today. ++// 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 ) +{ @@ -137,12 +190,25 @@ index 81f5cd8..987d54c 100644 + 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( entry ); -+ s_ulGnHoldCount++; ++ held.push_back( heldEntry ); + return true; + } + @@ -158,33 +224,62 @@ index 81f5cd8..987d54c 100644 + break; + } + } -+ replaced = entry; -+ s_ulGnHoldReplacedCount++; ++ replaced = heldEntry; ++ s_ulGnReplaced++; + return true; +} + -+// One line per ~600 cycles while holds happen, to confirm the hold is active. ++// 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_ulLastHold = 0, s_ulLastReplaced = 0; ++ static uint64_t s_ulLast[ 4 ] = {}; + if ( vblank_idx < s_ulLastReportIdx + 600 ) + return; -+ if ( s_ulGnHoldCount != s_ulLastHold || s_ulGnHoldReplacedCount != s_ulLastReplaced ) ++ 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: held=%" PRIu64 " replaced=%" PRIu64 " over %" PRIu64 " vblanks (total held=%" PRIu64 " replaced=%" PRIu64 ")", -+ s_ulGnHoldCount - s_ulLastHold, s_ulGnHoldReplacedCount - s_ulLastReplaced, -+ vblank_idx - s_ulLastReportIdx, s_ulGnHoldCount, s_ulGnHoldReplacedCount ); ++ 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; -+ s_ulLastHold = s_ulGnHoldCount; -+ s_ulLastReplaced = s_ulGnHoldReplacedCount; ++ 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 +8364,10 @@ void handle_done_commits_xwayland( xwayland_ctx_t *ctx, bool vblank, uint64_t vb +@@ -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 ); @@ -195,7 +290,7 @@ index 81f5cd8..987d54c 100644 uint64_t now = get_time_in_nanos(); // very fast loop yes -@@ -8257,6 +8392,11 @@ void handle_done_commits_xwayland( xwayland_ctx_t *ctx, bool vblank, uint64_t vb +@@ -8257,6 +8471,11 @@ void handle_done_commits_xwayland( xwayland_ctx_t *ctx, bool vblank, uint64_t vb continue; } @@ -207,12 +302,12 @@ index 81f5cd8..987d54c 100644 if (!entry.earliestPresentTime) { entry.earliestPresentTime = next_refresh_time; -@@ -8273,10 +8413,15 @@ void handle_done_commits_xwayland( xwayland_ctx_t *ctx, bool vblank, uint64_t vb +@@ -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. -+ entry_win->gnMailboxLatchedForPaint = gn_mailbox_paint_id( vblank_idx ); ++ gn_mailbox_hold_latched( entry_win, entry, vblank_idx ); } } @@ -223,7 +318,7 @@ index 81f5cd8..987d54c 100644 } void handle_done_commits_xdg( bool vblank, uint64_t vblank_idx ) -@@ -8295,6 +8440,10 @@ 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 ); @@ -234,7 +329,7 @@ index 81f5cd8..987d54c 100644 uint64_t now = get_time_in_nanos(); // very fast loop yes -@@ -8318,6 +8467,11 @@ void handle_done_commits_xdg( bool vblank, uint64_t vblank_idx ) +@@ -8318,6 +8546,11 @@ void handle_done_commits_xdg( bool vblank, uint64_t vblank_idx ) commits_before_their_time.push_back( entry ); continue; } @@ -246,27 +341,44 @@ index 81f5cd8..987d54c 100644 if (!entry.earliestPresentTime) { -@@ -8335,6 +8489,8 @@ void handle_done_commits_xdg( bool vblank, uint64_t vblank_idx ) +@@ -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. -+ entry_win->gnMailboxLatchedForPaint = gn_mailbox_paint_id( vblank_idx ); ++ gn_mailbox_hold_latched( entry_win, entry, vblank_idx ); } } diff --git a/src/steamcompmgr_shared.hpp b/src/steamcompmgr_shared.hpp -index aea260b..6bdfb54 100644 +index aea260b..5da643d 100644 --- a/src/steamcompmgr_shared.hpp +++ b/src/steamcompmgr_shared.hpp -@@ -164,6 +164,10 @@ struct steamcompmgr_win_t { +@@ -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); see gn_mailbox_hold_entry. ++ // 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