From c5ab6f388b20aae388d543b9f1879471e3b17df9 Mon Sep 17 00:00:00 2001 From: Joshua Ashton Date: Tue, 25 Jan 2022 07:14:18 +0000 Subject: [PATCH] vblankmanager: Implement FPS limiter by strategic buffer withholding --- src/steamcompmgr.cpp | 81 ++++++++++++++++++- src/vblankmanager.cpp | 184 ++++++++++++++++++++++++++++++++++++++++++ src/vblankmanager.hpp | 8 ++ src/xwayland_ctx.hpp | 2 + 4 files changed, 274 insertions(+), 1 deletion(-) diff --git a/src/steamcompmgr.cpp b/src/steamcompmgr.cpp index b9546e7..a8b51dd 100644 --- a/src/steamcompmgr.cpp +++ b/src/steamcompmgr.cpp @@ -42,6 +42,7 @@ #include #include #include +#include #include #include @@ -224,6 +225,46 @@ std::mutex g_SteamCompMgrXWaylandServerMutex; uint64_t g_SteamCompMgrVBlankTime = 0; +std::mutex g_FrameLimitCommitsMutex; +std::queue< std::shared_ptr > g_FrameLimitCommits; + +static int g_nSteamCompMgrTargetFPS = 0; + +bool steamcompmgr_window_should_limit_fps( win *w ) +{ + return g_nSteamCompMgrTargetFPS != 0 && !w->isSteam && w->appID != 769 && !w->isOverlay && !w->isExternalOverlay; +} + +void steamcompmgr_fpslimit_add_commit( std::shared_ptr commit ) +{ + std::unique_lock lock(g_FrameLimitCommitsMutex); + g_FrameLimitCommits.push( commit ); +} + +void steamcompmgr_fpslimit_release_commit() +{ + std::unique_lock lock(g_FrameLimitCommitsMutex); + if ( !g_FrameLimitCommits.empty() ) + g_FrameLimitCommits.pop(); +} + + +void steamcompmgr_fpslimit_release_all() +{ + std::unique_lock lock(g_FrameLimitCommitsMutex); + g_FrameLimitCommits = std::queue< std::shared_ptr >(); +} + +void steamcompmgr_set_target_fps( int nTarget ) +{ + if ( g_nSteamCompMgrTargetFPS != nTarget ) + { + g_nSteamCompMgrTargetFPS = nTarget; + fpslimit_set_target( nTarget ); + steamcompmgr_fpslimit_release_all(); + } +} + enum HeldCommitTypes_t { HELD_COMMIT_BASE, @@ -408,7 +449,10 @@ retry: nudge_steamcompmgr(); if ( entry.mangoapp_nudge ) + { + fpslimit_mark_frame(); mangoapp_update(); + } goto retry; } @@ -2438,6 +2482,9 @@ determine_and_apply_focus() hasRepaint = true; } + if ( previous_focus.focusWindow != global_focus.focusWindow ) + steamcompmgr_fpslimit_release_all(); + // Backchannel to Steam unsigned long focusedWindow = 0; unsigned long focusedAppId = 0; @@ -3668,6 +3715,10 @@ handle_property_notify(xwayland_ctx_t *ctx, XPropertyEvent *ev) } } } + if ( ev->atom == ctx->atoms.gamescopeFPSLimit ) + { + steamcompmgr_set_target_fps( get_prop( ctx, ctx->root, ctx->atoms.gamescopeFPSLimit, 0 ) ); + } } static int @@ -3882,6 +3933,10 @@ void handle_done_commits( xwayland_ctx_t *ctx ) if ( w == global_focus.focusWindow && !w->isSteamStreamingClient ) { g_HeldCommits[ HELD_COMMIT_BASE ] = w->commit_queue[ j ]; + if ( steamcompmgr_window_should_limit_fps( w ) ) + { + steamcompmgr_fpslimit_add_commit( w->commit_queue[ j ] ); + } hasRepaint = true; } @@ -4375,6 +4430,8 @@ xwayland_ctx_t g_ctx; static bool setup_error_handlers = false; +static int g_nVBlankCount = 0; + void init_xwayland_ctx(gamescope_xwayland_server_t *xwayland_server) { assert(!xwayland_server->ctx); @@ -4518,6 +4575,7 @@ void init_xwayland_ctx(gamescope_xwayland_server_t *xwayland_server) ctx->atoms.gamescopeColorGain = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_GAIN", false ); ctx->atoms.gamescopeColorLinearGainBlend = XInternAtom( ctx->dpy, "GAMESCOPE_COLOR_LINEARGAIN_BLEND", false ); ctx->atoms.gamescopeXWaylandModeControl = XInternAtom( ctx->dpy, "GAMESCOPE_XWAYLAND_MODE_CONTROL", false ); + ctx->atoms.gamescopeFPSLimit = XInternAtom( ctx->dpy, "GAMESCOPE_FPS_LIMIT", false ); ctx->root_width = DisplayWidth(ctx->dpy, ctx->scr); ctx->root_height = DisplayHeight(ctx->dpy, ctx->scr); @@ -4649,6 +4707,8 @@ steamcompmgr_main(int argc, char **argv) int vblankFD = vblank_init(); assert( vblankFD >= 0 ); + fpslimit_init(); + std::unique_lock xwayland_server_guard(g_SteamCompMgrXWaylandServerMutex); // Initialize any xwayland ctxs we have @@ -4863,13 +4923,19 @@ steamcompmgr_main(int argc, char **argv) // Ask for a new surface every vblank if ( vblank == true ) { + bool bFocusWindowFrameCallbacks = fpslimit_use_frame_callbacks_for_focus_window( g_nSteamCompMgrTargetFPS, g_nVBlankCount++ ); + { gamescope_xwayland_server_t *server = NULL; for (size_t i = 0; (server = wlserver_get_xwayland_server(i)); i++) { for (win *w = server->ctx->list; w; w = w->next) { - if ( w->surface.wlr != nullptr ) + bool bSendCallback = w->surface.wlr != nullptr; + if ( w == global_focus.focusWindow && steamcompmgr_window_should_limit_fps( w ) && !bFocusWindowFrameCallbacks ) + bSendCallback = false; + + if ( bSendCallback ) { // Acknowledge commit once. wlserver_lock(); @@ -4916,6 +4982,19 @@ steamcompmgr_main(int argc, char **argv) finish_drm( &g_DRM ); } +void steamcompmgr_send_frame_done_to_focus_window() +{ + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + + if ( global_focus.focusWindow && global_focus.focusWindow->surface.wlr ) + { + wlserver_lock(); + wlserver_send_frame_done( global_focus.focusWindow->surface.wlr , &now ); + wlserver_unlock(); + } +} + gamescope_xwayland_server_t *steamcompmgr_get_focused_server() { if (global_focus.inputFocusWindow != nullptr) diff --git a/src/vblankmanager.cpp b/src/vblankmanager.cpp index e3b58e6..5f2898c 100644 --- a/src/vblankmanager.cpp +++ b/src/vblankmanager.cpp @@ -1,9 +1,11 @@ // Try to figure out when vblank is and notify steamcompmgr to render some time before it +#include #include #include #include #include +#include #include #include @@ -42,6 +44,8 @@ uint64_t g_uVBlankRateOfDecayPercentage = g_uDefaultVBlankRateOfDecayPercentage; const uint64_t g_uVBlankRateOfDecayMax = 100; +static std::atomic g_uRollingMaxDrawTime = { g_uStartingDrawTime }; + //#define VBLANK_DEBUG void vblankThreadRun( void ) @@ -72,6 +76,8 @@ void vblankThreadRun( void ) // Clamp our max time to half of the vblank if we can. rollingMaxDrawTime = std::min( rollingMaxDrawTime + g_uVblankDrawBufferRedZoneNS, nsecInterval / 2 ) - g_uVblankDrawBufferRedZoneNS; + g_uRollingMaxDrawTime = rollingMaxDrawTime; + uint64_t offset = rollingMaxDrawTime + g_uVblankDrawBufferRedZoneNS; #ifdef VBLANK_DEBUG @@ -146,3 +152,181 @@ void vblank_mark_possible_vblank( uint64_t nanos ) { g_lastVblank = nanos; } + +// fps limit manager + +static std::mutex g_TargetFPSMutex; +static std::condition_variable g_TargetFPSCondition; +static int g_nFpsLimitTargetFPS = 0; + +void steamcompmgr_fpslimit_release_commit(); +void steamcompmgr_fpslimit_release_all(); +void steamcompmgr_send_frame_done_to_focus_window(); + +// Dump some stats. +//#define FPS_LIMIT_DEBUG + +// Probably way too low, but a starting point. +uint64_t g_uFPSLimiterRedZoneNS = 700'000; + +void fpslimitThreadRun( void ) +{ + pthread_setname_np( pthread_self(), "gamescope-fps" ); + + uint64_t lastCommitReleased = get_time_in_nanos(); + const uint64_t range = g_uVBlankRateOfDecayMax; + uint64_t rollingMaxFrameTime = g_uStartingDrawTime; + uint64_t vblank = 0; + while ( true ) + { + int nTargetFPS; + uint64_t targetInterval; + bool no_frame = false; + + { + std::unique_lock lock( g_TargetFPSMutex ); + nTargetFPS = g_nFpsLimitTargetFPS; + if ( nTargetFPS == 0 ) + { + g_TargetFPSCondition.wait(lock); + } + else + { + targetInterval = 1'000'000'000ul / nTargetFPS; + auto wait_time = std::chrono::nanoseconds(int64_t(lastCommitReleased + targetInterval) - get_time_in_nanos()); + if ( wait_time > std::chrono::nanoseconds(0) ) + { + no_frame = g_TargetFPSCondition.wait_for(lock, std::chrono::nanoseconds(wait_time)) == std::cv_status::timeout; + } + else + { + no_frame = true; + } + } + nTargetFPS = g_nFpsLimitTargetFPS; + } + + if ( nTargetFPS ) + { + targetInterval = 1'000'000'000ul / nTargetFPS; + + // Check if we are unaligned or not, as to whether + // we call frame callbacks from this thread instead of steamcompmgr based + // on vblank count. + bool useFrameCallbacks = fpslimit_use_frame_callbacks_for_focus_window( nTargetFPS, 0 ); + + uint64_t t0 = lastCommitReleased; + uint64_t t1 = get_time_in_nanos(); + + // Not the actual frame time of the game + // this is the time of the amount of work a 'frame' has done. + uint64_t frameTime = t1 - t0; + // If we didn't get a frame, set our frame time as the target interval. + if ( no_frame || !frameTime ) + { +#ifdef FPS_LIMIT_DEBUG + fprintf( stderr, "no frame\n" ); +#endif + frameTime = targetInterval; + } + + const int refresh = g_nNestedRefresh ? g_nNestedRefresh : g_nOutputRefresh; + const uint64_t vblankInterval = 1'000'000'000ul / refresh; + + // Currently, + // Only affect rolling max frame time by 2% + // Tends to be much more varied than the vblank timings. + // Try to be much more defensive about it. + // + // Do we want something better here? Right now, because this moves around all the time + // sometimes we can see judder in the mangoapp frametime graph when gpu clocks are changing around + // in the downtime when we aren't rendering as it measures done->done time, + // rather than present->present time, and done->done time changes as we move buffers around. + // Maybe we want to tweak this alpha value to like 99.something% or change this rolling max to something even more defensive + // to keep a more consistent latency. However, I also cannot feel this judder given how small it is, so maybe it doesn't matter? + // We can tune this later by tweaking alpha + range anyway... + const uint64_t alpha = 98; + rollingMaxFrameTime = ( ( alpha * std::max( rollingMaxFrameTime, frameTime ) ) + ( range - alpha ) * frameTime ) / range; + + rollingMaxFrameTime = std::min( rollingMaxFrameTime + g_uFPSLimiterRedZoneNS, targetInterval ) - g_uFPSLimiterRedZoneNS; + + int64_t targetPoint; + int64_t sleepyTime = targetInterval; + if ( refresh % nTargetFPS == 0 ) + { + sleepyTime -= rollingMaxFrameTime; + sleepyTime -= g_uRollingMaxDrawTime.load(); + sleepyTime -= g_uVblankDrawBufferRedZoneNS; + sleepyTime -= g_uFPSLimiterRedZoneNS; + + vblank = g_lastVblank; + while ( vblank < t1 ) + vblank += vblankInterval; + targetPoint = vblank + sleepyTime; + } + else + { + sleepyTime -= frameTime; + targetPoint = t1 + sleepyTime; + } + + +#ifdef FPS_LIMIT_DEBUG + fprintf( stderr, "Sleeping from %lu to %ld (%ld - %.2fms) to reach %d fps - vblank: %lu sleepytime: %.2fms rollingMaxFrameTime: %.2fms frametime: %.2fms\n", t1, targetPoint, targetPoint - t1, (targetPoint - t1) / 1'000'000.0, nTargetFPS, vblank, sleepyTime / 1'000'000.0, rollingMaxFrameTime / 1'000'000.0, frameTime / 1'000'000.0 ); +#endif + + + sleep_until_nanos( targetPoint ); + lastCommitReleased = get_time_in_nanos(); + steamcompmgr_fpslimit_release_commit(); + + // If we aren't vblank aligned, nudge ourselves to process done commits now. + if ( !useFrameCallbacks ) + { + steamcompmgr_send_frame_done_to_focus_window(); + nudge_steamcompmgr(); + } + } + } +} + +void fpslimit_init( void ) +{ + std::thread fpslimitThread( fpslimitThreadRun ); + fpslimitThread.detach(); +} + +void fpslimit_mark_frame( void ) +{ + g_TargetFPSCondition.notify_all(); +} + +bool fpslimit_use_frame_callbacks_for_focus_window( int nTargetFPS, int nVBlankCount ) +{ + if ( !nTargetFPS ) + return true; + + const int refresh = g_nNestedRefresh ? g_nNestedRefresh : g_nOutputRefresh; + if ( refresh % nTargetFPS == 0 ) + { + // Aligned, limit based on vblank count. + return nVBlankCount % ( refresh / nTargetFPS ); + } + else + { + // Unaligned from VBlank, never use frame callbacks on SteamCompMgr thread. + // call them from fpslimit + return false; + } +} + +// Called from steamcompmgr thread +void fpslimit_set_target( int nTargetFPS ) +{ + { + std::unique_lock lock(g_TargetFPSMutex); + g_nFpsLimitTargetFPS = nTargetFPS; + } + + g_TargetFPSCondition.notify_all(); +} diff --git a/src/vblankmanager.hpp b/src/vblankmanager.hpp index 84cbdc2..864d5e6 100644 --- a/src/vblankmanager.hpp +++ b/src/vblankmanager.hpp @@ -11,3 +11,11 @@ const unsigned int g_uDefaultVBlankRateOfDecayPercentage = 93; extern uint64_t g_uVblankDrawBufferRedZoneNS; extern uint64_t g_uVBlankRateOfDecayPercentage; + +void fpslimit_init( void ); + +void fpslimit_mark_frame( void ); + +bool fpslimit_use_frame_callbacks_for_focus_window( int nTargetFPS, int nVBlankCount ); + +void fpslimit_set_target( int nTargetFPS ); diff --git a/src/xwayland_ctx.hpp b/src/xwayland_ctx.hpp index 894c055..d99461a 100644 --- a/src/xwayland_ctx.hpp +++ b/src/xwayland_ctx.hpp @@ -121,5 +121,7 @@ struct xwayland_ctx_t Atom gamescopeColorLinearGainBlend; Atom gamescopeXWaylandModeControl; + + Atom gamescopeFPSLimit; } atoms; };