summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNiels Martin Hansen <nielsm@indvikleren.dk>2018-07-19 21:17:07 +0200
committerPatric Stout <truebrain@openttd.org>2018-07-19 21:17:07 +0200
commit2a868b9f3b8e3b5f8b9e5f728f628ec88fd5e3ad (patch)
tree36da708e128fc68d9ceac32362503df43af2fc22
parenta3d1950b656787b76fbccec1aedd63407c34c2f1 (diff)
downloadopenttd-2a868b9f3b8e3b5f8b9e5f728f628ec88fd5e3ad.tar.xz
Feature: Framerate display window (#6822)
Frame rate and various game loop/graphics timing measurements and graphs. Accessible via the Help menu, and can print some stats in the console via the fps command.
-rw-r--r--README.md65
-rw-r--r--projects/openttd_vs100.vcxproj2
-rw-r--r--projects/openttd_vs100.vcxproj.filters6
-rw-r--r--projects/openttd_vs140.vcxproj2
-rw-r--r--projects/openttd_vs140.vcxproj.filters6
-rw-r--r--projects/openttd_vs141.vcxproj2
-rw-r--r--projects/openttd_vs141.vcxproj.filters6
-rw-r--r--projects/openttd_vs80.vcproj8
-rw-r--r--projects/openttd_vs90.vcproj8
-rw-r--r--source.list2
-rw-r--r--src/aircraft_cmd.cpp3
-rw-r--r--src/animated_tile.cpp3
-rw-r--r--src/console_cmds.cpp33
-rw-r--r--src/framerate_gui.cpp837
-rw-r--r--src/framerate_type.h67
-rw-r--r--src/landscape.cpp15
-rw-r--r--src/lang/english.txt51
-rw-r--r--src/linkgraph/linkgraphschedule.cpp2
-rw-r--r--src/mixer.cpp8
-rw-r--r--src/openttd.cpp12
-rw-r--r--src/roadveh_cmd.cpp3
-rw-r--r--src/ship_cmd.cpp3
-rw-r--r--src/toolbar_gui.cpp12
-rw-r--r--src/train_cmd.cpp3
-rw-r--r--src/vehicle.cpp12
-rw-r--r--src/video/allegro_v.cpp3
-rw-r--r--src/video/cocoa/wnd_quartz.mm3
-rw-r--r--src/video/cocoa/wnd_quickdraw.mm3
-rw-r--r--src/video/sdl_v.cpp3
-rw-r--r--src/video/win32_v.cpp3
-rw-r--r--src/viewport.cpp3
-rw-r--r--src/window.cpp4
-rw-r--r--src/window_type.h12
33 files changed, 1194 insertions, 11 deletions
diff --git a/README.md b/README.md
index 7f5d244f5..7ab63c9f4 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,7 @@
- 4.4) [Files in tar (archives)](#44-files-in-tar-archives)
- 5.0) [OpenTTD features](#50-openttd-features)
- 5.1) [Logging of potentially dangerous actions](#51-logging-of-potentially-dangerous-actions)
+ - 5.2) [Frame rate and performance metrics](#52-frame-rate-and-performance-metrics)
- 6.0) [Configuration file](#60-configuration-file)
- 7.0) [Compiling](#70-compiling)
- 7.1) [Required/optional libraries](#71-requiredoptional-libraries)
@@ -433,6 +434,70 @@ No personal information is stored.
You can show the game log by typing 'gamelog' in the console or by running
OpenTTD in debug mode.
+### 5.2) Frame rate and performance metrics
+
+The Help menu in-game has a function to open the Frame rate window. This
+window shows various real-time performance statistics, measuring what parts
+of the game require the most processing power currently.
+
+A summary of the statistics can also be retrieved from the console with the
+`fps` command. This is especially useful on dedicated servers, where the
+administrator might want to determine what's limiting performance in a slow
+game.
+
+The frame rate is given as two figures, the simulation rate and the graphics
+frame rate. Usually these are identical, as the screen is rendered exactly
+once per simulated tick, but in the future there might be support for graphics
+and simulation running at different rates. When the game is paused, the
+simulation rate drops to zero.
+
+In addition to the simulation rate, a game speed factor is also calculated.
+This is based on the target simulation speed, which is 30 milliseconds per
+game tick. At that speed, the expected frame rate is 33.33 frames/second, and
+the game speed factor is how close to that target the actual rate is. When
+the game is in fast forward mode, the game speed factor shows how much
+speed up is achieved.
+
+The lower part of the window shows timing statistics for individual parts of
+the game. The times shown are short-term and long-term averages of how long
+it takes to process one tick of game time, all figures are in milliseconds.
+
+Clicking a line in the lower part of the window opens a graph window, giving
+detailed readings on each tick simulated by the game.
+
+The following is an explanation of the different statistics:
+
+- *Game loop* - Total processing time used per simulated "tick" in the game.
+ This includes all pathfinding, world updates, and economy handling.
+- *Cargo handling* - Time spent loading/unloading cargo at stations, and
+ industries and towns sending/retrieving cargo from stations.
+- *Train ticks*, *Road vehicle ticks*, *Ship ticks*, *Aircraft ticks* -
+ Time spent on pathfinding and other processing for each player vehicle type.
+- *World ticks* - Time spent on other world/landscape processing. This
+ includes towns growing, building animations, updates of farmland and trees,
+ and station rating updates.
+- *Link graph delay* - Time overruns of the cargo distribution link graph
+ update thread. Usually the link graph is updated in a background thread,
+ but these updates need to synchronise with the main game loop occasionally,
+ if the time spent on link graph updates is longer than the time taken to
+ otherwise simulate the game while it was updating, these delays are counted
+ in this figure.
+- *Graphics rendering* - Total time spent rendering all graphics, including
+ both GUI and world viewports. This typically spikes when panning the view
+ around, and when more things are happening on screen at once.
+- *World viewport rendering* - Isolated time spent rendering just world
+ viewports. If this figure is significantly lower than the total graphics
+ rendering time, most time is spent rendering GUI than rendering world.
+- *Video output* - Speed of copying the rendered graphics to the display
+ adapter. Usually this should be very fast (in the range of 0-3 ms), large
+ values for this can indicate a graphics driver problem.
+- *Sound mixing* - Speed of mixing active audio samples together. Usually
+ this should be very fast (in the range of 0-3 ms), if it is slow, consider
+ switching to the NoSound set.
+
+If the frame rate window is shaded, the title bar will instead show just the
+current simulation rate and the game speed factor.
+
## 6.0) Configuration file
The configuration file for OpenTTD (openttd.cfg) is in a simple Windows-like
diff --git a/projects/openttd_vs100.vcxproj b/projects/openttd_vs100.vcxproj
index 2297c9be2..6fbd5e3f0 100644
--- a/projects/openttd_vs100.vcxproj
+++ b/projects/openttd_vs100.vcxproj
@@ -481,6 +481,7 @@
<ClInclude Include="..\src\fios.h" />
<ClInclude Include="..\src\fontcache.h" />
<ClInclude Include="..\src\fontdetection.h" />
+ <ClInclude Include="..\src\framerate_type.h" />
<ClInclude Include="..\src\base_consist.h" />
<ClInclude Include="..\src\gamelog.h" />
<ClInclude Include="..\src\gamelog_internal.h" />
@@ -731,6 +732,7 @@
<ClCompile Include="..\src\engine_gui.cpp" />
<ClCompile Include="..\src\error_gui.cpp" />
<ClCompile Include="..\src\fios_gui.cpp" />
+ <ClCompile Include="..\src\framerate_gui.cpp" />
<ClCompile Include="..\src\genworld_gui.cpp" />
<ClCompile Include="..\src\goal_gui.cpp" />
<ClCompile Include="..\src\graph_gui.cpp" />
diff --git a/projects/openttd_vs100.vcxproj.filters b/projects/openttd_vs100.vcxproj.filters
index 7f529a397..7eee89731 100644
--- a/projects/openttd_vs100.vcxproj.filters
+++ b/projects/openttd_vs100.vcxproj.filters
@@ -600,6 +600,9 @@
<ClInclude Include="..\src\fontdetection.h">
<Filter>Header Files</Filter>
</ClInclude>
+ <ClInclude Include="..\src\framerate_type.h">
+ <Filter>Header Files</Filter>
+ </ClInclude>
<ClInclude Include="..\src\base_consist.h">
<Filter>Header Files</Filter>
</ClInclude>
@@ -1350,6 +1353,9 @@
<ClCompile Include="..\src\fios_gui.cpp">
<Filter>GUI Source Code</Filter>
</ClCompile>
+ <ClCompile Include="..\src\framerate_gui.cpp">
+ <Filter>GUI Source Code</Filter>
+ </ClCompile>
<ClCompile Include="..\src\genworld_gui.cpp">
<Filter>GUI Source Code</Filter>
</ClCompile>
diff --git a/projects/openttd_vs140.vcxproj b/projects/openttd_vs140.vcxproj
index 61f9ccc77..fdc1fd411 100644
--- a/projects/openttd_vs140.vcxproj
+++ b/projects/openttd_vs140.vcxproj
@@ -502,6 +502,7 @@
<ClInclude Include="..\src\fios.h" />
<ClInclude Include="..\src\fontcache.h" />
<ClInclude Include="..\src\fontdetection.h" />
+ <ClInclude Include="..\src\framerate_type.h" />
<ClInclude Include="..\src\base_consist.h" />
<ClInclude Include="..\src\gamelog.h" />
<ClInclude Include="..\src\gamelog_internal.h" />
@@ -752,6 +753,7 @@
<ClCompile Include="..\src\engine_gui.cpp" />
<ClCompile Include="..\src\error_gui.cpp" />
<ClCompile Include="..\src\fios_gui.cpp" />
+ <ClCompile Include="..\src\framerate_gui.cpp" />
<ClCompile Include="..\src\genworld_gui.cpp" />
<ClCompile Include="..\src\goal_gui.cpp" />
<ClCompile Include="..\src\graph_gui.cpp" />
diff --git a/projects/openttd_vs140.vcxproj.filters b/projects/openttd_vs140.vcxproj.filters
index 7f529a397..7eee89731 100644
--- a/projects/openttd_vs140.vcxproj.filters
+++ b/projects/openttd_vs140.vcxproj.filters
@@ -600,6 +600,9 @@
<ClInclude Include="..\src\fontdetection.h">
<Filter>Header Files</Filter>
</ClInclude>
+ <ClInclude Include="..\src\framerate_type.h">
+ <Filter>Header Files</Filter>
+ </ClInclude>
<ClInclude Include="..\src\base_consist.h">
<Filter>Header Files</Filter>
</ClInclude>
@@ -1350,6 +1353,9 @@
<ClCompile Include="..\src\fios_gui.cpp">
<Filter>GUI Source Code</Filter>
</ClCompile>
+ <ClCompile Include="..\src\framerate_gui.cpp">
+ <Filter>GUI Source Code</Filter>
+ </ClCompile>
<ClCompile Include="..\src\genworld_gui.cpp">
<Filter>GUI Source Code</Filter>
</ClCompile>
diff --git a/projects/openttd_vs141.vcxproj b/projects/openttd_vs141.vcxproj
index 0cfa9af69..0b450cbfd 100644
--- a/projects/openttd_vs141.vcxproj
+++ b/projects/openttd_vs141.vcxproj
@@ -502,6 +502,7 @@
<ClInclude Include="..\src\fios.h" />
<ClInclude Include="..\src\fontcache.h" />
<ClInclude Include="..\src\fontdetection.h" />
+ <ClInclude Include="..\src\framerate_type.h" />
<ClInclude Include="..\src\base_consist.h" />
<ClInclude Include="..\src\gamelog.h" />
<ClInclude Include="..\src\gamelog_internal.h" />
@@ -752,6 +753,7 @@
<ClCompile Include="..\src\engine_gui.cpp" />
<ClCompile Include="..\src\error_gui.cpp" />
<ClCompile Include="..\src\fios_gui.cpp" />
+ <ClCompile Include="..\src\framerate_gui.cpp" />
<ClCompile Include="..\src\genworld_gui.cpp" />
<ClCompile Include="..\src\goal_gui.cpp" />
<ClCompile Include="..\src\graph_gui.cpp" />
diff --git a/projects/openttd_vs141.vcxproj.filters b/projects/openttd_vs141.vcxproj.filters
index 7f529a397..7eee89731 100644
--- a/projects/openttd_vs141.vcxproj.filters
+++ b/projects/openttd_vs141.vcxproj.filters
@@ -600,6 +600,9 @@
<ClInclude Include="..\src\fontdetection.h">
<Filter>Header Files</Filter>
</ClInclude>
+ <ClInclude Include="..\src\framerate_type.h">
+ <Filter>Header Files</Filter>
+ </ClInclude>
<ClInclude Include="..\src\base_consist.h">
<Filter>Header Files</Filter>
</ClInclude>
@@ -1350,6 +1353,9 @@
<ClCompile Include="..\src\fios_gui.cpp">
<Filter>GUI Source Code</Filter>
</ClCompile>
+ <ClCompile Include="..\src\framerate_gui.cpp">
+ <Filter>GUI Source Code</Filter>
+ </ClCompile>
<ClCompile Include="..\src\genworld_gui.cpp">
<Filter>GUI Source Code</Filter>
</ClCompile>
diff --git a/projects/openttd_vs80.vcproj b/projects/openttd_vs80.vcproj
index 8ae32e70a..2ea6fdcd6 100644
--- a/projects/openttd_vs80.vcproj
+++ b/projects/openttd_vs80.vcproj
@@ -1111,6 +1111,10 @@
>
</File>
<File
+ RelativePath=".\..\src\framerate_type.h"
+ >
+ </File>
+ <File
RelativePath=".\..\src\base_consist.h"
>
</File>
@@ -2119,6 +2123,10 @@
>
</File>
<File
+ RelativePath=".\..\src\framerate_gui.cpp"
+ >
+ </File>
+ <File
RelativePath=".\..\src\genworld_gui.cpp"
>
</File>
diff --git a/projects/openttd_vs90.vcproj b/projects/openttd_vs90.vcproj
index b488daadb..174677586 100644
--- a/projects/openttd_vs90.vcproj
+++ b/projects/openttd_vs90.vcproj
@@ -1108,6 +1108,10 @@
>
</File>
<File
+ RelativePath=".\..\src\framerate_type.h"
+ >
+ </File>
+ <File
RelativePath=".\..\src\base_consist.h"
>
</File>
@@ -2116,6 +2120,10 @@
>
</File>
<File
+ RelativePath=".\..\src\framerate_gui.cpp"
+ >
+ </File>
+ <File
RelativePath=".\..\src\genworld_gui.cpp"
>
</File>
diff --git a/source.list b/source.list
index abc18266d..ee8e69ef5 100644
--- a/source.list
+++ b/source.list
@@ -193,6 +193,7 @@ fileio_type.h
fios.h
fontcache.h
fontdetection.h
+framerate_type.h
base_consist.h
gamelog.h
gamelog_internal.h
@@ -462,6 +463,7 @@ dock_gui.cpp
engine_gui.cpp
error_gui.cpp
fios_gui.cpp
+framerate_gui.cpp
genworld_gui.cpp
goal_gui.cpp
graph_gui.cpp
diff --git a/src/aircraft_cmd.cpp b/src/aircraft_cmd.cpp
index 94ad00fb9..de8e0ee32 100644
--- a/src/aircraft_cmd.cpp
+++ b/src/aircraft_cmd.cpp
@@ -37,6 +37,7 @@
#include "core/backup_type.hpp"
#include "zoom_func.h"
#include "disaster_vehicle.h"
+#include "framerate_type.h"
#include "table/strings.h"
@@ -2038,6 +2039,8 @@ bool Aircraft::Tick()
{
if (!this->IsNormalAircraft()) return true;
+ PerformanceAccumulator framerate(PFE_GL_AIRCRAFT);
+
this->tick_counter++;
if (!(this->vehstatus & VS_STOPPED)) this->running_ticks++;
diff --git a/src/animated_tile.cpp b/src/animated_tile.cpp
index 796c5f5e0..2a4cd8958 100644
--- a/src/animated_tile.cpp
+++ b/src/animated_tile.cpp
@@ -14,6 +14,7 @@
#include "core/smallvec_type.hpp"
#include "tile_cmd.h"
#include "viewport_func.h"
+#include "framerate_type.h"
#include "safeguards.h"
@@ -50,6 +51,8 @@ void AddAnimatedTile(TileIndex tile)
*/
void AnimateAnimatedTiles()
{
+ PerformanceAccumulator framerate(PFE_GL_LANDSCAPE);
+
const TileIndex *ti = _animated_tiles.Begin();
while (ti < _animated_tiles.End()) {
const TileIndex curr = *ti;
diff --git a/src/console_cmds.cpp b/src/console_cmds.cpp
index 3b65224de..f44809d76 100644
--- a/src/console_cmds.cpp
+++ b/src/console_cmds.cpp
@@ -1895,6 +1895,37 @@ static void IConsoleDebugLibRegister()
}
#endif
+DEF_CONSOLE_CMD(ConFramerate)
+{
+ extern void ConPrintFramerate(); // framerate_gui.cpp
+
+ if (argc == 0) {
+ IConsoleHelp("Show frame rate and game speed information");
+ return true;
+ }
+
+ ConPrintFramerate();
+ return true;
+}
+
+DEF_CONSOLE_CMD(ConFramerateWindow)
+{
+ extern void ShowFramerateWindow();
+
+ if (argc == 0) {
+ IConsoleHelp("Open the frame rate window");
+ return true;
+ }
+
+ if (_network_dedicated) {
+ IConsoleError("Can not open frame rate window on a dedicated server");
+ return false;
+ }
+
+ ShowFramerateWindow();
+ return true;
+}
+
/*******************************
* console command registration
*******************************/
@@ -2025,6 +2056,8 @@ void IConsoleStdLibRegister()
#ifdef _DEBUG
IConsoleDebugLibRegister();
#endif
+ IConsoleCmdRegister("fps", ConFramerate);
+ IConsoleCmdRegister("fps_wnd", ConFramerateWindow);
/* NewGRF development stuff */
IConsoleCmdRegister("reload_newgrfs", ConNewGRFReload, ConHookNewGRFDeveloperTool);
diff --git a/src/framerate_gui.cpp b/src/framerate_gui.cpp
new file mode 100644
index 000000000..0d6cc55c5
--- /dev/null
+++ b/src/framerate_gui.cpp
@@ -0,0 +1,837 @@
+/* $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 <chrono>
+#include "gfx_func.h"
+#include "window_gui.h"
+#include "table/sprites.h"
+#include "strings_func.h"
+#include "debug.h"
+#include "console_func.h"
+#include "console_type.h"
+
+
+namespace {
+
+ /** Number of data points to keep in buffer for each performance measurement */
+ const int NUM_FRAMERATE_POINTS = 512;
+ /** Units a second is divided into in performance measurements */
+ const TimingMeasurement TIMESTAMP_PRECISION = 1000000;
+
+ struct PerformanceData {
+ /** Duration value indicating the value is not valid should be considered a gap in measurements */
+ static const TimingMeasurement INVALID_DURATION = UINT64_MAX;
+
+ /** Time spent processing each cycle of the performance element, circular buffer */
+ TimingMeasurement durations[NUM_FRAMERATE_POINTS];
+ /** Start time of each cycle of the performance element, circular buffer */
+ TimingMeasurement timestamps[NUM_FRAMERATE_POINTS];
+ /** Expected number of cycles per second when the system is running without slowdowns */
+ double expected_rate;
+ /** Next index to write to in \c durations and \c timestamps */
+ int next_index;
+ /** Last index written to in \c durations and \c timestamps */
+ int prev_index;
+ /** Number of data points recorded, clamped to \c NUM_FRAMERATE_POINTS */
+ int num_valid;
+
+ /** Current accumulated duration */
+ TimingMeasurement acc_duration;
+ /** Start time for current accumulation cycle */
+ TimingMeasurement acc_timestamp;
+
+ explicit PerformanceData(double expected_rate) : expected_rate(expected_rate), next_index(0), prev_index(0), num_valid(0) { }
+
+ void Add(TimingMeasurement start_time, TimingMeasurement end_time)
+ {
+ this->durations[this->next_index] = end_time - start_time;
+ this->timestamps[this->next_index] = start_time;
+ this->prev_index = this->next_index;
+ this->next_index += 1;
+ if (this->next_index >= NUM_FRAMERATE_POINTS) this->next_index = 0;
+ this->num_valid = min(NUM_FRAMERATE_POINTS, this->num_valid + 1);
+ }
+
+ void BeginAccumulate(TimingMeasurement start_time)
+ {
+ this->timestamps[this->next_index] = this->acc_timestamp;
+ this->durations[this->next_index] = this->acc_duration;
+ this->prev_index = this->next_index;
+ this->next_index += 1;
+ if (this->next_index >= NUM_FRAMERATE_POINTS) this->next_index = 0;
+ this->num_valid = min(NUM_FRAMERATE_POINTS, this->num_valid + 1);
+
+ this->acc_duration = 0;
+ this->acc_timestamp = start_time;
+ }
+
+ void AddAccumulate(TimingMeasurement duration)
+ {
+ this->acc_duration += duration;
+ }
+
+ void AddPause(TimingMeasurement start_time)
+ {
+ if (this->durations[this->prev_index] != INVALID_DURATION) {
+ this->timestamps[this->next_index] = start_time;
+ this->durations[this->next_index] = INVALID_DURATION;
+ this->prev_index = this->next_index;
+ this->next_index += 1;
+ if (this->next_index >= NUM_FRAMERATE_POINTS) this->next_index = 0;
+ this->num_valid += 1;
+ }
+ }
+
+ /** Get average cycle processing time over a number of data points */
+ double GetAverageDurationMilliseconds(int count)
+ {
+ count = min(count, this->num_valid);
+
+ int first_point = this->prev_index - count;
+ if (first_point < 0) first_point += NUM_FRAMERATE_POINTS;
+
+ /* Sum durations, skipping invalid points */
+ double sumtime = 0;
+ for (int i = first_point; i < first_point + count; i++) {
+ auto d = this->durations[i % NUM_FRAMERATE_POINTS];
+ if (d != INVALID_DURATION) {
+ sumtime += d;
+ } else {
+ /* Don't count the invalid durations */
+ count--;
+ }
+ }
+
+ if (count == 0) return 0; // avoid div by zero
+ return sumtime * 1000 / count / TIMESTAMP_PRECISION;
+ }
+
+ /** Get current rate of a performance element, based on approximately the past one second of data */
+ double GetRate()
+ {
+ /* Start at last recorded point, end at latest when reaching the earliest recorded point */
+ int point = this->prev_index;
+ int last_point = this->next_index - this->num_valid;
+ if (last_point < 0) last_point += NUM_FRAMERATE_POINTS;
+
+ /** Number of data points collected */
+ int count = 0;
+ /** Time of previous data point */
+ TimingMeasurement last = this->timestamps[point];
+ /** Total duration covered by collected points */
+ TimingMeasurement total = 0;
+
+ while (point != last_point) {
+ /* Only record valid data points, but pretend the gaps in measurements aren't there */
+ if (this->durations[point] != INVALID_DURATION) {
+ total += last - this->timestamps[point];
+ count++;
+ }
+ last = this->timestamps[point];
+ if (total >= TIMESTAMP_PRECISION) break; // end after 1 second has been collected
+ point--;
+ if (point < 0) point = NUM_FRAMERATE_POINTS - 1;
+ }
+
+ if (total == 0 || count == 0) return 0;
+ return (double)count * TIMESTAMP_PRECISION / total;
+ }
+ };
+
+ /** Game loop rate, cycles per second */
+ static const double GL_RATE = 1000.0 / MILLISECONDS_PER_TICK;
+
+ PerformanceData _pf_data[PFE_MAX] = {
+ PerformanceData(GL_RATE), // PFE_GAMELOOP
+ PerformanceData(1), // PFE_ACC_GL_ECONOMY
+ PerformanceData(1), // PFE_ACC_GL_TRAINS
+ PerformanceData(1), // PFE_ACC_GL_ROADVEHS
+ PerformanceData(1), // PFE_ACC_GL_SHIPS
+ PerformanceData(1), // PFE_ACC_GL_AIRCRAFT
+ PerformanceData(1), // PFE_GL_LANDSCAPE
+ PerformanceData(1), // PFE_GL_LINKGRAPH
+ PerformanceData(GL_RATE), // PFE_DRAWING
+ PerformanceData(1), // PFE_ACC_DRAWWORLD
+ PerformanceData(60.0), // PFE_VIDEO
+ PerformanceData(1000.0 * 8192 / 44100), // PFE_SOUND
+ };
+
+}
+
+
+/**
+ * Return a timestamp with \c TIMESTAMP_PRECISION ticks per second precision.
+ * The basis of the timestamp is implementation defined, but the value should be steady,
+ * so differences can be taken to reliably measure intervals.
+ */
+static TimingMeasurement GetPerformanceTimer()
+{
+ using namespace std::chrono;
+ return (TimingMeasurement)time_point_cast<microseconds>(high_resolution_clock::now()).time_since_epoch().count();
+}
+
+
+/** Begin a cycle of a measured element. */
+PerformanceMeasurer::PerformanceMeasurer(PerformanceElement elem)
+{
+ assert(elem < PFE_MAX);
+
+ this->elem = elem;
+ this->start_time = GetPerformanceTimer();
+}
+
+/** Finish a cycle of a measured element and store the measurement taken. */
+PerformanceMeasurer::~PerformanceMeasurer()
+{
+ _pf_data[this->elem].Add(this->start_time, GetPerformanceTimer());
+}
+
+/** Set the rate of expected cycles per second of a performance element. */
+void PerformanceMeasurer::SetExpectedRate(double rate)
+{
+ _pf_data[this->elem].expected_rate = rate;
+}
+
+/** Indicate that a cycle of "pause" where no processing occurs. */
+void PerformanceMeasurer::Paused(PerformanceElement elem)
+{
+ _pf_data[elem].AddPause(GetPerformanceTimer());
+}
+
+
+/** Begin measuring one block of the accumulating value. */
+PerformanceAccumulator::PerformanceAccumulator(PerformanceElement elem)
+{
+ assert(elem < PFE_MAX);
+
+ this->elem = elem;
+ this->start_time = GetPerformanceTimer();
+}
+
+/** Finish and add one block of the accumulating value. */
+PerformanceAccumulator::~PerformanceAccumulator()
+{
+ _pf_data[this->elem].AddAccumulate(GetPerformanceTimer() - this->start_time);
+}
+
+/** Store the previous accumulator value and reset for a new cycle of accumulating measurements. */
+void PerformanceAccumulator::Reset(PerformanceElement elem)
+{
+ _pf_data[elem].BeginAccumulate(GetPerformanceTimer());
+}
+
+
+void ShowFrametimeGraphWindow(PerformanceElement elem);
+
+
+enum FramerateWindowWidgets {
+ WID_FRW_CAPTION,
+ WID_FRW_RATE_GAMELOOP,
+ WID_FRW_RATE_DRAWING,
+ WID_FRW_RATE_FACTOR,
+ WID_FRW_INFO_DATA_POINTS,
+ WID_FRW_TIMES_NAMES,
+ WID_FRW_TIMES_CURRENT,
+ WID_FRW_TIMES_AVERAGE,
+};
+
+static const NWidgetPart _framerate_window_widgets[] = {
+ NWidget(NWID_HORIZONTAL),
+ NWidget(WWT_CLOSEBOX, COLOUR_GREY),
+ NWidget(WWT_CAPTION, COLOUR_GREY, WID_FRW_CAPTION), SetDataTip(STR_FRAMERATE_CAPTION, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS),
+ NWidget(WWT_SHADEBOX, COLOUR_GREY),
+ NWidget(WWT_STICKYBOX, COLOUR_GREY),
+ EndContainer(),
+ NWidget(WWT_PANEL, COLOUR_GREY),
+ NWidget(NWID_VERTICAL), SetPadding(6), SetPIP(0, 3, 0),
+ NWidget(WWT_TEXT, COLOUR_GREY, WID_FRW_RATE_GAMELOOP), SetDataTip(STR_FRAMERATE_RATE_GAMELOOP, STR_FRAMERATE_RATE_GAMELOOP_TOOLTIP),
+ NWidget(WWT_TEXT, COLOUR_GREY, WID_FRW_RATE_DRAWING), SetDataTip(STR_FRAMERATE_RATE_BLITTER, STR_FRAMERATE_RATE_BLITTER_TOOLTIP),
+ NWidget(WWT_TEXT, COLOUR_GREY, WID_FRW_RATE_FACTOR), SetDataTip(STR_FRAMERATE_SPEED_FACTOR, STR_FRAMERATE_SPEED_FACTOR_TOOLTIP),
+ EndContainer(),
+ EndContainer(),
+ NWidget(WWT_PANEL, COLOUR_GREY),
+ NWidget(NWID_VERTICAL), SetPadding(6), SetPIP(0, 3, 0),
+ NWidget(NWID_HORIZONTAL), SetPIP(0, 6, 0),
+ NWidget(WWT_EMPTY, COLOUR_GREY, WID_FRW_TIMES_NAMES),
+ NWidget(WWT_EMPTY, COLOUR_GREY, WID_FRW_TIMES_CURRENT),
+ NWidget(WWT_EMPTY, COLOUR_GREY, WID_FRW_TIMES_AVERAGE),
+ EndContainer(),
+ NWidget(WWT_TEXT, COLOUR_GREY, WID_FRW_INFO_DATA_POINTS), SetDataTip(STR_FRAMERATE_DATA_POINTS, 0x0),
+ EndContainer(),
+ EndContainer(),
+};
+
+struct FramerateWindow : Window {
+ bool small;
+ uint32 next_update;
+
+ struct CachedDecimal {
+ StringID strid;
+ uint32 value;
+
+ inline void SetRate(double value, double target)
+ {
+ const double threshold_good = target * 0.95;
+ const double threshold_bad = target * 2 / 3;
+ value = min(9999.99, value);
+ this->value = (uint32)(value * 100);
+ this->strid = (value > threshold_good) ? STR_FRAMERATE_FPS_GOOD : (value < threshold_bad) ? STR_FRAMERATE_FPS_BAD : STR_FRAMERATE_FPS_WARN;
+ }
+
+ inline void SetTime(double value, double target)
+ {
+ const double threshold_good = target / 3;
+ const double threshold_bad = target;
+ value = min(9999.99, value);
+ this->value = (uint32)(value * 100);
+ this->strid = (value < threshold_good) ? STR_FRAMERATE_MS_GOOD : (value > threshold_bad) ? STR_FRAMERATE_MS_BAD : STR_FRAMERATE_MS_WARN;
+ }
+
+ inline void InsertDParams(uint n) const
+ {
+ SetDParam(n, this->value);
+ SetDParam(n + 1, 2);
+ }
+ };
+
+ CachedDecimal rate_gameloop; ///< cached game loop tick rate
+ CachedDecimal rate_drawing; ///< cached drawing frame rate
+ CachedDecimal speed_gameloop; ///< cached game loop speed factor
+ CachedDecimal times_shortterm[PFE_MAX]; ///< cached short term average times
+ CachedDecimal times_longterm[PFE_MAX]; ///< cached long term average times
+
+ static const int VSPACING = 3; ///< space between column heading and values
+
+ FramerateWindow(WindowDesc *desc, WindowNumber number) : Window(desc)
+ {
+ this->InitNested(number);
+ this->small = this->IsShaded();
+ this->UpdateData();
+ }
+
+ virtual void OnTick()
+ {
+ /* Check if the shaded state has changed, switch caption text if it has */
+ if (this->small != this->IsShaded()) {
+ this->small = this->IsShaded();
+ this->GetWidget<NWidgetLeaf>(WID_FRW_CAPTION)->SetDataTip(this->small ? STR_FRAMERATE_CAPTION_SMALL : STR_FRAMERATE_CAPTION, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS);
+ this->next_update = 0;
+ }
+
+ if (_realtime_tick >= this->next_update) {
+ this->UpdateData();
+ this->SetDirty();
+ this->next_update = _realtime_tick + 100;
+ }
+ }
+
+ void UpdateData()
+ {
+ double gl_rate = _pf_data[PFE_GAMELOOP].GetRate();
+ this->rate_gameloop.SetRate(gl_rate, _pf_data[PFE_GAMELOOP].expected_rate);
+ this->speed_gameloop.SetRate(gl_rate / _pf_data[PFE_GAMELOOP].expected_rate, 1.0);
+ if (this->small) return; // in small mode, this is everything needed
+
+ this->rate_drawing.SetRate(_pf_data[PFE_DRAWING].GetRate(), _pf_data[PFE_DRAWING].expected_rate);
+
+ for (PerformanceElement e = PFE_FIRST; e < PFE_MAX; e++) {
+ this->times_shortterm[e].SetTime(_pf_data[e].GetAverageDurationMilliseconds(8), MILLISECONDS_PER_TICK);
+ this->times_longterm[e].SetTime(_pf_data[e].GetAverageDurationMilliseconds(NUM_FRAMERATE_POINTS), MILLISECONDS_PER_TICK);
+ }
+ }
+
+ virtual void SetStringParameters(int widget) const
+ {
+ switch (widget) {
+ case WID_FRW_CAPTION:
+ /* When the window is shaded, the caption shows game loop rate and speed factor */
+ if (!this->small) break;
+ SetDParam(0, this->rate_gameloop.strid);
+ this->rate_gameloop.InsertDParams(1);
+ this->speed_gameloop.InsertDParams(3);
+ break;
+
+ case WID_FRW_RATE_GAMELOOP:
+ SetDParam(0, this->rate_gameloop.strid);
+ this->rate_gameloop.InsertDParams(1);
+ break;
+ case WID_FRW_RATE_DRAWING:
+ SetDParam(0, this->rate_drawing.strid);
+ this->rate_drawing.InsertDParams(1);
+ break;
+ case WID_FRW_RATE_FACTOR:
+ this->speed_gameloop.InsertDParams(0);
+ break;
+ case WID_FRW_INFO_DATA_POINTS:
+ SetDParam(0, NUM_FRAMERATE_POINTS);
+ break;
+ }
+ }
+
+ virtual void UpdateWidgetSize(int widget, Dimension *size, const Dimension &padding, Dimension *fill, Dimension *resize)
+ {
+ switch (widget) {
+ case WID_FRW_RATE_GAMELOOP:
+ SetDParam(0, STR_FRAMERATE_FPS_GOOD);
+ SetDParam(1, 999999);
+ SetDParam(2, 2);
+ *size = GetStringBoundingBox(STR_FRAMERATE_RATE_GAMELOOP);
+ break;
+ case WID_FRW_RATE_DRAWING:
+ SetDParam(0, STR_FRAMERATE_FPS_GOOD);
+ SetDParam(1, 999999);
+ SetDParam(2, 2);
+ *size = GetStringBoundingBox(STR_FRAMERATE_RATE_BLITTER);
+ break;
+ case WID_FRW_RATE_FACTOR:
+ SetDParam(0, 999999);
+ SetDParam(1, 2);
+ *size = GetStringBoundingBox(STR_FRAMERATE_SPEED_FACTOR);
+ break;
+
+ case WID_FRW_TIMES_NAMES: {
+ int linecount = PFE_MAX - PFE_FIRST;
+ size->width = 0;
+ size->height = FONT_HEIGHT_NORMAL * (linecount + 1) + VSPACING;
+ for (int line = 0; line < linecount; line++) {
+ Dimension line_size = GetStringBoundingBox(STR_FRAMERATE_GAMELOOP + line);
+ size->width = max(size->width, line_size.width);
+ }
+ break;
+ }
+
+ case WID_FRW_TIMES_CURRENT:
+ case WID_FRW_TIMES_AVERAGE: {
+ int linecount = PFE_MAX - PFE_FIRST;
+ *size = GetStringBoundingBox(STR_FRAMERATE_CURRENT + (widget - WID_FRW_TIMES_CURRENT));
+ SetDParam(0, 999999);
+ SetDParam(1, 2);
+ Dimension item_size = GetStringBoundingBox(STR_FRAMERATE_MS_GOOD);
+ size->width = max(size->width, item_size.width);
+ size->height += FONT_HEIGHT_NORMAL * linecount + VSPACING;
+ break;
+ }
+ }
+ }
+
+ /** Render a column of formatted average durations */
+ void DrawElementTimesColumn(const Rect &r, StringID heading_str, const CachedDecimal *values) const
+ {
+ int y = r.top;
+ DrawString(r.left, r.right, y, heading_str, TC_FROMSTRING, SA_CENTER);
+ y += FONT_HEIGHT_NORMAL + VSPACING;
+
+ for (PerformanceElement e = PFE_FIRST; e < PFE_MAX; e++) {
+ values[e].InsertDParams(0);
+ DrawString(r.left, r.right, y, values[e].strid, TC_FROMSTRING, SA_RIGHT);
+ y += FONT_HEIGHT_NORMAL;
+ }
+ }
+
+ virtual void DrawWidget(const Rect &r, int widget) const
+ {
+ switch (widget) {
+ case WID_FRW_TIMES_NAMES: {
+ /* Render a column of titles for performance element names */
+ int linecount = PFE_MAX - PFE_FIRST;
+ int y = r.top + FONT_HEIGHT_NORMAL + VSPACING; // first line contains headings in the value columns
+ for (int i = 0; i < linecount; i++) {
+ DrawString(r.left, r.right, y, STR_FRAMERATE_GAMELOOP + i, TC_FROMSTRING, SA_LEFT);
+ y += FONT_HEIGHT_NORMAL;
+ }
+ break;
+ }
+ case WID_FRW_TIMES_CURRENT:
+ /* Render short-term average values */
+ DrawElementTimesColumn(r, STR_FRAMERATE_CURRENT, this->times_shortterm);
+ break;
+ case WID_FRW_TIMES_AVERAGE:
+ /* Render averages of all recorded values */
+ DrawElementTimesColumn(r, STR_FRAMERATE_AVERAGE, this->times_longterm);
+ break;
+ }
+ }
+
+ virtual void OnClick(Point pt, int widget, int click_count)
+ {
+ switch (widget) {
+ case WID_FRW_TIMES_NAMES:
+ case WID_FRW_TIMES_CURRENT:
+ case WID_FRW_TIMES_AVERAGE: {
+ /* Open time graph windows when clicking detail measurement lines */
+ int line = this->GetRowFromWidget(pt.y, widget, VSPACING, FONT_HEIGHT_NORMAL);
+ if (line > 0) {
+ line -= 1;
+ ShowFrametimeGraphWindow((PerformanceElement)line);
+ }
+ break;
+ }
+ }
+ }
+};
+
+static WindowDesc _framerate_display_desc(
+ WDP_AUTO, "framerate_display", 60, 40,
+ WC_FRAMERATE_DISPLAY, WC_NONE,
+ 0,
+ _framerate_window_widgets, lengthof(_framerate_window_widgets)
+);
+
+
+enum FrametimeGraphWindowWidgets {
+ WID_FGW_CAPTION,
+ WID_FGW_GRAPH,
+};
+
+static const NWidgetPart _frametime_graph_window_widgets[] = {
+ NWidget(NWID_HORIZONTAL),
+ NWidget(WWT_CLOSEBOX, COLOUR_GREY),
+ NWidget(WWT_CAPTION, COLOUR_GREY, WID_FGW_CAPTION), SetDataTip(STR_WHITE_STRING, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS),
+ NWidget(WWT_STICKYBOX, COLOUR_GREY),
+ EndContainer(),
+ NWidget(WWT_PANEL, COLOUR_GREY),
+ NWidget(NWID_VERTICAL), SetPadding(6),
+ NWidget(WWT_EMPTY, COLOUR_GREY, WID_FGW_GRAPH),
+ EndContainer(),
+ EndContainer(),
+};
+
+struct FrametimeGraphWindow : Window {
+ int vertical_scale; ///< number of TIMESTAMP_PRECISION units vertically
+ int horizontal_scale; ///< number of half-second units horizontally
+ uint32 next_scale_update; ///< realtime tick for next scale update
+
+ PerformanceElement element; ///< what element this window renders graph for
+ Dimension graph_size; ///< size of the main graph area (excluding axis labels)
+
+ FrametimeGraphWindow(WindowDesc *desc, WindowNumber number) : Window(desc)
+ {
+ this->element = (PerformanceElement)number;
+ this->horizontal_scale = 4;
+ this->vertical_scale = TIMESTAMP_PRECISION / 10;
+ this->next_scale_update = 0;
+
+ this->InitNested(number);
+ }
+
+ virtual void SetStringParameters(int widget) const
+ {
+ switch (widget) {
+ case WID_FGW_CAPTION:
+ SetDParam(0, STR_FRAMETIME_CAPTION_GAMELOOP + this->element);
+ break;
+ }
+ }
+
+ virtual void UpdateWidgetSize(int widget, Dimension *size, const Dimension &padding, Dimension *fill, Dimension *resize)
+ {
+ if (widget == WID_FGW_GRAPH) {
+ SetDParam(0, 100);
+ Dimension size_ms_label = GetStringBoundingBox(STR_FRAMERATE_GRAPH_MILLISECONDS);
+ SetDParam(0, 100);
+ Dimension size_s_label = GetStringBoundingBox(STR_FRAMERATE_GRAPH_SECONDS);
+
+ /* Size graph in height to fit at least 10 vertical labels with space between, or at least 100 pixels */
+ graph_size.height = max<uint>(100, 10 * (size_ms_label.height + 1));
+ /* Always 2:1 graph area */
+ graph_size.width = 2 * graph_size.height;
+ *size = graph_size;
+
+ size->width += size_ms_label.width + 2;
+ size->height += size_s_label.height + 2;
+ }
+ }
+
+ void SelectHorizontalScale(TimingMeasurement range)
+ {
+ /* Determine horizontal scale based on period covered by 60 points
+ * (slightly less than 2 seconds at full game speed) */
+ struct ScaleDef { TimingMeasurement range; int scale; };
+ static const ScaleDef hscales[] = {
+ { 120, 60 },
+ { 10, 20 },
+ { 5, 10 },
+ { 3, 4 },
+ { 1, 2 },
+ };
+ for (const ScaleDef *sc = hscales; sc < hscales + lengthof(hscales); sc++) {
+ if (range < sc->range) this->horizontal_scale = sc->scale;
+ }
+ }
+
+ void SelectVerticalScale(TimingMeasurement range)
+ {
+ /* Determine vertical scale based on peak value (within the horizontal scale + a bit) */
+ static const TimingMeasurement vscales[] = {
+ TIMESTAMP_PRECISION * 100,
+ TIMESTAMP_PRECISION * 10,
+ TIMESTAMP_PRECISION * 5,
+ TIMESTAMP_PRECISION,
+ TIMESTAMP_PRECISION / 2,
+ TIMESTAMP_PRECISION / 5,
+ TIMESTAMP_PRECISION / 10,
+ TIMESTAMP_PRECISION / 50,
+ TIMESTAMP_PRECISION / 200,
+ };
+ for (const TimingMeasurement *sc = vscales; sc < vscales + lengthof(vscales); sc++) {
+ if (range < *sc) this->vertical_scale = (int)*sc;
+ }
+ }
+
+ /** Recalculate the graph scaling factors based on current recorded data */
+ void UpdateScale()
+ {
+ const TimingMeasurement *durations = _pf_data[this->element].durations;
+ const TimingMeasurement *timestamps = _pf_data[this->element].timestamps;
+ int num_valid = _pf_data[this->element].num_valid;
+ int point = _pf_data[this->element].prev_index;
+
+ TimingMeasurement lastts = timestamps[point];
+ TimingMeasurement time_sum = 0;
+ TimingMeasurement peak_value = 0;
+ int count = 0;
+
+ /* Sensible default for when too few measurements are available */
+ this->horizontal_scale = 4;
+
+ for (int i = 1; i < num_valid; i++) {
+ point--;
+ if (point < 0) point = NUM_FRAMERATE_POINTS - 1;
+
+ TimingMeasurement value = durations[point];
+ if (value == PerformanceData::INVALID_DURATION) {
+ /* Skip gaps in data by pretending time is continuous across them */
+ lastts = timestamps[point];
+ continue;
+ }
+ if (value > peak_value) peak_value = value;
+ count++;
+
+ /* Accumulate period of time covered by data */
+ time_sum += lastts - timestamps[point];
+ lastts = timestamps[point];
+
+ /* Enough data to select a range and get decent data density */
+ if (count == 60) this->SelectHorizontalScale(time_sum / TIMESTAMP_PRECISION);
+
+ /* End when enough points have been collected and the horizontal scale has been exceeded */
+ if (count >= 60 && time_sum >= (this->horizontal_scale + 2) * TIMESTAMP_PRECISION / 2) break;
+ }
+
+ this->SelectVerticalScale(peak_value);
+ }
+
+ virtual void OnTick()
+ {
+ this->SetDirty();
+
+ if (this->next_scale_update < _realtime_tick) {
+ this->next_scale_update = _realtime_tick + 500;
+ this->UpdateScale();
+ }
+ }
+
+ /** Scale and interpolate a value from a source range into a destination range */
+ template<typename T>
+ static inline T Scinterlate(T dst_min, T dst_max, T src_min, T src_max, T value)
+ {
+ T dst_diff = dst_max - dst_min;
+ T src_diff = src_max - src_min;
+ return (value - src_min) * dst_diff / src_diff + dst_min;
+ }
+
+ virtual void DrawWidget(const Rect &r, int widget) const
+ {
+ if (widget == WID_FGW_GRAPH) {
+ const TimingMeasurement *durations = _pf_data[this->element].durations;
+ const TimingMeasurement *timestamps = _pf_data[this->element].timestamps;
+ int point = _pf_data[this->element].prev_index;
+
+ const int x_zero = r.right - (int)this->graph_size.width;
+ const int x_max = r.right;
+ const int y_zero = r.top + (int)this->graph_size.height;
+ const int y_max = r.top;
+ const int c_grid = PC_DARK_GREY;
+ const int c_lines = PC_BLACK;
+ const int c_peak = PC_DARK_RED;
+
+ const TimingMeasurement draw_horz_scale = (TimingMeasurement)this->horizontal_scale * TIMESTAMP_PRECISION / 2;
+ const TimingMeasurement draw_vert_scale = (TimingMeasurement)this->vertical_scale;
+
+ /* Number of \c horizontal_scale units in each horizontal division */
+ const uint horz_div_scl = (this->horizontal_scale <= 20) ? 1 : 10;
+ /* Number of divisions of the horizontal axis */
+ const uint horz_divisions = this->horizontal_scale / horz_div_scl;
+ /* Number of divisions of the vertical axis */
+ const uint vert_divisions = 10;
+
+ /* Draw division lines and labels for the vertical axis */
+ for (uint division = 0; division < vert_divisions; division++) {
+ int y = Scinterlate(y_zero, y_max, 0, (int)vert_divisions, (int)division);
+ GfxDrawLine(x_zero, y, x_max, y, c_grid);
+ if (division % 2 == 0) {
+ if ((TimingMeasurement)this->vertical_scale > TIMESTAMP_PRECISION) {
+ SetDParam(0, this->vertical_scale * division / 10 / TIMESTAMP_PRECISION);
+ DrawString(r.left, x_zero - 2, y - FONT_HEIGHT_SMALL, STR_FRAMERATE_GRAPH_SECONDS, TC_GREY, SA_RIGHT | SA_FORCE, false, FS_SMALL);
+ } else {
+ SetDParam(0, this->vertical_scale * division / 10 * 1000 / TIMESTAMP_PRECISION);
+ DrawString(r.left, x_zero - 2, y - FONT_HEIGHT_SMALL, STR_FRAMERATE_GRAPH_MILLISECONDS, TC_GREY, SA_RIGHT | SA_FORCE, false, FS_SMALL);
+ }
+ }
+ }
+ /* Draw divison lines and labels for the horizontal axis */
+ for (uint division = horz_divisions; division > 0; division--) {
+ int x = Scinterlate(x_zero, x_max, 0, (int)horz_divisions, (int)horz_divisions - (int)division);
+ GfxDrawLine(x, y_max, x, y_zero, c_grid);
+ if (division % 2 == 0) {
+ SetDParam(0, division * horz_div_scl / 2);
+ DrawString(x, x_max, y_zero + 2, STR_FRAMERATE_GRAPH_SECONDS, TC_GREY, SA_LEFT | SA_FORCE, false, FS_SMALL);
+ }
+ }
+
+ /* Position of last rendered data point */
+ Point lastpoint = {
+ x_max,
+ (int)Scinterlate<int64>(y_zero, y_max, 0, this->vertical_scale, durations[point])
+ };
+ /* Timestamp of last rendered data point */
+ TimingMeasurement lastts = timestamps[point];
+
+ TimingMeasurement peak_value = 0;
+ Point peak_point = { 0, 0 };
+ TimingMeasurement value_sum = 0;
+ TimingMeasurement time_sum = 0;
+ int points_drawn = 0;
+
+ for (int i = 1; i < NUM_FRAMERATE_POINTS; i++) {
+ point--;
+ if (point < 0) point = NUM_FRAMERATE_POINTS - 1;
+
+ TimingMeasurement value = durations[point];
+ if (value == PerformanceData::INVALID_DURATION) {
+ /* Skip gaps in measurements, pretend the data points on each side are continuous */
+ lastts = timestamps[point];
+ continue;
+ }
+
+ /* Use total time period covered for value along horizontal axis */
+ time_sum += lastts - timestamps[point];
+ lastts = timestamps[point];
+ /* Stop if past the width of the graph */
+ if (time_sum > draw_horz_scale) break;
+
+ /* Draw line from previous point to new point */
+ Point newpoint = {
+ (int)Scinterlate<int64>(x_zero, x_max, 0, (int64)draw_horz_scale, (int64)draw_horz_scale - (int64)time_sum),
+ (int)Scinterlate<int64>(y_zero, y_max, 0, (int64)draw_vert_scale, (int64)value)
+ };
+ assert(newpoint.x <= lastpoint.x);
+ GfxDrawLine(lastpoint.x, lastpoint.y, newpoint.x, newpoint.y, c_lines);
+ lastpoint = newpoint;
+
+ /* Record peak and average value across graphed data */
+ value_sum += value;
+ points_drawn++;
+ if (value > peak_value) {
+ peak_value = value;
+ peak_point = newpoint;
+ }
+ }
+
+ /* If the peak value is significantly larger than the average, mark and label it */
+ if (points_drawn > 0 && peak_value > TIMESTAMP_PRECISION / 100 && 2 * peak_value > 3 * value_sum / points_drawn) {
+ TextColour tc_peak = (TextColour)(TC_IS_PALETTE_COLOUR | c_peak);
+ GfxFillRect(peak_point.x - 1, peak_point.y - 1, peak_point.x + 1, peak_point.y + 1, c_peak);
+ SetDParam(0, peak_value * 1000 / TIMESTAMP_PRECISION);
+ int label_y = max(y_max, peak_point.y - FONT_HEIGHT_SMALL);
+ if (peak_point.x - x_zero > (int)this->graph_size.width / 2) {
+ DrawString(x_zero, peak_point.x - 2, label_y, STR_FRAMERATE_GRAPH_MILLISECONDS, tc_peak, SA_RIGHT | SA_FORCE, false, FS_SMALL);
+ } else {
+ DrawString(peak_point.x + 2, x_max, label_y, STR_FRAMERATE_GRAPH_MILLISECONDS, tc_peak, SA_LEFT | SA_FORCE, false, FS_SMALL);
+ }
+ }
+ }
+ }
+};
+
+static WindowDesc _frametime_graph_window_desc(
+ WDP_AUTO, "frametime_graph", 140, 90,
+ WC_FRAMETIME_GRAPH, WC_NONE,
+ 0,
+ _frametime_graph_window_widgets, lengthof(_frametime_graph_window_widgets)
+);
+
+
+
+void ShowFramerateWindow()
+{
+ AllocateWindowDescFront<FramerateWindow>(&_framerate_display_desc, 0);
+}
+
+void ShowFrametimeGraphWindow(PerformanceElement elem)
+{
+ if (elem < PFE_FIRST || elem >= PFE_MAX) return; // maybe warn?
+ AllocateWindowDescFront<FrametimeGraphWindow>(&_frametime_graph_window_desc, elem, true);
+}
+
+void ConPrintFramerate()
+{
+ const int count1 = NUM_FRAMERATE_POINTS / 8;
+ const int count2 = NUM_FRAMERATE_POINTS / 4;
+ const int count3 = NUM_FRAMERATE_POINTS / 1;
+
+ IConsolePrintF(TC_SILVER, "Based on num. data points: %d %d %d", count1, count2, count3);
+
+ static const char *MEASUREMENT_NAMES[PFE_MAX] = {
+ "Game loop",
+ " GL station ticks",
+ " GL train ticks",
+ " GL road vehicle ticks",
+ " GL ship ticks",
+ " GL aircraft ticks",
+ " GL landscape ticks",
+ " GL link graph delays",
+ "Drawing",
+ " Viewport drawing",
+ "Video output",
+ "Sound mixing",
+ };
+
+ static const PerformanceElement rate_elements[] = { PFE_GAMELOOP, PFE_DRAWING, PFE_VIDEO };
+
+ bool printed_anything = false;
+
+ for (const PerformanceElement *e = rate_elements; e < rate_elements + lengthof(rate_elements); e++) {
+ auto &pf = _pf_data[*e];
+ if (pf.num_valid == 0) continue;
+ IConsolePrintF(TC_GREEN, "%s rate: %.2ffps (expected: %.2ffps)",
+ MEASUREMENT_NAMES[*e],
+ pf.GetRate(),
+ pf.expected_rate);
+ printed_anything = true;
+ }
+
+ for (PerformanceElement e = PFE_FIRST; e < PFE_MAX; e++) {
+ auto &pf = _pf_data[e];
+ if (pf.num_valid == 0) continue;
+ IConsolePrintF(TC_LIGHT_BLUE, "%s times: %.2fms %.2fms %.2fms",
+ MEASUREMENT_NAMES[e],
+ pf.GetAverageDurationMilliseconds(count1),
+ pf.GetAverageDurationMilliseconds(count2),
+ pf.GetAverageDurationMilliseconds(count3));
+ printed_anything = true;
+ }
+
+ if (!printed_anything) {
+ IConsoleWarning("No performance measurements have been taken yet");
+ }
+}
diff --git a/src/framerate_type.h b/src/framerate_type.h
new file mode 100644
index 000000000..295939efe
--- /dev/null
+++ b/src/framerate_type.h
@@ -0,0 +1,67 @@
+/* $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/>.
+*/
+
+#ifndef FRAMERATE_GUI_H
+#define FRAMERATE_GUI_H
+
+#include "stdafx.h"
+#include "core/enum_type.hpp"
+
+enum PerformanceElement {
+ PFE_FIRST = 0,
+ PFE_GAMELOOP = 0, ///< Speed of gameloop processing.
+ PFE_GL_ECONOMY, ///< Time spent processing cargo movement
+ PFE_GL_TRAINS, ///< Time spent processing trains
+ PFE_GL_ROADVEHS, ///< Time spend processing road vehicles
+ PFE_GL_SHIPS, ///< Time spent processing ships
+ PFE_GL_AIRCRAFT, ///< Time spent processing aircraft
+ PFE_GL_LANDSCAPE, ///< Time spent processing other world features
+ PFE_GL_LINKGRAPH, ///< Time spent waiting for link graph background jobs
+ PFE_DRAWING, ///< Speed of drawing world and GUI.
+ PFE_DRAWWORLD, ///< Time spent drawing world viewports in GUI
+ PFE_VIDEO, ///< Speed of painting drawn video buffer.
+ PFE_SOUND, ///< Speed of mixing audio samples
+ PFE_MAX, ///< End of enum, must be last.
+};
+DECLARE_POSTFIX_INCREMENT(PerformanceElement)
+
+typedef uint64 TimingMeasurement;
+
+/**
+ * RAII class for measuring simple elements of performance.
+ * Construct an object with the appropriate element parameter when processing begins,
+ * time is automatically taken when the object goes out of scope again.
+ */
+class PerformanceMeasurer {
+ PerformanceElement elem;
+ TimingMeasurement start_time;
+public:
+ PerformanceMeasurer(PerformanceElement elem);
+ ~PerformanceMeasurer();
+ void SetExpectedRate(double rate);
+ static void Paused(PerformanceElement elem);
+};
+
+/**
+ * RAII class for measuring multi-step elements of performance.
+ * At the beginning of a frame, call Reset on the element, then construct an object in the scope where
+ * each processing cycle happens. The measurements are summed between resets.
+ */
+class PerformanceAccumulator {
+ PerformanceElement elem;
+ TimingMeasurement start_time;
+public:
+ PerformanceAccumulator(PerformanceElement elem);
+ ~PerformanceAccumulator();
+ static void Reset(PerformanceElement elem);
+};
+
+void ShowFramerateWindow();
+
+#endif /* FRAMERATE_GUI_H */
diff --git a/src/landscape.cpp b/src/landscape.cpp
index a29c97b9d..18f27807d 100644
--- a/src/landscape.cpp
+++ b/src/landscape.cpp
@@ -32,6 +32,7 @@
#include "company_func.h"
#include "pathfinder/npf/aystar.h"
#include "saveload/saveload.h"
+#include "framerate_type.h"
#include <list>
#include <set>
@@ -720,6 +721,8 @@ TileIndex _cur_tileloop_tile;
*/
void RunTileLoop()
{
+ PerformanceAccumulator framerate(PFE_GL_LANDSCAPE);
+
/* The pseudorandom sequence of tiles is generated using a Galois linear feedback
* shift register (LFSR). This allows a deterministic pseudorandom ordering, but
* still with minimal state and fast iteration. */
@@ -1303,10 +1306,14 @@ void OnTick_LinkGraph();
void CallLandscapeTick()
{
- OnTick_Town();
- OnTick_Trees();
- OnTick_Station();
- OnTick_Industry();
+ {
+ PerformanceAccumulator framerate(PFE_GL_LANDSCAPE);
+
+ OnTick_Town();
+ OnTick_Trees();
+ OnTick_Station();
+ OnTick_Industry();
+ }
OnTick_Companies();
OnTick_LinkGraph();
diff --git a/src/lang/english.txt b/src/lang/english.txt
index 5344ef7d4..1be138706 100644
--- a/src/lang/english.txt
+++ b/src/lang/english.txt
@@ -475,6 +475,7 @@ STR_ABOUT_MENU_SCREENSHOT :Screenshot
STR_ABOUT_MENU_ZOOMIN_SCREENSHOT :Fully zoomed in screenshot
STR_ABOUT_MENU_DEFAULTZOOM_SCREENSHOT :Default zoom screenshot
STR_ABOUT_MENU_GIANT_SCREENSHOT :Whole map screenshot
+STR_ABOUT_MENU_SHOW_FRAMERATE :Show frame rate
STR_ABOUT_MENU_ABOUT_OPENTTD :About 'OpenTTD'
STR_ABOUT_MENU_SPRITE_ALIGNER :Sprite aligner
STR_ABOUT_MENU_TOGGLE_BOUNDING_BOXES :Toggle bounding boxes
@@ -2697,6 +2698,56 @@ STR_ABOUT_ORIGINAL_COPYRIGHT :{BLACK}Original
STR_ABOUT_VERSION :{BLACK}OpenTTD version {REV}
STR_ABOUT_COPYRIGHT_OPENTTD :{BLACK}OpenTTD {COPYRIGHT} 2002-2018 The OpenTTD team
+# Framerate display window
+STR_FRAMERATE_CAPTION :{WHITE}Frame rate
+STR_FRAMERATE_CAPTION_SMALL :{STRING2}{WHITE} ({DECIMAL}x)
+STR_FRAMERATE_RATE_GAMELOOP :{WHITE}Simulation rate: {STRING2}
+STR_FRAMERATE_RATE_GAMELOOP_TOOLTIP :{BLACK}Number of game ticks simulated per second.
+STR_FRAMERATE_RATE_BLITTER :{WHITE}Graphics frame rate: {STRING2}
+STR_FRAMERATE_RATE_BLITTER_TOOLTIP :{BLACK}Number of video frames rendered per second.
+STR_FRAMERATE_SPEED_FACTOR :{WHITE}Current game speed factor: {DECIMAL}x
+STR_FRAMERATE_SPEED_FACTOR_TOOLTIP :{BLACK}How fast the game is currently running, compared to the expected speed at normal simulation rate.
+STR_FRAMERATE_CURRENT :{WHITE}Current
+STR_FRAMERATE_AVERAGE :{WHITE}Average
+STR_FRAMERATE_DATA_POINTS :{WHITE}Data based on {COMMA} measurements
+STR_FRAMERATE_MS_GOOD :{LTBLUE}{DECIMAL}{WHITE} ms
+STR_FRAMERATE_MS_WARN :{YELLOW}{DECIMAL}{WHITE} ms
+STR_FRAMERATE_MS_BAD :{RED}{DECIMAL}{WHITE} ms
+STR_FRAMERATE_FPS_GOOD :{LTBLUE}{DECIMAL}{WHITE} frames/s
+STR_FRAMERATE_FPS_WARN :{YELLOW}{DECIMAL}{WHITE} frames/s
+STR_FRAMERATE_FPS_BAD :{RED}{DECIMAL}{WHITE} frames/s
+STR_FRAMERATE_GRAPH_MILLISECONDS :{TINY_FONT}{COMMA} ms
+STR_FRAMERATE_GRAPH_SECONDS :{TINY_FONT}{COMMA} s
+############ Leave those lines in this order!!
+STR_FRAMERATE_GAMELOOP :{WHITE}Game loop total:
+STR_FRAMERATE_GL_ECONOMY :{WHITE} Cargo handling:
+STR_FRAMERATE_GL_TRAINS :{WHITE} Train ticks:
+STR_FRAMERATE_GL_ROADVEHS :{WHITE} Road vehicle ticks:
+STR_FRAMERATE_GL_SHIPS :{WHITE} Ship ticks:
+STR_FRAMERATE_GL_AIRCRAFT :{WHITE} Aircraft ticks:
+STR_FRAMERATE_GL_LANDSCAPE :{WHITE} World ticks:
+STR_FRAMERATE_GL_LINKGRAPH :{WHITE} Link graph delay:
+STR_FRAMERATE_DRAWING :{WHITE}Graphics rendering:
+STR_FRAMERATE_DRAWING_VIEWPORTS :{WHITE} World viewports:
+STR_FRAMERATE_VIDEO :{WHITE}Video output:
+STR_FRAMERATE_SOUND :{WHITE}Sound mixing:
+############ End of leave-in-this-order
+############ Leave those lines in this order!!
+STR_FRAMETIME_CAPTION_GAMELOOP :Game loop
+STR_FRAMETIME_CAPTION_GL_ECONOMY :Cargo handling
+STR_FRAMETIME_CAPTION_GL_TRAINS :Train ticks
+STR_FRAMETIME_CAPTION_GL_ROADVEHS :Road vehicle ticks
+STR_FRAMETIME_CAPTION_GL_SHIPS :Ship ticks
+STR_FRAMETIME_CAPTION_GL_AIRCRAFT :Aircraft ticks
+STR_FRAMETIME_CAPTION_GL_LANDSCAPE :World ticks
+STR_FRAMETIME_CAPTION_GL_LINKGRAPH :Link graph delay
+STR_FRAMETIME_CAPTION_DRAWING :Graphics rendering
+STR_FRAMETIME_CAPTION_DRAWING_VIEWPORTS :World viewport rendering
+STR_FRAMETIME_CAPTION_VIDEO :Video output
+STR_FRAMETIME_CAPTION_SOUND :Sound mixing
+############ End of leave-in-this-order
+
+
# Save/load game/scenario
STR_SAVELOAD_SAVE_CAPTION :{WHITE}Save Game
STR_SAVELOAD_LOAD_CAPTION :{WHITE}Load Game
diff --git a/src/linkgraph/linkgraphschedule.cpp b/src/linkgraph/linkgraphschedule.cpp
index a65783a5e..8c508d817 100644
--- a/src/linkgraph/linkgraphschedule.cpp
+++ b/src/linkgraph/linkgraphschedule.cpp
@@ -15,6 +15,7 @@
#include "demands.h"
#include "mcf.h"
#include "flowmapper.h"
+#include "../framerate_type.h"
#include "../safeguards.h"
@@ -151,6 +152,7 @@ void OnTick_LinkGraph()
if (offset == 0) {
LinkGraphSchedule::instance.SpawnNext();
} else if (offset == _settings_game.linkgraph.recalc_interval / 2) {
+ PerformanceMeasurer framerate(PFE_GL_LINKGRAPH);
LinkGraphSchedule::instance.JoinNext();
}
}
diff --git a/src/mixer.cpp b/src/mixer.cpp
index 5945bd235..6aaa8204d 100644
--- a/src/mixer.cpp
+++ b/src/mixer.cpp
@@ -12,6 +12,7 @@
#include "stdafx.h"
#include <math.h>
#include "core/math_func.hpp"
+#include "framerate_type.h"
#include "safeguards.h"
@@ -138,6 +139,13 @@ static void MxCloseChannel(MixerChannel *mc)
void MxMixSamples(void *buffer, uint samples)
{
+ PerformanceMeasurer framerate(PFE_SOUND);
+ static uint last_samples = 0;
+ if (samples != last_samples) {
+ framerate.SetExpectedRate((double)_play_rate / samples);
+ last_samples = samples;
+ }
+
MixerChannel *mc;
/* Clear the buffer */
diff --git a/src/openttd.cpp b/src/openttd.cpp
index 43c6b1170..510f00427 100644
--- a/src/openttd.cpp
+++ b/src/openttd.cpp
@@ -63,6 +63,7 @@
#include "subsidy_func.h"
#include "gfx_layout.h"
#include "viewport_sprite_sorter.h"
+#include "framerate_type.h"
#include "linkgraph/linkgraphschedule.h"
@@ -1336,6 +1337,14 @@ void StateGameLoop()
{
/* don't execute the state loop during pause */
if (_pause_mode != PM_UNPAUSED) {
+ PerformanceMeasurer::Paused(PFE_GAMELOOP);
+ PerformanceMeasurer::Paused(PFE_GL_ECONOMY);
+ PerformanceMeasurer::Paused(PFE_GL_TRAINS);
+ PerformanceMeasurer::Paused(PFE_GL_ROADVEHS);
+ PerformanceMeasurer::Paused(PFE_GL_SHIPS);
+ PerformanceMeasurer::Paused(PFE_GL_AIRCRAFT);
+ PerformanceMeasurer::Paused(PFE_GL_LANDSCAPE);
+
UpdateLandscapingLimits();
#ifndef DEBUG_DUMP_COMMANDS
Game::GameLoop();
@@ -1343,6 +1352,9 @@ void StateGameLoop()
CallWindowTickEvent();
return;
}
+
+ PerformanceMeasurer framerate(PFE_GAMELOOP);
+ PerformanceAccumulator::Reset(PFE_GL_LANDSCAPE);
if (HasModalProgress()) return;
Layouter::ReduceLineCache();
diff --git a/src/roadveh_cmd.cpp b/src/roadveh_cmd.cpp
index 7d0007fd2..ed67a5221 100644
--- a/src/roadveh_cmd.cpp
+++ b/src/roadveh_cmd.cpp
@@ -35,6 +35,7 @@
#include "core/backup_type.hpp"
#include "newgrf.h"
#include "zoom_func.h"
+#include "framerate_type.h"
#include "table/strings.h"
@@ -1587,6 +1588,8 @@ Money RoadVehicle::GetRunningCost() const
bool RoadVehicle::Tick()
{
+ PerformanceAccumulator framerate(PFE_GL_ROADVEHS);
+
this->tick_counter++;
if (this->IsFrontEngine()) {
diff --git a/src/ship_cmd.cpp b/src/ship_cmd.cpp
index 0bdcf7041..ca8bdfc8e 100644
--- a/src/ship_cmd.cpp
+++ b/src/ship_cmd.cpp
@@ -34,6 +34,7 @@
#include "company_base.h"
#include "tunnelbridge_map.h"
#include "zoom_func.h"
+#include "framerate_type.h"
#include "table/strings.h"
@@ -638,6 +639,8 @@ reverse_direction:
bool Ship::Tick()
{
+ PerformanceAccumulator framerate(PFE_GL_SHIPS);
+
if (!(this->vehstatus & VS_STOPPED)) this->running_ticks++;
ShipController(this);
diff --git a/src/toolbar_gui.cpp b/src/toolbar_gui.cpp
index 7966d2898..40ac3634f 100644
--- a/src/toolbar_gui.cpp
+++ b/src/toolbar_gui.cpp
@@ -47,6 +47,7 @@
#include "goal_base.h"
#include "story_base.h"
#include "toolbar_gui.h"
+#include "framerate_type.h"
#include "widgets/toolbar_widget.h"
@@ -1045,7 +1046,7 @@ static CallBackFunction PlaceLandBlockInfo()
static CallBackFunction ToolbarHelpClick(Window *w)
{
- PopupMainToolbMenu(w, WID_TN_HELP, STR_ABOUT_MENU_LAND_BLOCK_INFO, _settings_client.gui.newgrf_developer_tools ? 12 : 9);
+ PopupMainToolbMenu(w, WID_TN_HELP, STR_ABOUT_MENU_LAND_BLOCK_INFO, _settings_client.gui.newgrf_developer_tools ? 13 : 10);
return CBF_NONE;
}
@@ -1147,10 +1148,11 @@ static CallBackFunction MenuClickHelp(int index)
case 5: MenuClickLargeWorldScreenshot(SC_ZOOMEDIN); break;
case 6: MenuClickLargeWorldScreenshot(SC_DEFAULTZOOM); break;
case 7: MenuClickLargeWorldScreenshot(SC_WORLD); break;
- case 8: ShowAboutWindow(); break;
- case 9: ShowSpriteAlignerWindow(); break;
- case 10: ToggleBoundingBoxes(); break;
- case 11: ToggleDirtyBlocks(); break;
+ case 8: ShowFramerateWindow(); break;
+ case 9: ShowAboutWindow(); break;
+ case 10: ShowSpriteAlignerWindow(); break;
+ case 11: ToggleBoundingBoxes(); break;
+ case 12: ToggleDirtyBlocks(); break;
}
return CBF_NONE;
}
diff --git a/src/train_cmd.cpp b/src/train_cmd.cpp
index daebb9789..60a7b2ca9 100644
--- a/src/train_cmd.cpp
+++ b/src/train_cmd.cpp
@@ -35,6 +35,7 @@
#include "order_backup.h"
#include "zoom_func.h"
#include "newgrf_debug.h"
+#include "framerate_type.h"
#include "table/strings.h"
#include "table/train_cmd.h"
@@ -3902,6 +3903,8 @@ Money Train::GetRunningCost() const
*/
bool Train::Tick()
{
+ PerformanceAccumulator framerate(PFE_GL_TRAINS);
+
this->tick_counter++;
if (this->IsFrontEngine()) {
diff --git a/src/vehicle.cpp b/src/vehicle.cpp
index b4c790722..ae826a50c 100644
--- a/src/vehicle.cpp
+++ b/src/vehicle.cpp
@@ -52,6 +52,7 @@
#include "gamelog.h"
#include "linkgraph/linkgraph.h"
#include "linkgraph/refresh.h"
+#include "framerate_type.h"
#include "table/strings.h"
@@ -945,8 +946,15 @@ void CallVehicleTicks()
RunVehicleDayProc();
- Station *st;
- FOR_ALL_STATIONS(st) LoadUnloadStation(st);
+ {
+ PerformanceMeasurer framerate(PFE_GL_ECONOMY);
+ Station *st;
+ FOR_ALL_STATIONS(st) LoadUnloadStation(st);
+ }
+ PerformanceAccumulator::Reset(PFE_GL_TRAINS);
+ PerformanceAccumulator::Reset(PFE_GL_ROADVEHS);
+ PerformanceAccumulator::Reset(PFE_GL_SHIPS);
+ PerformanceAccumulator::Reset(PFE_GL_AIRCRAFT);
Vehicle *v;
FOR_ALL_VEHICLES(v) {
diff --git a/src/video/allegro_v.cpp b/src/video/allegro_v.cpp
index 50fe0c499..960d7fb7c 100644
--- a/src/video/allegro_v.cpp
+++ b/src/video/allegro_v.cpp
@@ -24,6 +24,7 @@
#include "../network/network.h"
#include "../core/random_func.hpp"
#include "../core/math_func.hpp"
+#include "../framerate_type.h"
#include "allegro_v.h"
#include <allegro.h>
@@ -56,6 +57,8 @@ void VideoDriver_Allegro::MakeDirty(int left, int top, int width, int height)
static void DrawSurfaceToScreen()
{
+ PerformanceMeasurer framerate(PFE_VIDEO);
+
int n = _num_dirty_rects;
if (n == 0) return;
diff --git a/src/video/cocoa/wnd_quartz.mm b/src/video/cocoa/wnd_quartz.mm
index 7bec27824..4245a3c18 100644
--- a/src/video/cocoa/wnd_quartz.mm
+++ b/src/video/cocoa/wnd_quartz.mm
@@ -33,6 +33,7 @@
#include "cocoa_v.h"
#include "../../core/math_func.hpp"
#include "../../gfx_func.h"
+#include "../../framerate_type.h"
/* On some old versions of MAC OS this may not be defined.
* Those versions generally only produce code for PPC. So it should be safe to
@@ -431,6 +432,8 @@ WindowQuartzSubdriver::~WindowQuartzSubdriver()
void WindowQuartzSubdriver::Draw(bool force_update)
{
+ PerformanceMeasurer framerate(PFE_VIDEO);
+
/* Check if we need to do anything */
if (this->num_dirty_rects == 0 || [ this->window isMiniaturized ]) return;
diff --git a/src/video/cocoa/wnd_quickdraw.mm b/src/video/cocoa/wnd_quickdraw.mm
index df10d8773..8475efb0f 100644
--- a/src/video/cocoa/wnd_quickdraw.mm
+++ b/src/video/cocoa/wnd_quickdraw.mm
@@ -32,6 +32,7 @@
#include "cocoa_v.h"
#include "../../core/math_func.hpp"
#include "../../gfx_func.h"
+#include "../../framerate_type.h"
/**
* Important notice regarding all modifications!!!!!!!
@@ -361,6 +362,8 @@ WindowQuickdrawSubdriver::~WindowQuickdrawSubdriver()
void WindowQuickdrawSubdriver::Draw(bool force_update)
{
+ PerformanceMeasurer framerate(PFE_VIDEO);
+
/* Check if we need to do anything */
if (this->num_dirty_rects == 0 || [ this->window isMiniaturized ]) return;
diff --git a/src/video/sdl_v.cpp b/src/video/sdl_v.cpp
index 81005ab04..f40c4b646 100644
--- a/src/video/sdl_v.cpp
+++ b/src/video/sdl_v.cpp
@@ -23,6 +23,7 @@
#include "../core/random_func.hpp"
#include "../core/math_func.hpp"
#include "../fileio_func.h"
+#include "../framerate_type.h"
#include "sdl_v.h"
#include <SDL.h>
@@ -148,6 +149,8 @@ static void CheckPaletteAnim()
static void DrawSurfaceToScreen()
{
+ PerformanceMeasurer framerate(PFE_VIDEO);
+
int n = _num_dirty_rects;
if (n == 0) return;
diff --git a/src/video/win32_v.cpp b/src/video/win32_v.cpp
index 9559163c3..0655065a6 100644
--- a/src/video/win32_v.cpp
+++ b/src/video/win32_v.cpp
@@ -23,6 +23,7 @@
#include "../progress.h"
#include "../window_gui.h"
#include "../window_func.h"
+#include "../framerate_type.h"
#include "win32_v.h"
#include <windows.h>
#include <imm.h>
@@ -359,6 +360,8 @@ bool VideoDriver_Win32::MakeWindow(bool full_screen)
/** Do palette animation and blit to the window. */
static void PaintWindow(HDC dc)
{
+ PerformanceMeasurer framerate(PFE_VIDEO);
+
HDC dc2 = CreateCompatibleDC(dc);
HBITMAP old_bmp = (HBITMAP)SelectObject(dc2, _wnd.dib_sect);
HPALETTE old_palette = SelectPalette(dc, _wnd.gdi_palette, FALSE);
diff --git a/src/viewport.cpp b/src/viewport.cpp
index 07d4e92fd..cb0b36f40 100644
--- a/src/viewport.cpp
+++ b/src/viewport.cpp
@@ -87,6 +87,7 @@
#include "company_base.h"
#include "command_func.h"
#include "network/network_func.h"
+#include "framerate_type.h"
#include <map>
@@ -1654,6 +1655,8 @@ static inline void ViewportDraw(const ViewPort *vp, int left, int top, int right
*/
void Window::DrawViewport() const
{
+ PerformanceAccumulator framerate(PFE_DRAWWORLD);
+
DrawPixelInfo *dpi = _cur_dpi;
dpi->left += this->left;
diff --git a/src/window.cpp b/src/window.cpp
index e2ce84542..d46447d52 100644
--- a/src/window.cpp
+++ b/src/window.cpp
@@ -36,6 +36,7 @@
#include "error.h"
#include "game/game.hpp"
#include "video/video_driver.hpp"
+#include "framerate_type.h"
#include "safeguards.h"
@@ -3077,6 +3078,9 @@ void InputLoop()
*/
void UpdateWindows()
{
+ PerformanceMeasurer framerate(PFE_DRAWING);
+ PerformanceAccumulator::Reset(PFE_DRAWWORLD);
+
Window *w;
static int highlight_timer = 1;
diff --git a/src/window_type.h b/src/window_type.h
index 809e81d48..c90ef51b6 100644
--- a/src/window_type.h
+++ b/src/window_type.h
@@ -681,6 +681,18 @@ enum WindowClass {
*/
WC_SAVE_PRESET,
+ /**
+ * Framerate display; %Window numbers:
+ * - 0 = #FramerateDisplayWidgets
+ */
+ WC_FRAMERATE_DISPLAY,
+
+ /**
+ * Frame time graph; %Window numbers:
+ * - 0 = #FramerateDisplayWidgets
+ */
+ WC_FRAMETIME_GRAPH,
+
WC_INVALID = 0xFFFF, ///< Invalid window.
};