vblankmanager: Implement FPS limiter by strategic buffer withholding

This commit is contained in:
Joshua Ashton 2022-01-25 07:14:18 +00:00 committed by Joshie
parent 9fa6fcd9ac
commit c5ab6f388b
4 changed files with 274 additions and 1 deletions

View file

@ -42,6 +42,7 @@
#include <iostream>
#include <fstream>
#include <string>
#include <queue>
#include <assert.h>
#include <stdlib.h>
@ -224,6 +225,46 @@ std::mutex g_SteamCompMgrXWaylandServerMutex;
uint64_t g_SteamCompMgrVBlankTime = 0;
std::mutex g_FrameLimitCommitsMutex;
std::queue< std::shared_ptr<commit_t> > 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_t> commit )
{
std::unique_lock<std::mutex> lock(g_FrameLimitCommitsMutex);
g_FrameLimitCommits.push( commit );
}
void steamcompmgr_fpslimit_release_commit()
{
std::unique_lock<std::mutex> lock(g_FrameLimitCommitsMutex);
if ( !g_FrameLimitCommits.empty() )
g_FrameLimitCommits.pop();
}
void steamcompmgr_fpslimit_release_all()
{
std::unique_lock<std::mutex> lock(g_FrameLimitCommitsMutex);
g_FrameLimitCommits = std::queue< std::shared_ptr<commit_t> >();
}
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<std::mutex> 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)

View file

@ -1,9 +1,11 @@
// Try to figure out when vblank is and notify steamcompmgr to render some time before it
#include <mutex>
#include <thread>
#include <vector>
#include <chrono>
#include <atomic>
#include <condition_variable>
#include <assert.h>
#include <fcntl.h>
@ -42,6 +44,8 @@ uint64_t g_uVBlankRateOfDecayPercentage = g_uDefaultVBlankRateOfDecayPercentage;
const uint64_t g_uVBlankRateOfDecayMax = 100;
static std::atomic<uint64_t> 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<std::mutex> 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<std::mutex> lock(g_TargetFPSMutex);
g_nFpsLimitTargetFPS = nTargetFPS;
}
g_TargetFPSCondition.notify_all();
}

View file

@ -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 );

View file

@ -121,5 +121,7 @@ struct xwayland_ctx_t
Atom gamescopeColorLinearGainBlend;
Atom gamescopeXWaylandModeControl;
Atom gamescopeFPSLimit;
} atoms;
};