vblankmanager: Implement FPS limiter by strategic buffer withholding
This commit is contained in:
parent
9fa6fcd9ac
commit
c5ab6f388b
4 changed files with 274 additions and 1 deletions
|
@ -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)
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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 );
|
||||
|
|
|
@ -121,5 +121,7 @@ struct xwayland_ctx_t
|
|||
Atom gamescopeColorLinearGainBlend;
|
||||
|
||||
Atom gamescopeXWaylandModeControl;
|
||||
|
||||
Atom gamescopeFPSLimit;
|
||||
} atoms;
|
||||
};
|
||||
|
|
Loading…
Reference in a new issue