another desperate attempt at an accurate fps limiter

This commit is contained in:
Andrei Alexeyev 2017-10-04 08:07:04 +03:00
parent 7ba1099902
commit 05478cd543
No known key found for this signature in database
GPG key ID: 363707CD4C7FE8A4
17 changed files with 368 additions and 175 deletions

View file

@ -42,6 +42,14 @@ In addition to the variables listed here, those processed by our runtime depende
* **TAISEI_GL_EXT_OVERRIDES** *(default: unset)*: Space-separated list of OpenGL extensions that are assumed to be supported, even if the driver says they aren't. Prefix an extension with `-` to invert this behaviour. Might be used to work around bugs in some weird/ancient/broken drivers, but your chances are slim. Also note that Taisei assumes many extensions to be available on any sane OpenGL 2.1+ implementation and doesn't test for them, so you can't disable code that uses those this way.
### Timing
* **TAISEI_HIRES_TIMER** *(default value: `1`)*: if `1`, try to use the system's high resolution timer to limit the game's framerate. Disabling this is not recommended; it will likely make Taisei run slower or faster than intended and the reported FPS will be less accurate.
* **TAISEI_FRAMELIMITER_SLEEP** *(default value: `10`)*: if over `0`, try to sleep this many milliseconds after every frame if it was processed quickly enough. This reduces CPU usage by having the game spend less time in a busy loop, but may hurt framerate stability if set too high, especially if the high resolution timer is disabled or not available. The default value should be reasonable.
* **TAISEI_FRAMELIMITER_SLEEP_EXACT** *(default value: `1`)*: if `1`, the framerate limiter will either try to sleep the exact amount of time set in `TAISEI_FRAMELIMITER_SLEEP`, or none at all. Mitigates the aforementioned framerate stability issues by effectively making `TAISEI_FRAMELIMITER_SLEEP` do nothing if the value is too high for your system. This should be a safe default.
### Logging
Taisei's logging system currently has four basic levels and works by dispatching messages to a few output handlers. Each handler has a level filter, which is configured by a separate environment variable. All of those variables work the same way: their value looks like an IRC mode string, and represents a modification of the handler's default settings. If this doesn't make sense, take a look at the *Examples* section.

View file

@ -69,6 +69,7 @@ set(SRCs
color.c
difficulty.c
audio_common.c
hirestime.c
menu/menu.c
menu/mainmenu.c
menu/options.c
@ -213,6 +214,7 @@ set(LIBDIRs
)
if(WIN32)
add_definitions(-D__USE_MINGW_ANSI_STDIO)
set(LIBs ${LIBs} -ldxguid -lwinmm)
string(REPLACE "gcc" "windres" CMAKE_RC_COMPILER_INIT ${CMAKE_C_COMPILER})

25
src/compat.h Normal file
View file

@ -0,0 +1,25 @@
/*
* This software is licensed under the terms of the MIT-License
* See COPYING for further information.
* ---
* Copyright (c) 2011-2017, Lukas Weber <laochailan@web.de>.
* Copyright (c) 2012-2017, Andrei Alexeyev <akari@alienslab.net>.
*/
#pragma once
#ifndef __GNUC__ // clang defines this too
#define __attribute__(...)
#define __extension__
#define PRAGMA(p)
#else
#define PRAGMA(p) _Pragma(#p)
#endif
#ifdef __MINGW_PRINTF_FORMAT
#define FORMAT_ATTR __MINGW_PRINTF_FORMAT
#else
#define FORMAT_ATTR printf
#endif
void shut_up_stupid_warning(void);

View file

@ -263,16 +263,18 @@ void credits_preload(void) {
NULL);
}
static bool credits_frame(void *arg) {
events_poll(NULL, 0);
credits_process();
credits_draw();
global.frames++;
SDL_GL_SwapWindow(video.window);
return !credits.end;
}
void credits_loop(void) {
credits_preload();
credits_init();
while(credits.end) {
events_poll(NULL, 0);
credits_process();
credits_draw();
global.frames++;
SDL_GL_SwapWindow(video.window);
limit_frame_rate(&global.lasttime);
}
loop_at_fps(credits_frame, NULL, NULL, FPS);
credits_free();
}

View file

@ -135,29 +135,31 @@ void ending_preload(void) {
preload_resource(RES_BGM, "ending", RESF_OPTIONAL);
}
void ending_loop(void) {
Ending e;
static bool ending_frame(void *arg) {
Ending *e = arg;
ending_preload();
create_ending(&e);
events_poll(NULL, 0);
ending_draw(e);
global.frames++;
SDL_GL_SwapWindow(video.window);
global.frames = 0;
set_ortho();
while(e.pos < e.count-1) {
events_poll(NULL, 0);
ending_draw(&e);
global.frames++;
SDL_GL_SwapWindow(video.window);
limit_frame_rate(&global.lasttime);
if(global.frames >= e.entries[e.pos+1].time)
e.pos++;
if(global.frames == e.entries[e.count-1].time-ENDING_FADE_OUT)
set_transition(TransFadeWhite, ENDING_FADE_OUT, ENDING_FADE_OUT);
if(global.frames >= e->entries[e->pos+1].time) {
e->pos++;
}
if(global.frames == e->entries[e->count-1].time-ENDING_FADE_OUT) {
set_transition(TransFadeWhite, ENDING_FADE_OUT, ENDING_FADE_OUT);
}
return e->pos >= e->count - 1;
}
void ending_loop(void) {
Ending e;
ending_preload();
create_ending(&e);
global.frames = 0;
set_ortho();
loop_at_fps(ending_frame, NULL, &e, FPS);
free_ending(&e);
}

View file

@ -45,6 +45,8 @@
#include "audio.h"
#include "rwops/all.h"
#include "cli.h"
#include "hirestime.h"
#include "log.h"
enum {
// defaults
@ -90,9 +92,9 @@ typedef struct {
Projectile *particles;
int frames;
uint64_t lasttime; // frame limiter
int timer;
int frames; // stage global timer
int timer; // stage event timer (freezes on bosses, dialogs, etc.)
int frameskip;
Boss *boss;

85
src/hirestime.c Normal file
View file

@ -0,0 +1,85 @@
/*
* This software is licensed under the terms of the MIT-License
* See COPYING for further information.
* ---
* Copyright (c) 2011-2017, Lukas Weber <laochailan@web.de>.
* Copyright (c) 2012-2017, Andrei Alexeyev <akari@alienslab.net>.
*/
#include "util.h"
static bool use_hires;
static long double time_current;
static long double time_offset;
static uint64_t prev_hires_time;
static uint64_t prev_hires_freq;
static SDL_mutex *paranoia;
static void time_update(void) {
bool retry;
do {
retry = false;
uint64_t freq = SDL_GetPerformanceFrequency();
uint64_t cntr = SDL_GetPerformanceCounter();
if(freq != prev_hires_freq) {
log_debug("High resolution timer frequency changed: was %"PRIu64", now %"PRIu64". Saved time offset: %.16Lf", prev_hires_freq, freq, time_offset);
time_offset = time_current;
prev_hires_freq = freq;
prev_hires_time = SDL_GetPerformanceCounter();
retry = true;
continue;
}
long double time_new = time_offset + (long double)(cntr - prev_hires_time) / freq;
if(time_new < time_current) {
log_warn("BUG: time went backwards. Was %.16Lf, now %.16Lf. Possible cause: your OS sucks spherical objects. Attempting to correct this...", time_current, time_new);
time_offset = time_current;
time_current = 0;
prev_hires_time = SDL_GetPerformanceCounter();
retry = true;
} else {
time_current = time_new;
}
} while(retry);
}
void time_init(void) {
use_hires = getenvint("TAISEI_HIRES_TIMER", 1);
if(use_hires) {
if(!(paranoia = SDL_CreateMutex())) {
log_warn("Not using the system high resolution timer: SDL_CreateMutex() failed: %s", SDL_GetError());
return;
}
log_info("Using the system high resolution timer");
prev_hires_time = SDL_GetPerformanceCounter();
prev_hires_freq = SDL_GetPerformanceFrequency();
} else {
log_info("Not using the system high resolution timer: disabled by environment");
return;
}
}
void time_shutdown(void) {
if(paranoia) {
SDL_DestroyMutex(paranoia);
paranoia = NULL;
}
}
long double time_get(void) {
if(use_hires) {
SDL_LockMutex(paranoia);
time_update();
long double t = time_current;
SDL_UnlockMutex(paranoia);
return t;
}
return SDL_GetTicks() / 1000.0;
}

13
src/hirestime.h Normal file
View file

@ -0,0 +1,13 @@
/*
* This software is licensed under the terms of the MIT-License
* See COPYING for further information.
* ---
* Copyright (c) 2011-2017, Lukas Weber <laochailan@web.de>.
* Copyright (c) 2012-2017, Andrei Alexeyev <akari@alienslab.net>.
*/
#pragma once
void time_init(void);
void time_shutdown(void);
long double time_get(void);

View file

@ -11,6 +11,7 @@
#include <stdnoreturn.h>
#include <stdbool.h>
#include <SDL.h>
#include "compat.h"
typedef enum LogLevel {
LOG_NONE = 0,
@ -92,9 +93,7 @@ bool log_initialized(void);
//
void _taisei_log(LogLevel lvl, bool is_backtrace, const char *funcname, const char *fmt, ...)
__attribute__((format(printf, 4, 5)));
__attribute__((format(FORMAT_ATTR, 4, 5)));
noreturn void _taisei_log_fatal(LogLevel lvl, const char *funcname, const char *fmt, ...)
__attribute__((format(printf, 3, 4)));
__attribute__((format(FORMAT_ATTR, 3, 4)));

View file

@ -39,6 +39,7 @@ static void taisei_shutdown(void) {
config_uninit();
vfs_uninit();
events_shutdown();
time_shutdown();
log_info("Good bye");
SDL_Quit();
@ -182,6 +183,7 @@ int main(int argc, char **argv) {
log_lib_versions();
init_sdl();
time_init();
init_global(&a);
events_init();
init_fonts();

View file

@ -144,6 +144,27 @@ void menu_logic(MenuData *menu) {
menu->frames++;
}
static bool menu_frame(void *arg) {
MenuData *menu = arg;
menu->logic(menu);
if(menu->state != MS_FadeOut || menu->flags & MF_AlwaysProcessInput) {
assert(menu->input);
menu->input(menu);
} else {
menu_no_input(menu);
}
assert(menu->draw);
menu->draw(menu);
draw_and_update_transition();
SDL_GL_SwapWindow(video.window);
return menu->state != MS_Dead;
}
int menu_loop(MenuData *menu) {
set_ortho();
@ -151,24 +172,8 @@ int menu_loop(MenuData *menu) {
menu->begin(menu);
}
while(menu->state != MS_Dead) {
assert(menu->logic);
menu->logic(menu);
if(menu->state != MS_FadeOut || menu->flags & MF_AlwaysProcessInput) {
assert(menu->input);
menu->input(menu);
} else {
menu_no_input(menu);
}
assert(menu->draw);
menu->draw(menu);
draw_and_update_transition();
SDL_GL_SwapWindow(video.window);
limit_frame_rate(&menu->lasttime);
}
assert(menu->logic != NULL);
loop_at_fps(menu_frame, NULL, menu, FPS);
if(menu->end) {
menu->end(menu);

View file

@ -55,7 +55,6 @@ struct MenuData {
int ecount;
int frames;
uint64_t lasttime;
int state;
int transition_in_time;

View file

@ -185,8 +185,6 @@ static void stage_start(StageInfo *stage) {
global.game_over = 0;
global.shake_view = 0;
fpscounter_reset(&global.fps);
prepare_player_for_next_stage(&global.plr);
if(stage->type == STAGE_SPELL) {
@ -489,6 +487,85 @@ void stage_start_bgm(const char *bgm) {
}
}
typedef struct StageFrameState {
StageInfo *stage;
int transition_delay;
uint16_t last_replay_fps;
} StageFrameState;
static bool stage_fpslimit_condition(void *arg) {
return (global.replaymode != REPLAY_PLAY || !gamekeypressed(KEY_SKIP)) && !global.frameskip;
}
static bool stage_frame(void *arg) {
StageFrameState *fstate = arg;
StageInfo *stage = fstate->stage;
((global.replaymode == REPLAY_PLAY) ? replay_input : stage_input)();
if(global.game_over != GAMEOVER_TRANSITIONING) {
if((!global.boss || boss_is_fleeing(global.boss)) && !global.dialog) {
stage->procs->event();
}
if(stage->type == STAGE_SPELL && !global.boss && global.game_over != GAMEOVER_RESTART) {
stage_finish(GAMEOVER_WIN);
fstate->transition_delay = 60;
}
}
if(!global.timer && stage->type != STAGE_SPELL) {
// must be done here to let the event function start a BGM first
display_stage_title(stage);
}
replay_stage_check_desync(global.replay_stage, global.frames, (tsrand() ^ global.plr.points) & 0xFFFF, global.replaymode);
stage_logic();
if(global.replaymode == REPLAY_RECORD && global.plr.points > progress.hiscore) {
progress.hiscore = global.plr.points;
}
if(fstate->transition_delay) {
--fstate->transition_delay;
}
if(global.frameskip && global.frames % global.frameskip) {
if(!fstate->transition_delay) {
update_transition();
}
return true;
}
fpscounter_update(&global.fps);
if(global.replaymode == REPLAY_RECORD) {
uint16_t replay_fps = (uint16_t)rint(global.fps.fps);
if(replay_fps != fstate->last_replay_fps) {
replay_stage_event(global.replay_stage, global.frames, EV_FPS, replay_fps);
fstate->last_replay_fps = replay_fps;
}
}
tsrand_lock(&global.rand_game);
tsrand_switch(&global.rand_visual);
stage_draw_scene(stage);
tsrand_unlock(&global.rand_game);
tsrand_switch(&global.rand_game);
draw_transition();
if(!fstate->transition_delay) {
update_transition();
}
SDL_GL_SwapWindow(video.window);
return global.game_over <= 0;
}
void stage_loop(StageInfo *stage) {
assert(stage);
assert(stage->procs);
@ -566,68 +643,9 @@ void stage_loop(StageInfo *stage) {
stage->procs->begin();
int transition_delay = 0;
while(global.game_over <= 0) {
if(global.game_over != GAMEOVER_TRANSITIONING) {
if((!global.boss || boss_is_fleeing(global.boss)) && !global.dialog) {
stage->procs->event();
}
if(stage->type == STAGE_SPELL && !global.boss && global.game_over != GAMEOVER_RESTART) {
stage_finish(GAMEOVER_WIN);
transition_delay = 60;
}
}
if(!global.timer && stage->type != STAGE_SPELL) {
// must be done here to let the event function start a BGM first
display_stage_title(stage);
}
((global.replaymode == REPLAY_PLAY) ? replay_input : stage_input)();
replay_stage_check_desync(global.replay_stage, global.frames, (tsrand() ^ global.plr.points) & 0xFFFF, global.replaymode);
stage_logic();
if(global.replaymode == REPLAY_RECORD && global.plr.points > progress.hiscore) {
progress.hiscore = global.plr.points;
}
if(transition_delay) {
--transition_delay;
}
if(global.frameskip && global.frames % global.frameskip) {
if(!transition_delay) {
update_transition();
}
continue;
}
if(fpscounter_update(&global.fps) && global.replaymode == REPLAY_RECORD) {
replay_stage_event(global.replay_stage, global.frames, EV_FPS, global.fps.show_fps);
}
tsrand_lock(&global.rand_game);
tsrand_switch(&global.rand_visual);
stage_draw_scene(stage);
tsrand_unlock(&global.rand_game);
tsrand_switch(&global.rand_game);
draw_transition();
if(!transition_delay) {
update_transition();
}
SDL_GL_SwapWindow(video.window);
if(global.replaymode == REPLAY_PLAY && gamekeypressed(KEY_SKIP)) {
global.lasttime = SDL_GetTicks();
} else {
limit_frame_rate(&global.lasttime);
}
}
StageFrameState fstate = { .stage = stage };
fpscounter_reset(&global.fps);
loop_at_fps(stage_frame, stage_fpslimit_condition, &fstate, FPS);
if(global.game_over == GAMEOVER_RESTART && global.stage->type != STAGE_SPELL) {
stop_bgm(true);

View file

@ -517,7 +517,12 @@ void stage_draw_hud(void) {
glPopMatrix();
snprintf(buf, sizeof(buf), "%i fps", global.fps.show_fps);
#ifdef DEBUG
snprintf(buf, sizeof(buf), "%.16f fps", global.fps.fps);
#else
snprintf(buf, sizeof(buf), "%.2f fps", global.fps.fps);
#endif
draw_text(AL_Right, SCREEN_W, SCREEN_H - 0.5 * stringheight(buf, _fonts.standard), buf, _fonts.standard);
if(global.boss) {

View file

@ -302,41 +302,80 @@ float smoothreclamp(float x, float old_min, float old_max, float new_min, float
// gl/video utils
//
void limit_frame_rate(uint64_t *lasttime) {
if(global.frameskip) {
return;
}
double passed = (double)(SDL_GetPerformanceCounter() - *lasttime) / SDL_GetPerformanceFrequency();
double delay = max(0, 1.0 / FPS - passed);
int32_t delay_ms = (int32_t)rint(delay * 1000.0);
if(delay_ms > 0) {
SDL_Delay(delay_ms);
}
*lasttime = SDL_GetPerformanceCounter();
}
void fpscounter_reset(FPSCounter *fps) {
fps->show_fps = FPS;
fps->fps = 0;
fps->fpstime = SDL_GetTicks();
}
long double frametime = 1.0 / FPS;
const int log_size = sizeof(fps->frametimes)/sizeof(long double);
bool fpscounter_update(FPSCounter *fps) {
bool updated = false;
if(SDL_GetTicks() > fps->fpstime+1000) {
fps->show_fps = fps->fps;
fps->fps = 0;
fps->fpstime = SDL_GetTicks();
updated = true;
} else {
fps->fps++;
for(int i = 0; i < log_size; ++i) {
fps->frametimes[i] = frametime;
}
return updated;
fps->fps = 1.0 / frametime;
fps->last_update_time = time_get();
}
void fpscounter_update(FPSCounter *fps) {
const int log_size = sizeof(fps->frametimes)/sizeof(long double);
double frametime = time_get() - fps->last_update_time;
memmove(fps->frametimes, fps->frametimes + 1, (log_size - 1) * sizeof(long double));
fps->frametimes[log_size - 1] = frametime;
long double avg = 0.0;
for(int i = 0; i < log_size; ++i) {
avg += fps->frametimes[i];
}
fps->fps = 1.0 / (avg / log_size);
fps->last_update_time = time_get();
}
void loop_at_fps(bool (*frame_func)(void*), bool (*limiter_cond_func)(void*), void *arg, uint32_t fps) {
assert(frame_func != NULL);
assert(fps > 0);
long double real_time = time_get();
long double next_frame_time = real_time;
long double target_frame_time = ((long double)1.0) / fps;
int32_t delay = getenvint("TAISEI_FRAMELIMITER_SLEEP", 10);
bool exact_delay = getenvint("TAISEI_FRAMELIMITER_SLEEP_EXACT", 1);
while(true) {
real_time = time_get();
if(real_time < next_frame_time) {
continue;
}
if(!frame_func(arg)) {
return;
}
if(!limiter_cond_func || limiter_cond_func(arg)) {
next_frame_time = real_time + target_frame_time;
if(delay > 0) {
int32_t realdelay = delay;
int32_t mindelay = (int32_t)(1000 * (next_frame_time - time_get()));
if(realdelay > mindelay) {
if(exact_delay) {
log_debug("Delay of %i ignored. Minimum is %i but TAISEI_FRAMELIMITER_SLEEP_EXACT is active", realdelay, mindelay);
realdelay = 0;
} else {
log_debug("Delay reduced from %i to %i", realdelay, mindelay);
realdelay = mindelay;
}
}
if(realdelay > 0) {
SDL_Delay(realdelay);
}
}
}
}
}
void set_ortho_ex(float w, float h) {
@ -568,7 +607,7 @@ void _ts_assert_fail(const char *cond, const char *func, const char *file, int l
int getenvint(const char *v, int defaultval) {
char *e = getenv(v);
if(e) {
if(e && *e) {
return atoi(e);
}

View file

@ -15,21 +15,10 @@
#include <zlib.h> // compiling under mingw may fail without this...
#include <png.h>
#include <SDL.h>
#include "log.h"
#include "hashtable.h"
#include "vfs/public.h"
//
// compatibility
//
#ifndef __GNUC__ // clang defines this too
#define __attribute__(...)
#define __extension__
#define PRAGMA(p)
#else
#define PRAGMA(p) _Pragma(#p)
#endif
#include "log.h"
#include "compat.h"
//
// string utils
@ -48,7 +37,7 @@ bool strstartswith_any(const char *s, const char **earray) __attribute__((pure))
void stralloc(char **dest, const char *src);
char* strjoin(const char *first, ...) __attribute__((sentinel));
char* vstrfmt(const char *fmt, va_list args);
char* strfmt(const char *fmt, ...) __attribute__((format(printf, 1, 2)));
char* strfmt(const char *fmt, ...) __attribute__((format(FORMAT_ATTR, 1, 2)));
void strip_trailing_slashes(char *buf);
char* strtok_r(char *str, const char *delim, char **nextp);
char* strappend(char **dst, char *src);
@ -101,14 +90,14 @@ float smoothreclamp(float x, float old_min, float old_max, float new_min, float
//
typedef struct {
int fpstime; // frame counter
int fps;
int show_fps;
long double frametimes[120]; // size = number of frames to average
double fps; // average fps over the last X frames
long double last_update_time; // internal; last time the average was recalculated
} FPSCounter;
void limit_frame_rate(uint64_t *lasttime);
void loop_at_fps(bool (*frame_func)(void*), bool (*limiter_cond_func)(void*), void *arg, uint32_t fps);
void fpscounter_reset(FPSCounter *fps);
bool fpscounter_update(FPSCounter *fps);
void fpscounter_update(FPSCounter *fps);
void set_ortho(void);
void set_ortho_ex(float w, float h);
void colorfill(float r, float g, float b, float a);
@ -129,10 +118,10 @@ void png_init_rwops_read(png_structp png, SDL_RWops *rwops);
void png_init_rwops_write(png_structp png, SDL_RWops *rwops);
char* SDL_RWgets(SDL_RWops *rwops, char *buf, size_t bufsize);
size_t SDL_RWprintf(SDL_RWops *rwops, const char* fmt, ...) __attribute__((format(printf, 2, 3)));
size_t SDL_RWprintf(SDL_RWops *rwops, const char* fmt, ...) __attribute__((format(FORMAT_ATTR, 2, 3)));
// This is for the very few legitimate uses for printf/fprintf that shouldn't be replaced with log_*
void tsfprintf(FILE *out, const char *restrict fmt, ...) __attribute__((format(printf, 2, 3)));
void tsfprintf(FILE *out, const char *restrict fmt, ...) __attribute__((format(FORMAT_ATTR, 2, 3)));
//
// misc utils
@ -199,5 +188,3 @@ int sprintf(char *, const char*, ...) __attribute__((deprecated(
"Use snprintf or strfmt instead")));
PRAGMA(GCC diagnostic pop)

View file

@ -102,7 +102,7 @@ bool vfs_mount(VFSNode *root, const char *mountpoint, VFSNode *subtree);
const char* vfs_iter(VFSNode *node, void **opaque);
void vfs_iter_stop(VFSNode *node, void **opaque);
void vfs_set_error(char *fmt, ...) __attribute__((format(printf, 1, 2)));
void vfs_set_error(char *fmt, ...) __attribute__((format(FORMAT_ATTR, 1, 2)));
void vfs_set_error_from_sdl(void);
void vfs_print_tree_recurse(SDL_RWops *dest, VFSNode *root, char *prefix, const char *name);