Graphics: Attempt to provide more consistent frame timings

We now actively track how far behind / in front of an
ideal timestep we are during each frame, and try to
catch up / delay approximate this timing.

Therefore we use more precise timers and sleep functions
(nanosleep if available). We also delay **before** the
final buffer swap so the frame displays at more consistent
points in time.

Not only should this provide a somewhat more consistent
looking map scrolling at lower frame rates, it also
guarantees that we don't fall out of sync eg. with the
Audio during longer cutscenes.

'Graphics.frameReset()' now finally has a function, in
that it resets the ideal timestep approximation, which I
beliefe was also its job in the original RMXP engine.

I'm not sure how well this will work when the frame rate
is set to the monitor refresh rate and vsync is turned on.
Very likely unnecessary frame skips will occur here and there
due to imprecise timers. In the future we should probably
check if the frame rate is equal to or higher than the
monitor rate, and disable frame skip accordingly.

These changes currently break the F2 FPS display (it shows
a value that's slightly too high).
This commit is contained in:
Jonas Kulla 2013-10-06 14:11:50 +02:00
parent 906f9fae17
commit 58bd60a70f
3 changed files with 101 additions and 16 deletions

View File

@ -394,7 +394,7 @@ void EventThread::notifyFrame()
static uint64_t freq = SDL_GetPerformanceFrequency(); static uint64_t freq = SDL_GetPerformanceFrequency();
int32_t currFPS = freq / diff; double currFPS = (double) freq / diff;
fps.acc += currFPS; fps.acc += currFPS;
++fps.accDiv; ++fps.accDiv;

View File

@ -99,7 +99,7 @@ private:
bool displaying; bool displaying;
bool immInitFlag; bool immInitFlag;
bool immFiniFlag; bool immFiniFlag;
uint64_t acc; double acc;
uint32_t accDiv; uint32_t accDiv;
} fps; } fps;
}; };

View File

@ -38,6 +38,8 @@
#include "SDL_video.h" #include "SDL_video.h"
#include "SDL_timer.h" #include "SDL_timer.h"
#include <time.h>
struct PingPong struct PingPong
{ {
TEXFBO rt[2]; TEXFBO rt[2];
@ -244,32 +246,105 @@ private:
struct FPSLimiter struct FPSLimiter
{ {
unsigned lastTickCount; uint64_t lastTickCount;
unsigned mspf; /* ms per frame */
FPSLimiter(unsigned desiredFPS) /* ticks per frame */
: lastTickCount(SDL_GetTicks()) int64_t tpf;
/* Ticks per second */
const uint64_t tickFreq;
/* Ticks per milisecond */
const uint64_t tickFreqMS;
/* Ticks per nanosecond */
const double tickFreqNS;
/* Data for frame timing adjustment */
struct
{
/* Last tick count */
uint64_t last;
/* How far behind/in front we are for ideal frame timing */
int64_t idealDiff;
bool resetFlag;
} adj;
FPSLimiter(uint16_t desiredFPS)
: lastTickCount(SDL_GetPerformanceCounter()),
tickFreq(SDL_GetPerformanceFrequency()),
tickFreqMS(tickFreq / 1000),
tickFreqNS(tickFreq / 1000000000)
{ {
setDesiredFPS(desiredFPS); setDesiredFPS(desiredFPS);
adj.last = SDL_GetPerformanceCounter();
adj.idealDiff = 0;
adj.resetFlag = false;
} }
void setDesiredFPS(unsigned value) void setDesiredFPS(uint16_t value)
{ {
mspf = 1000 / value; tpf = tickFreq / value;
} }
void delay() void delay()
{ {
unsigned tmpTicks = SDL_GetTicks(); int64_t tickDelta = SDL_GetPerformanceCounter() - lastTickCount;
unsigned tickDelta = tmpTicks - lastTickCount; int64_t toDelay = tpf - tickDelta;
lastTickCount = tmpTicks;
/* Compensate for the last delta
* to the ideal timestep */
toDelay -= adj.idealDiff;
int toDelay = mspf - tickDelta;
if (toDelay < 0) if (toDelay < 0)
toDelay = 0; toDelay = 0;
SDL_Delay(toDelay); delayTicks(toDelay);
lastTickCount = SDL_GetTicks();
uint64_t now = lastTickCount = SDL_GetPerformanceCounter();
int64_t diff = now - adj.last;
adj.last = now;
/* Recalculate our temporal position
* relative to the ideal timestep */
adj.idealDiff = diff - tpf + adj.idealDiff;
if (adj.resetFlag)
{
adj.idealDiff = 0;
adj.resetFlag = false;
}
}
void resetFrameAdjust()
{
adj.resetFlag = true;
}
/* If we're more than a full frame's worth
* of ticks behind the ideal timestep,
* there's no choice but to skip frame(s)
* to catch up */
bool frameSkipRequired() const
{
return adj.idealDiff > tpf;
}
private:
void delayTicks(uint64_t ticks)
{
#ifdef HAVE_NANOSLEEP
struct timespec req;
req.tv_sec = 0;
req.tv_nsec = ticks / tickFreqNS;
while (nanosleep(&req, &req) == -1)
;
#else
SDL_Delay(ticks / tickFreqMS);
#endif
} }
}; };
@ -408,8 +483,8 @@ struct GraphicsPrivate
void swapGLBuffer() void swapGLBuffer()
{ {
SDL_GL_SwapWindow(threadData->window);
fpsLimiter.delay(); fpsLimiter.delay();
SDL_GL_SwapWindow(threadData->window);
++frameCount; ++frameCount;
@ -471,6 +546,16 @@ void Graphics::update()
if (p->frozen) if (p->frozen)
return; return;
if (p->fpsLimiter.frameSkipRequired())
{
/* Skip frame */
p->fpsLimiter.delay();
++p->frameCount;
p->threadData->ethread->notifyFrame();
return;
}
p->checkResize(); p->checkResize();
p->redrawScreen(); p->redrawScreen();
@ -567,7 +652,7 @@ void Graphics::transition(int duration,
void Graphics::frameReset() void Graphics::frameReset()
{ {
p->fpsLimiter.resetFrameAdjust();
} }
#undef RET_IF_DISP #undef RET_IF_DISP