-
-
Save nielsmh/2a800f795261bedc589995385ec13964 to your computer and use it in GitHub Desktop.
OpenTTD Framerate GUI with timestamp-bucketized FPS calculation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* $Id$ */ | |
/* | |
* This file is part of OpenTTD. | |
* OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2. | |
* OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. | |
* See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <http://www.gnu.org/licenses/>. | |
*/ | |
/** @file framerate_gui.cpp GUI for displaying framerate/game speed information. */ | |
#include "framerate_type.h" | |
#include "gfx_func.h" | |
#include "window_gui.h" | |
#include "strings_func.h" | |
#include "debug.h" | |
#include "console_func.h" | |
#include "console_type.h" | |
namespace { | |
template<size_t NumBuckets, uint Divisor = 1, typename ValueType = uint> | |
struct ModuloBucketCounter { | |
const size_t NUM_BUCKETS = NumBuckets; | |
const uint DIVISOR = Divisor; | |
ValueType counts[NumBuckets]; | |
size_t last_bucket; | |
ValueType GetTotalCount(bool include_current_bucket = false) | |
{ | |
ValueType sum{}; | |
for (size_t i = 0; i < NumBuckets; i++) { | |
if (i != this->last_bucket || include_current_bucket) sum += counts[i]; | |
} | |
return sum; | |
} | |
void Increment(size_t value) | |
{ | |
size_t bucket = (value / Divisor) % NumBuckets; | |
while (bucket != this->last_bucket) { | |
this->last_bucket = (this->last_bucket + 1) % NumBuckets; | |
this->counts[this->last_bucket] = 0; | |
} | |
this->counts[bucket] += 1; | |
} | |
}; | |
const int NUM_FRAMERATE_POINTS = 128; | |
uint32 _framerate_durations[FRAMERATE_MAX][NUM_FRAMERATE_POINTS] = {}; | |
uint32 _framerate_timestamps[FRAMERATE_MAX][NUM_FRAMERATE_POINTS] = {}; | |
ModuloBucketCounter<201, 10> _framerate_timestamp_buckets[FRAMERATE_MAX] = {}; | |
double _framerate_expected_rate[FRAMERATE_MAX] = { | |
1000.0 / MILLISECONDS_PER_TICK, // FRAMERATE_GAMELOOP | |
1000.0 / MILLISECONDS_PER_TICK, // FRAMERATE_DRAWING | |
60.0, // FRAMERATE_VIDEO | |
1000.0 * 8192 / 44100, // FRAMERATE_SOUND | |
}; | |
int _framerate_next_measurement_point[FRAMERATE_MAX] = {}; | |
void StoreMeasurement(FramerateElement elem, uint32 start_time, uint32 end_time) | |
{ | |
_framerate_durations[elem][_framerate_next_measurement_point[elem]] = end_time - start_time; | |
_framerate_timestamps[elem][_framerate_next_measurement_point[elem]] = start_time; | |
_framerate_timestamp_buckets[elem].Increment(start_time); | |
_framerate_next_measurement_point[elem] += 1; | |
_framerate_next_measurement_point[elem] %= NUM_FRAMERATE_POINTS; | |
} | |
double GetAverageDuration(FramerateElement elem, int points) | |
{ | |
assert(elem < FRAMERATE_MAX); | |
int first_point = _framerate_next_measurement_point[elem] - points - 1; | |
while (first_point < 0) first_point += NUM_FRAMERATE_POINTS; | |
double sumtime = 0; | |
for (int i = first_point; i < first_point + points; i++) { | |
sumtime += _framerate_durations[elem][i % NUM_FRAMERATE_POINTS]; | |
} | |
return sumtime / points; | |
} | |
uint32 GetAverageTimestep(FramerateElement elem, int points) | |
{ | |
assert(elem < FRAMERATE_MAX); | |
int first = _framerate_next_measurement_point[elem] - points; | |
int last = _framerate_next_measurement_point[elem] - 1; | |
if (first < 0) first += NUM_FRAMERATE_POINTS; | |
if (last < 0) last += NUM_FRAMERATE_POINTS; | |
return (_framerate_timestamps[elem][last] - _framerate_timestamps[elem][first]) / points; | |
} | |
double MillisecondsToFps(uint32 ms) | |
{ | |
return 1000.0 / ms; | |
} | |
double GetFramerate(FramerateElement elem) | |
{ | |
auto &bucketiser = _framerate_timestamp_buckets[elem]; | |
return bucketiser.GetTotalCount(false) * (1000.0 / bucketiser.DIVISOR) / (bucketiser.NUM_BUCKETS - 1.0); | |
} | |
} | |
FramerateMeasurer::FramerateMeasurer(FramerateElement elem) | |
{ | |
assert(elem < FRAMERATE_MAX); | |
this->elem = elem; | |
this->start_time = GetTime(); | |
} | |
FramerateMeasurer::~FramerateMeasurer() | |
{ | |
StoreMeasurement(this->elem, this->start_time, GetTime()); | |
} | |
void FramerateMeasurer::SetExpectedRate(double rate) | |
{ | |
_framerate_expected_rate[this->elem] = rate; | |
} | |
enum FramerateWindowWidgets { | |
WID_FRW_CAPTION, | |
WID_FRW_TIMES_GAMELOOP, | |
WID_FRW_TIMES_DRAWING, | |
WID_FRW_TIMES_VIDEO, | |
WID_FRW_TIMES_SOUND, | |
WID_FRW_FPS_GAMELOOP, | |
WID_FRW_FPS_DRAWING, | |
WID_FRW_FPS_VIDEO, | |
WID_FRW_FPS_SOUND, | |
}; | |
struct FramerateWindow : Window { | |
FramerateWindow(WindowDesc *desc, WindowNumber number) : Window(desc) | |
{ | |
this->InitNested(number); | |
} | |
virtual void OnInvalidateData(int data = 0, bool gui_scope = true) | |
{ | |
if (!gui_scope) return; | |
this->SetDirty(); | |
} | |
virtual void OnTick() | |
{ | |
this->InvalidateData(); | |
} | |
static void SetDParmGoodWarnBadDuration(double value) | |
{ | |
const double threshold_good = MILLISECONDS_PER_TICK / 3; | |
const double threshold_bad = MILLISECONDS_PER_TICK; | |
uint tpl; | |
if (value < threshold_good) tpl = STR_FRAMERATE_MS_GOOD; | |
else if (value > threshold_bad) tpl = STR_FRAMERATE_MS_BAD; | |
else tpl = STR_FRAMERATE_MS_WARN; | |
value = min(9999.99, value); | |
SetDParam(0, tpl); | |
SetDParam(1, (int)(value * 100)); | |
SetDParam(2, 2); | |
} | |
static void SetDParmGoodWarnBadRate(double value, FramerateElement elem) | |
{ | |
const double threshold_good = _framerate_expected_rate[elem] * 0.95; | |
const double threshold_bad = _framerate_expected_rate[elem] * 2 / 3; | |
uint tpl; | |
if (value > threshold_good) tpl = STR_FRAMERATE_FPS_GOOD; | |
else if (value < threshold_bad) tpl = STR_FRAMERATE_FPS_BAD; | |
else tpl = STR_FRAMERATE_FPS_WARN; | |
value = min(9999.99, value); | |
SetDParam(0, tpl); | |
SetDParam(1, (int)(value * 100)); | |
SetDParam(2, 2); | |
} | |
virtual void SetStringParameters(int widget) const | |
{ | |
static char text_value[FRAMERATE_MAX][32]; | |
double value; | |
switch (widget) { | |
case WID_FRW_TIMES_GAMELOOP: | |
value = GetAverageDuration(FRAMERATE_GAMELOOP, NUM_FRAMERATE_POINTS); | |
SetDParmGoodWarnBadDuration(value); | |
break; | |
case WID_FRW_TIMES_DRAWING: | |
value = GetAverageDuration(FRAMERATE_DRAWING, NUM_FRAMERATE_POINTS); | |
SetDParmGoodWarnBadDuration(value); | |
break; | |
case WID_FRW_TIMES_VIDEO: | |
value = GetAverageDuration(FRAMERATE_VIDEO, NUM_FRAMERATE_POINTS); | |
SetDParmGoodWarnBadDuration(value); | |
break; | |
case WID_FRW_TIMES_SOUND: | |
value = GetAverageDuration(FRAMERATE_SOUND, NUM_FRAMERATE_POINTS); | |
SetDParmGoodWarnBadDuration(value); | |
break; | |
case WID_FRW_FPS_GAMELOOP: | |
value = GetFramerate(FRAMERATE_GAMELOOP); | |
SetDParmGoodWarnBadRate(value, FRAMERATE_GAMELOOP); | |
break; | |
case WID_FRW_FPS_DRAWING: | |
value = GetFramerate(FRAMERATE_DRAWING); | |
SetDParmGoodWarnBadRate(value, FRAMERATE_DRAWING); | |
break; | |
case WID_FRW_FPS_VIDEO: | |
value = GetFramerate(FRAMERATE_VIDEO); | |
SetDParmGoodWarnBadRate(value, FRAMERATE_VIDEO); | |
break; | |
case WID_FRW_FPS_SOUND: | |
value = GetFramerate(FRAMERATE_SOUND); | |
SetDParmGoodWarnBadRate(value, FRAMERATE_SOUND); | |
break; | |
} | |
} | |
virtual void UpdateWidgetSize(int widget, Dimension *size, const Dimension &padding, Dimension *fill, Dimension *resize) | |
{ | |
switch (widget) { | |
case WID_FRW_TIMES_GAMELOOP: | |
case WID_FRW_TIMES_DRAWING: | |
case WID_FRW_TIMES_VIDEO: | |
case WID_FRW_TIMES_SOUND: | |
SetDParmGoodWarnBadDuration(9999.99); | |
*size = GetStringBoundingBox(STR_FRAMERATE_DISPLAY_VALUE); | |
break; | |
case WID_FRW_FPS_GAMELOOP: | |
case WID_FRW_FPS_DRAWING: | |
case WID_FRW_FPS_VIDEO: | |
case WID_FRW_FPS_SOUND: | |
SetDParmGoodWarnBadRate(9999.99, FRAMERATE_GAMELOOP); | |
*size = GetStringBoundingBox(STR_FRAMERATE_DISPLAY_VALUE); | |
break; | |
default: | |
Window::UpdateWidgetSize(widget, size, padding, fill, resize); | |
break; | |
} | |
} | |
}; | |
static const NWidgetPart _framerate_window_widgets[] = { | |
NWidget(NWID_HORIZONTAL), | |
NWidget(WWT_CLOSEBOX, COLOUR_GREY), | |
NWidget(WWT_CAPTION, COLOUR_GREY), SetDataTip(STR_FRAMERATE_DISPLAY_CAPTION, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS), | |
NWidget(WWT_STICKYBOX, COLOUR_GREY), | |
EndContainer(), | |
NWidget(WWT_PANEL, COLOUR_GREY), | |
NWidget(NWID_HORIZONTAL), SetPIP(6, 3, 6), | |
NWidget(NWID_VERTICAL), SetPIP(6, 6, 6), | |
NWidget(WWT_TEXT, COLOUR_GREY), SetDataTip(STR_FRAMERATE_DISPLAY_GAMELOOP, STR_FRAMERATE_DISPLAY_GAMELOOP_TOOLTIP), | |
NWidget(WWT_TEXT, COLOUR_GREY), SetDataTip(STR_FRAMERATE_DISPLAY_DRAWING, STR_FRAMERATE_DISPLAY_DRAWING_TOOLTIP), | |
NWidget(WWT_TEXT, COLOUR_GREY), SetDataTip(STR_FRAMERATE_DISPLAY_VIDEO, STR_FRAMERATE_DISPLAY_VIDEO_TOOLTIP), | |
NWidget(WWT_TEXT, COLOUR_GREY), SetDataTip(STR_FRAMERATE_DISPLAY_SOUND, STR_FRAMERATE_DISPLAY_SOUND_TOOLTIP), | |
EndContainer(), | |
NWidget(NWID_VERTICAL), SetPIP(6, 6, 6), | |
NWidget(WWT_TEXT, COLOUR_GREY, WID_FRW_TIMES_GAMELOOP), SetDataTip(STR_FRAMERATE_DISPLAY_VALUE, STR_FRAMERATE_DISPLAY_GAMELOOP_TOOLTIP), | |
NWidget(WWT_TEXT, COLOUR_GREY, WID_FRW_TIMES_DRAWING), SetDataTip(STR_FRAMERATE_DISPLAY_VALUE, STR_FRAMERATE_DISPLAY_DRAWING_TOOLTIP), | |
NWidget(WWT_TEXT, COLOUR_GREY, WID_FRW_TIMES_VIDEO), SetDataTip(STR_FRAMERATE_DISPLAY_VALUE, STR_FRAMERATE_DISPLAY_VIDEO_TOOLTIP), | |
NWidget(WWT_TEXT, COLOUR_GREY, WID_FRW_TIMES_SOUND), SetDataTip(STR_FRAMERATE_DISPLAY_VALUE, STR_FRAMERATE_DISPLAY_SOUND_TOOLTIP), | |
EndContainer(), | |
NWidget(NWID_VERTICAL), SetPIP(6, 6, 6), | |
NWidget(WWT_TEXT, COLOUR_GREY, WID_FRW_FPS_GAMELOOP), SetDataTip(STR_FRAMERATE_DISPLAY_VALUE, STR_FRAMERATE_DISPLAY_GAMELOOP_TOOLTIP), | |
NWidget(WWT_TEXT, COLOUR_GREY, WID_FRW_FPS_DRAWING), SetDataTip(STR_FRAMERATE_DISPLAY_VALUE, STR_FRAMERATE_DISPLAY_DRAWING_TOOLTIP), | |
NWidget(WWT_TEXT, COLOUR_GREY, WID_FRW_FPS_VIDEO), SetDataTip(STR_FRAMERATE_DISPLAY_VALUE, STR_FRAMERATE_DISPLAY_VIDEO_TOOLTIP), | |
NWidget(WWT_TEXT, COLOUR_GREY, WID_FRW_FPS_SOUND), SetDataTip(STR_FRAMERATE_DISPLAY_VALUE, STR_FRAMERATE_DISPLAY_SOUND_TOOLTIP), | |
EndContainer(), | |
EndContainer(), | |
EndContainer(), | |
}; | |
static WindowDesc _framerate_display_desc( | |
WDP_AUTO, "framerate_display", 60, 40, | |
WC_FRAMERATE_DISPLAY, WC_NONE, | |
0, | |
_framerate_window_widgets, lengthof(_framerate_window_widgets) | |
); | |
void ShowFramerateWindow() | |
{ | |
AllocateWindowDescFront<FramerateWindow>(&_framerate_display_desc, 0); | |
} | |
void ConPrintFramerate() | |
{ | |
const int count1 = NUM_FRAMERATE_POINTS / 1; | |
const int count2 = NUM_FRAMERATE_POINTS / 4; | |
const int count3 = NUM_FRAMERATE_POINTS / 8; | |
IConsolePrintF(TC_SILVER, "Based on num. data points: %d %d %d", count1, count2, count3); | |
static char *MEASUREMENT_NAMES[FRAMERATE_MAX] = { | |
"Game loop", | |
"Drawing", | |
"Video output", | |
"Sound mixing", | |
}; | |
for (FramerateElement e = FRAMERATE_FIRST; e < FRAMERATE_MAX; e = (FramerateElement)(e + 1)) { | |
IConsolePrintF(TC_GREEN, "%s rate: %.2ffps %.2ffps %.2ffps (expected: %.2ffps)", | |
MEASUREMENT_NAMES[e], | |
MillisecondsToFps(GetAverageTimestep(e, count1)), | |
MillisecondsToFps(GetAverageTimestep(e, count2)), | |
MillisecondsToFps(GetAverageTimestep(e, count3)), | |
_framerate_expected_rate[e]); | |
} | |
for (FramerateElement e = FRAMERATE_FIRST; e < FRAMERATE_MAX; e = (FramerateElement)(e + 1)) { | |
IConsolePrintF(TC_LIGHT_BLUE, "%s times: %.2fms %.2fms %.2fms", | |
MEASUREMENT_NAMES[e], | |
GetAverageDuration(e, count1), | |
GetAverageDuration(e, count2), | |
GetAverageDuration(e, count3)); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment