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.
pull/73/head
Niels Martin Hansen 6 years ago committed by Patric Stout
parent a3d1950b65
commit 2a868b9f3b

@ -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

@ -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" />

@ -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>

@ -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" />

@ -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>

@ -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" />

@ -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>

@ -1110,6 +1110,10 @@
RelativePath=".\..\src\fontdetection.h"
>
</File>
<File
RelativePath=".\..\src\framerate_type.h"
>
</File>
<File
RelativePath=".\..\src\base_consist.h"
>
@ -2118,6 +2122,10 @@
RelativePath=".\..\src\fios_gui.cpp"
>
</File>
<File
RelativePath=".\..\src\framerate_gui.cpp"
>
</File>
<File
RelativePath=".\..\src\genworld_gui.cpp"
>

@ -1107,6 +1107,10 @@
RelativePath=".\..\src\fontdetection.h"
>
</File>
<File
RelativePath=".\..\src\framerate_type.h"
>
</File>
<File
RelativePath=".\..\src\base_consist.h"
>
@ -2115,6 +2119,10 @@
RelativePath=".\..\src\fios_gui.cpp"
>
</File>
<File
RelativePath=".\..\src\framerate_gui.cpp"
>
</File>
<File
RelativePath=".\..\src\genworld_gui.cpp"
>

@ -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

@ -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++;

@ -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;

@ -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);

@ -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");
}
}

@ -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 */

@ -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();

@ -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

@ -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();
}
}

@ -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 */

@ -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();

@ -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()) {

@ -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);

@ -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;
}

@ -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()) {

@ -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) {

@ -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;

@ -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;

@ -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;

@ -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;

@ -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);

@ -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;

@ -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;

@ -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.
};

Loading…
Cancel
Save