2 * This file is part of OpenTTD.
3 * 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.
4 * 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.
5 * 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/>.
8 /** @file framerate_gui.cpp GUI for displaying framerate/game speed information. */
10 #include "framerate_type.h"
13 #include "window_gui.h"
14 #include "window_func.h"
15 #include "table/sprites.h"
16 #include "string_func.h"
17 #include "strings_func.h"
18 #include "console_func.h"
19 #include "console_type.h"
20 #include "guitimer_func.h"
21 #include "company_base.h"
22 #include "ai/ai_info.hpp"
23 #include "ai/ai_instance.hpp"
24 #include "game/game.hpp"
25 #include "game/game_instance.hpp"
27 #include "widgets/framerate_widget.h"
28 #include "safeguards.h"
32 * Private declarations for performance measurement implementation
36 /** Number of data points to keep in buffer for each performance measurement */
37 const int NUM_FRAMERATE_POINTS
= 512;
38 /** %Units a second is divided into in performance measurements */
39 const TimingMeasurement TIMESTAMP_PRECISION
= 1000000;
41 struct PerformanceData
{
42 /** Duration value indicating the value is not valid should be considered a gap in measurements */
43 static const TimingMeasurement INVALID_DURATION
= UINT64_MAX
;
45 /** Time spent processing each cycle of the performance element, circular buffer */
46 TimingMeasurement durations
[NUM_FRAMERATE_POINTS
];
47 /** Start time of each cycle of the performance element, circular buffer */
48 TimingMeasurement timestamps
[NUM_FRAMERATE_POINTS
];
49 /** Expected number of cycles per second when the system is running without slowdowns */
51 /** Next index to write to in \c durations and \c timestamps */
53 /** Last index written to in \c durations and \c timestamps */
55 /** Number of data points recorded, clamped to \c NUM_FRAMERATE_POINTS */
58 /** Current accumulated duration */
59 TimingMeasurement acc_duration
;
60 /** Start time for current accumulation cycle */
61 TimingMeasurement acc_timestamp
;
64 * Initialize a data element with an expected collection rate
65 * @param expected_rate
66 * Expected number of cycles per second of the performance element. Use 1 if unknown or not relevant.
67 * The rate is used for highlighting slow-running elements in the GUI.
69 explicit PerformanceData(double expected_rate
) : expected_rate(expected_rate
), next_index(0), prev_index(0), num_valid(0) { }
71 /** Collect a complete measurement, given start and ending times for a processing block */
72 void Add(TimingMeasurement start_time
, TimingMeasurement end_time
)
74 this->durations
[this->next_index
] = end_time
- start_time
;
75 this->timestamps
[this->next_index
] = start_time
;
76 this->prev_index
= this->next_index
;
77 this->next_index
+= 1;
78 if (this->next_index
>= NUM_FRAMERATE_POINTS
) this->next_index
= 0;
79 this->num_valid
= std::min(NUM_FRAMERATE_POINTS
, this->num_valid
+ 1);
82 /** Begin an accumulation of multiple measurements into a single value, from a given start time */
83 void BeginAccumulate(TimingMeasurement start_time
)
85 this->timestamps
[this->next_index
] = this->acc_timestamp
;
86 this->durations
[this->next_index
] = this->acc_duration
;
87 this->prev_index
= this->next_index
;
88 this->next_index
+= 1;
89 if (this->next_index
>= NUM_FRAMERATE_POINTS
) this->next_index
= 0;
90 this->num_valid
= std::min(NUM_FRAMERATE_POINTS
, this->num_valid
+ 1);
92 this->acc_duration
= 0;
93 this->acc_timestamp
= start_time
;
96 /** Accumulate a period onto the current measurement */
97 void AddAccumulate(TimingMeasurement duration
)
99 this->acc_duration
+= duration
;
102 /** Indicate a pause/expected discontinuity in processing the element */
103 void AddPause(TimingMeasurement start_time
)
105 if (this->durations
[this->prev_index
] != INVALID_DURATION
) {
106 this->timestamps
[this->next_index
] = start_time
;
107 this->durations
[this->next_index
] = INVALID_DURATION
;
108 this->prev_index
= this->next_index
;
109 this->next_index
+= 1;
110 if (this->next_index
>= NUM_FRAMERATE_POINTS
) this->next_index
= 0;
111 this->num_valid
+= 1;
115 /** Get average cycle processing time over a number of data points */
116 double GetAverageDurationMilliseconds(int count
)
118 count
= std::min(count
, this->num_valid
);
120 int first_point
= this->prev_index
- count
;
121 if (first_point
< 0) first_point
+= NUM_FRAMERATE_POINTS
;
123 /* Sum durations, skipping invalid points */
125 for (int i
= first_point
; i
< first_point
+ count
; i
++) {
126 auto d
= this->durations
[i
% NUM_FRAMERATE_POINTS
];
127 if (d
!= INVALID_DURATION
) {
130 /* Don't count the invalid durations */
135 if (count
== 0) return 0; // avoid div by zero
136 return sumtime
* 1000 / count
/ TIMESTAMP_PRECISION
;
139 /** Get current rate of a performance element, based on approximately the past one second of data */
142 /* Start at last recorded point, end at latest when reaching the earliest recorded point */
143 int point
= this->prev_index
;
144 int last_point
= this->next_index
- this->num_valid
;
145 if (last_point
< 0) last_point
+= NUM_FRAMERATE_POINTS
;
147 /* Number of data points collected */
149 /* Time of previous data point */
150 TimingMeasurement last
= this->timestamps
[point
];
151 /* Total duration covered by collected points */
152 TimingMeasurement total
= 0;
154 /* We have nothing to compare the first point against */
156 if (point
< 0) point
= NUM_FRAMERATE_POINTS
- 1;
158 while (point
!= last_point
) {
159 /* Only record valid data points, but pretend the gaps in measurements aren't there */
160 if (this->durations
[point
] != INVALID_DURATION
) {
161 total
+= last
- this->timestamps
[point
];
164 last
= this->timestamps
[point
];
165 if (total
>= TIMESTAMP_PRECISION
) break; // end after 1 second has been collected
167 if (point
< 0) point
= NUM_FRAMERATE_POINTS
- 1;
170 if (total
== 0 || count
== 0) return 0;
171 return (double)count
* TIMESTAMP_PRECISION
/ total
;
175 /** %Game loop rate, cycles per second */
176 static const double GL_RATE
= 1000.0 / MILLISECONDS_PER_TICK
;
179 * Storage for all performance element measurements.
180 * Elements are initialized with the expected rate in recorded values per second.
183 PerformanceData _pf_data
[PFE_MAX
] = {
184 PerformanceData(GL_RATE
), // PFE_GAMELOOP
185 PerformanceData(1), // PFE_ACC_GL_ECONOMY
186 PerformanceData(1), // PFE_ACC_GL_TRAINS
187 PerformanceData(1), // PFE_ACC_GL_ROADVEHS
188 PerformanceData(1), // PFE_ACC_GL_SHIPS
189 PerformanceData(1), // PFE_ACC_GL_AIRCRAFT
190 PerformanceData(1), // PFE_GL_LANDSCAPE
191 PerformanceData(1), // PFE_GL_LINKGRAPH
192 PerformanceData(1000.0 / 30), // PFE_DRAWING
193 PerformanceData(1), // PFE_ACC_DRAWWORLD
194 PerformanceData(60.0), // PFE_VIDEO
195 PerformanceData(1000.0 * 8192 / 44100), // PFE_SOUND
196 PerformanceData(1), // PFE_ALLSCRIPTS
197 PerformanceData(1), // PFE_GAMESCRIPT
198 PerformanceData(1), // PFE_AI0 ...
212 PerformanceData(1), // PFE_AI14
219 * Return a timestamp with \c TIMESTAMP_PRECISION ticks per second precision.
220 * The basis of the timestamp is implementation defined, but the value should be steady,
221 * so differences can be taken to reliably measure intervals.
223 static TimingMeasurement
GetPerformanceTimer()
225 using namespace std::chrono
;
226 return (TimingMeasurement
)time_point_cast
<microseconds
>(high_resolution_clock::now()).time_since_epoch().count();
231 * Begin a cycle of a measured element.
232 * @param elem The element to be measured
234 PerformanceMeasurer::PerformanceMeasurer(PerformanceElement elem
)
236 assert(elem
< PFE_MAX
);
239 this->start_time
= GetPerformanceTimer();
242 /** Finish a cycle of a measured element and store the measurement taken. */
243 PerformanceMeasurer::~PerformanceMeasurer()
245 if (this->elem
== PFE_ALLSCRIPTS
) {
246 /* Hack to not record scripts total when no scripts are active */
247 bool any_active
= _pf_data
[PFE_GAMESCRIPT
].num_valid
> 0;
248 for (uint e
= PFE_AI0
; e
< PFE_MAX
; e
++) any_active
|= _pf_data
[e
].num_valid
> 0;
250 PerformanceMeasurer::SetInactive(PFE_ALLSCRIPTS
);
254 _pf_data
[this->elem
].Add(this->start_time
, GetPerformanceTimer());
257 /** Set the rate of expected cycles per second of a performance element. */
258 void PerformanceMeasurer::SetExpectedRate(double rate
)
260 _pf_data
[this->elem
].expected_rate
= rate
;
263 /** Mark a performance element as not currently in use. */
264 /* static */ void PerformanceMeasurer::SetInactive(PerformanceElement elem
)
266 _pf_data
[elem
].num_valid
= 0;
267 _pf_data
[elem
].next_index
= 0;
268 _pf_data
[elem
].prev_index
= 0;
272 * Indicate that a cycle of "pause" where no processing occurs.
273 * @param elem The element not currently being processed
275 /* static */ void PerformanceMeasurer::Paused(PerformanceElement elem
)
277 PerformanceMeasurer::SetInactive(elem
);
278 _pf_data
[elem
].AddPause(GetPerformanceTimer());
283 * Begin measuring one block of the accumulating value.
284 * @param elem The element to be measured
286 PerformanceAccumulator::PerformanceAccumulator(PerformanceElement elem
)
288 assert(elem
< PFE_MAX
);
291 this->start_time
= GetPerformanceTimer();
294 /** Finish and add one block of the accumulating value. */
295 PerformanceAccumulator::~PerformanceAccumulator()
297 _pf_data
[this->elem
].AddAccumulate(GetPerformanceTimer() - this->start_time
);
301 * Store the previous accumulator value and reset for a new cycle of accumulating measurements.
302 * @note This function must be called once per frame, otherwise measurements are not collected.
303 * @param elem The element to begin a new measurement cycle of
305 void PerformanceAccumulator::Reset(PerformanceElement elem
)
307 _pf_data
[elem
].BeginAccumulate(GetPerformanceTimer());
311 void ShowFrametimeGraphWindow(PerformanceElement elem
);
314 static const PerformanceElement DISPLAY_ORDER_PFE
[PFE_MAX
] = {
346 static const char * GetAIName(int ai_index
)
348 if (!Company::IsValidAiID(ai_index
)) return "";
349 return Company::Get(ai_index
)->ai_info
->GetName();
352 /** @hideinitializer */
353 static const NWidgetPart _framerate_window_widgets
[] = {
354 NWidget(NWID_HORIZONTAL
),
355 NWidget(WWT_CLOSEBOX
, COLOUR_GREY
),
356 NWidget(WWT_CAPTION
, COLOUR_GREY
, WID_FRW_CAPTION
), SetDataTip(STR_FRAMERATE_CAPTION
, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS
),
357 NWidget(WWT_SHADEBOX
, COLOUR_GREY
),
358 NWidget(WWT_STICKYBOX
, COLOUR_GREY
),
360 NWidget(WWT_PANEL
, COLOUR_GREY
),
361 NWidget(NWID_VERTICAL
), SetPadding(6), SetPIP(0, 3, 0),
362 NWidget(WWT_TEXT
, COLOUR_GREY
, WID_FRW_RATE_GAMELOOP
), SetDataTip(STR_FRAMERATE_RATE_GAMELOOP
, STR_FRAMERATE_RATE_GAMELOOP_TOOLTIP
),
363 NWidget(WWT_TEXT
, COLOUR_GREY
, WID_FRW_RATE_DRAWING
), SetDataTip(STR_FRAMERATE_RATE_BLITTER
, STR_FRAMERATE_RATE_BLITTER_TOOLTIP
),
364 NWidget(WWT_TEXT
, COLOUR_GREY
, WID_FRW_RATE_FACTOR
), SetDataTip(STR_FRAMERATE_SPEED_FACTOR
, STR_FRAMERATE_SPEED_FACTOR_TOOLTIP
),
367 NWidget(NWID_HORIZONTAL
),
368 NWidget(WWT_PANEL
, COLOUR_GREY
),
369 NWidget(NWID_VERTICAL
), SetPadding(6), SetPIP(0, 3, 0),
370 NWidget(NWID_HORIZONTAL
), SetPIP(0, 6, 0),
371 NWidget(WWT_EMPTY
, COLOUR_GREY
, WID_FRW_TIMES_NAMES
), SetScrollbar(WID_FRW_SCROLLBAR
),
372 NWidget(WWT_EMPTY
, COLOUR_GREY
, WID_FRW_TIMES_CURRENT
), SetScrollbar(WID_FRW_SCROLLBAR
),
373 NWidget(WWT_EMPTY
, COLOUR_GREY
, WID_FRW_TIMES_AVERAGE
), SetScrollbar(WID_FRW_SCROLLBAR
),
374 NWidget(NWID_SELECTION
, INVALID_COLOUR
, WID_FRW_SEL_MEMORY
),
375 NWidget(WWT_EMPTY
, COLOUR_GREY
, WID_FRW_ALLOCSIZE
), SetScrollbar(WID_FRW_SCROLLBAR
),
378 NWidget(WWT_TEXT
, COLOUR_GREY
, WID_FRW_INFO_DATA_POINTS
), SetDataTip(STR_FRAMERATE_DATA_POINTS
, 0x0),
381 NWidget(NWID_VERTICAL
),
382 NWidget(NWID_VSCROLLBAR
, COLOUR_GREY
, WID_FRW_SCROLLBAR
),
383 NWidget(WWT_RESIZEBOX
, COLOUR_GREY
),
388 struct FramerateWindow
: Window
{
391 GUITimer next_update
;
395 struct CachedDecimal
{
399 inline void SetRate(double value
, double target
)
401 const double threshold_good
= target
* 0.95;
402 const double threshold_bad
= target
* 2 / 3;
403 this->value
= (uint32
)(value
* 100);
404 this->strid
= (value
> threshold_good
) ? STR_FRAMERATE_FPS_GOOD
: (value
< threshold_bad
) ? STR_FRAMERATE_FPS_BAD
: STR_FRAMERATE_FPS_WARN
;
407 inline void SetTime(double value
, double target
)
409 const double threshold_good
= target
/ 3;
410 const double threshold_bad
= target
;
411 this->value
= (uint32
)(value
* 100);
412 this->strid
= (value
< threshold_good
) ? STR_FRAMERATE_MS_GOOD
: (value
> threshold_bad
) ? STR_FRAMERATE_MS_BAD
: STR_FRAMERATE_MS_WARN
;
415 inline void InsertDParams(uint n
) const
417 SetDParam(n
, this->value
);
422 CachedDecimal rate_gameloop
; ///< cached game loop tick rate
423 CachedDecimal rate_drawing
; ///< cached drawing frame rate
424 CachedDecimal speed_gameloop
; ///< cached game loop speed factor
425 CachedDecimal times_shortterm
[PFE_MAX
]; ///< cached short term average times
426 CachedDecimal times_longterm
[PFE_MAX
]; ///< cached long term average times
428 static constexpr int VSPACING
= 3; ///< space between column heading and values
429 static constexpr int MIN_ELEMENTS
= 5; ///< smallest number of elements to display
431 FramerateWindow(WindowDesc
*desc
, WindowNumber number
) : Window(desc
)
433 this->InitNested(number
);
434 this->small
= this->IsShaded();
435 this->showing_memory
= true;
437 this->num_displayed
= this->num_active
;
438 this->next_update
.SetInterval(100);
440 /* Window is always initialised to MIN_ELEMENTS height, resize to contain num_displayed */
441 ResizeWindow(this, 0, (std::max(MIN_ELEMENTS
, this->num_displayed
) - MIN_ELEMENTS
) * FONT_HEIGHT_NORMAL
);
444 void OnRealtimeTick(uint delta_ms
) override
446 bool elapsed
= this->next_update
.Elapsed(delta_ms
);
448 /* Check if the shaded state has changed, switch caption text if it has */
449 if (this->small
!= this->IsShaded()) {
450 this->small
= this->IsShaded();
451 this->GetWidget
<NWidgetLeaf
>(WID_FRW_CAPTION
)->SetDataTip(this->small
? STR_FRAMERATE_CAPTION_SMALL
: STR_FRAMERATE_CAPTION
, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS
);
458 this->next_update
.SetInterval(100);
464 double gl_rate
= _pf_data
[PFE_GAMELOOP
].GetRate();
465 bool have_script
= false;
466 this->rate_gameloop
.SetRate(gl_rate
, _pf_data
[PFE_GAMELOOP
].expected_rate
);
467 this->speed_gameloop
.SetRate(gl_rate
/ _pf_data
[PFE_GAMELOOP
].expected_rate
, 1.0);
468 if (this->small
) return; // in small mode, this is everything needed
470 this->rate_drawing
.SetRate(_pf_data
[PFE_DRAWING
].GetRate(), _settings_client
.gui
.refresh_rate
);
473 for (PerformanceElement e
= PFE_FIRST
; e
< PFE_MAX
; e
++) {
474 this->times_shortterm
[e
].SetTime(_pf_data
[e
].GetAverageDurationMilliseconds(8), MILLISECONDS_PER_TICK
);
475 this->times_longterm
[e
].SetTime(_pf_data
[e
].GetAverageDurationMilliseconds(NUM_FRAMERATE_POINTS
), MILLISECONDS_PER_TICK
);
476 if (_pf_data
[e
].num_valid
> 0) {
478 if (e
== PFE_GAMESCRIPT
|| e
>= PFE_AI0
) have_script
= true;
482 if (this->showing_memory
!= have_script
) {
483 NWidgetStacked
*plane
= this->GetWidget
<NWidgetStacked
>(WID_FRW_SEL_MEMORY
);
484 plane
->SetDisplayedPlane(have_script
? 0 : SZSP_VERTICAL
);
485 this->showing_memory
= have_script
;
488 if (new_active
!= this->num_active
) {
489 this->num_active
= new_active
;
490 Scrollbar
*sb
= this->GetScrollbar(WID_FRW_SCROLLBAR
);
491 sb
->SetCount(this->num_active
);
492 sb
->SetCapacity(std::min(this->num_displayed
, this->num_active
));
497 void SetStringParameters(int widget
) const override
500 case WID_FRW_CAPTION
:
501 /* When the window is shaded, the caption shows game loop rate and speed factor */
502 if (!this->small
) break;
503 SetDParam(0, this->rate_gameloop
.strid
);
504 this->rate_gameloop
.InsertDParams(1);
505 this->speed_gameloop
.InsertDParams(3);
508 case WID_FRW_RATE_GAMELOOP
:
509 SetDParam(0, this->rate_gameloop
.strid
);
510 this->rate_gameloop
.InsertDParams(1);
512 case WID_FRW_RATE_DRAWING
:
513 SetDParam(0, this->rate_drawing
.strid
);
514 this->rate_drawing
.InsertDParams(1);
516 case WID_FRW_RATE_FACTOR
:
517 this->speed_gameloop
.InsertDParams(0);
519 case WID_FRW_INFO_DATA_POINTS
:
520 SetDParam(0, NUM_FRAMERATE_POINTS
);
525 void UpdateWidgetSize(int widget
, Dimension
*size
, const Dimension
&padding
, Dimension
*fill
, Dimension
*resize
) override
528 case WID_FRW_RATE_GAMELOOP
:
529 SetDParam(0, STR_FRAMERATE_FPS_GOOD
);
530 SetDParam(1, 999999);
532 *size
= GetStringBoundingBox(STR_FRAMERATE_RATE_GAMELOOP
);
534 case WID_FRW_RATE_DRAWING
:
535 SetDParam(0, STR_FRAMERATE_FPS_GOOD
);
536 SetDParam(1, 999999);
538 *size
= GetStringBoundingBox(STR_FRAMERATE_RATE_BLITTER
);
540 case WID_FRW_RATE_FACTOR
:
541 SetDParam(0, 999999);
543 *size
= GetStringBoundingBox(STR_FRAMERATE_SPEED_FACTOR
);
546 case WID_FRW_TIMES_NAMES
: {
548 size
->height
= FONT_HEIGHT_NORMAL
+ VSPACING
+ MIN_ELEMENTS
* FONT_HEIGHT_NORMAL
;
550 resize
->height
= FONT_HEIGHT_NORMAL
;
551 for (PerformanceElement e
: DISPLAY_ORDER_PFE
) {
552 if (_pf_data
[e
].num_valid
== 0) continue;
555 line_size
= GetStringBoundingBox(STR_FRAMERATE_GAMELOOP
+ e
);
557 SetDParam(0, e
- PFE_AI0
+ 1);
558 SetDParamStr(1, GetAIName(e
- PFE_AI0
));
559 line_size
= GetStringBoundingBox(STR_FRAMERATE_AI
);
561 size
->width
= std::max(size
->width
, line_size
.width
);
566 case WID_FRW_TIMES_CURRENT
:
567 case WID_FRW_TIMES_AVERAGE
:
568 case WID_FRW_ALLOCSIZE
: {
569 *size
= GetStringBoundingBox(STR_FRAMERATE_CURRENT
+ (widget
- WID_FRW_TIMES_CURRENT
));
570 SetDParam(0, 999999);
572 Dimension item_size
= GetStringBoundingBox(STR_FRAMERATE_MS_GOOD
);
573 size
->width
= std::max(size
->width
, item_size
.width
);
574 size
->height
+= FONT_HEIGHT_NORMAL
* MIN_ELEMENTS
+ VSPACING
;
576 resize
->height
= FONT_HEIGHT_NORMAL
;
582 /** Render a column of formatted average durations */
583 void DrawElementTimesColumn(const Rect
&r
, StringID heading_str
, const CachedDecimal
*values
) const
585 const Scrollbar
*sb
= this->GetScrollbar(WID_FRW_SCROLLBAR
);
586 uint16 skip
= sb
->GetPosition();
587 int drawable
= this->num_displayed
;
589 DrawString(r
.left
, r
.right
, y
, heading_str
, TC_FROMSTRING
, SA_CENTER
, true);
590 y
+= FONT_HEIGHT_NORMAL
+ VSPACING
;
591 for (PerformanceElement e
: DISPLAY_ORDER_PFE
) {
592 if (_pf_data
[e
].num_valid
== 0) continue;
596 values
[e
].InsertDParams(0);
597 DrawString(r
.left
, r
.right
, y
, values
[e
].strid
, TC_FROMSTRING
, SA_RIGHT
);
598 y
+= FONT_HEIGHT_NORMAL
;
600 if (drawable
== 0) break;
605 void DrawElementAllocationsColumn(const Rect
&r
) const
607 const Scrollbar
*sb
= this->GetScrollbar(WID_FRW_SCROLLBAR
);
608 uint16 skip
= sb
->GetPosition();
609 int drawable
= this->num_displayed
;
611 DrawString(r
.left
, r
.right
, y
, STR_FRAMERATE_MEMORYUSE
, TC_FROMSTRING
, SA_CENTER
, true);
612 y
+= FONT_HEIGHT_NORMAL
+ VSPACING
;
613 for (PerformanceElement e
: DISPLAY_ORDER_PFE
) {
614 if (_pf_data
[e
].num_valid
== 0) continue;
617 } else if (e
== PFE_GAMESCRIPT
|| e
>= PFE_AI0
) {
618 if (e
== PFE_GAMESCRIPT
) {
619 SetDParam(0, Game::GetInstance()->GetAllocatedMemory());
621 SetDParam(0, Company::Get(e
- PFE_AI0
)->ai_instance
->GetAllocatedMemory());
623 DrawString(r
.left
, r
.right
, y
, STR_FRAMERATE_BYTES_GOOD
, TC_FROMSTRING
, SA_RIGHT
);
624 y
+= FONT_HEIGHT_NORMAL
;
626 if (drawable
== 0) break;
628 /* skip non-script */
629 y
+= FONT_HEIGHT_NORMAL
;
631 if (drawable
== 0) break;
636 void DrawWidget(const Rect
&r
, int widget
) const override
639 case WID_FRW_TIMES_NAMES
: {
640 /* Render a column of titles for performance element names */
641 const Scrollbar
*sb
= this->GetScrollbar(WID_FRW_SCROLLBAR
);
642 uint16 skip
= sb
->GetPosition();
643 int drawable
= this->num_displayed
;
644 int y
= r
.top
+ FONT_HEIGHT_NORMAL
+ VSPACING
; // first line contains headings in the value columns
645 for (PerformanceElement e
: DISPLAY_ORDER_PFE
) {
646 if (_pf_data
[e
].num_valid
== 0) continue;
651 DrawString(r
.left
, r
.right
, y
, STR_FRAMERATE_GAMELOOP
+ e
, TC_FROMSTRING
, SA_LEFT
);
653 SetDParam(0, e
- PFE_AI0
+ 1);
654 SetDParamStr(1, GetAIName(e
- PFE_AI0
));
655 DrawString(r
.left
, r
.right
, y
, STR_FRAMERATE_AI
, TC_FROMSTRING
, SA_LEFT
);
657 y
+= FONT_HEIGHT_NORMAL
;
659 if (drawable
== 0) break;
664 case WID_FRW_TIMES_CURRENT
:
665 /* Render short-term average values */
666 DrawElementTimesColumn(r
, STR_FRAMERATE_CURRENT
, this->times_shortterm
);
668 case WID_FRW_TIMES_AVERAGE
:
669 /* Render averages of all recorded values */
670 DrawElementTimesColumn(r
, STR_FRAMERATE_AVERAGE
, this->times_longterm
);
672 case WID_FRW_ALLOCSIZE
:
673 DrawElementAllocationsColumn(r
);
678 void OnClick(Point pt
, int widget
, int click_count
) override
681 case WID_FRW_TIMES_NAMES
:
682 case WID_FRW_TIMES_CURRENT
:
683 case WID_FRW_TIMES_AVERAGE
: {
684 /* Open time graph windows when clicking detail measurement lines */
685 const Scrollbar
*sb
= this->GetScrollbar(WID_FRW_SCROLLBAR
);
686 int line
= sb
->GetScrolledRowFromWidget(pt
.y
, this, widget
, VSPACING
+ FONT_HEIGHT_NORMAL
);
687 if (line
!= INT_MAX
) {
689 /* Find the visible line that was clicked */
690 for (PerformanceElement e
: DISPLAY_ORDER_PFE
) {
691 if (_pf_data
[e
].num_valid
> 0) line
--;
693 ShowFrametimeGraphWindow(e
);
703 void OnResize() override
705 auto *wid
= this->GetWidget
<NWidgetResizeBase
>(WID_FRW_TIMES_NAMES
);
706 this->num_displayed
= (wid
->current_y
- wid
->min_y
- VSPACING
) / FONT_HEIGHT_NORMAL
- 1; // subtract 1 for headings
707 this->GetScrollbar(WID_FRW_SCROLLBAR
)->SetCapacity(this->num_displayed
);
711 static WindowDesc
_framerate_display_desc(
712 WDP_AUTO
, "framerate_display", 0, 0,
713 WC_FRAMERATE_DISPLAY
, WC_NONE
,
715 _framerate_window_widgets
, lengthof(_framerate_window_widgets
)
719 /** @hideinitializer */
720 static const NWidgetPart _frametime_graph_window_widgets
[] = {
721 NWidget(NWID_HORIZONTAL
),
722 NWidget(WWT_CLOSEBOX
, COLOUR_GREY
),
723 NWidget(WWT_CAPTION
, COLOUR_GREY
, WID_FGW_CAPTION
), SetDataTip(STR_WHITE_STRING
, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS
),
724 NWidget(WWT_STICKYBOX
, COLOUR_GREY
),
726 NWidget(WWT_PANEL
, COLOUR_GREY
),
727 NWidget(NWID_VERTICAL
), SetPadding(6),
728 NWidget(WWT_EMPTY
, COLOUR_GREY
, WID_FGW_GRAPH
),
733 struct FrametimeGraphWindow
: Window
{
734 int vertical_scale
; ///< number of TIMESTAMP_PRECISION units vertically
735 int horizontal_scale
; ///< number of half-second units horizontally
736 GUITimer next_scale_update
; ///< interval for next scale update
738 PerformanceElement element
; ///< what element this window renders graph for
739 Dimension graph_size
; ///< size of the main graph area (excluding axis labels)
741 FrametimeGraphWindow(WindowDesc
*desc
, WindowNumber number
) : Window(desc
)
743 this->element
= (PerformanceElement
)number
;
744 this->horizontal_scale
= 4;
745 this->vertical_scale
= TIMESTAMP_PRECISION
/ 10;
746 this->next_scale_update
.SetInterval(1);
748 this->InitNested(number
);
751 void SetStringParameters(int widget
) const override
754 case WID_FGW_CAPTION
:
755 if (this->element
< PFE_AI0
) {
756 SetDParam(0, STR_FRAMETIME_CAPTION_GAMELOOP
+ this->element
);
758 SetDParam(0, STR_FRAMETIME_CAPTION_AI
);
759 SetDParam(1, this->element
- PFE_AI0
+ 1);
760 SetDParamStr(2, GetAIName(this->element
- PFE_AI0
));
766 void UpdateWidgetSize(int widget
, Dimension
*size
, const Dimension
&padding
, Dimension
*fill
, Dimension
*resize
) override
768 if (widget
== WID_FGW_GRAPH
) {
770 Dimension size_ms_label
= GetStringBoundingBox(STR_FRAMERATE_GRAPH_MILLISECONDS
);
772 Dimension size_s_label
= GetStringBoundingBox(STR_FRAMERATE_GRAPH_SECONDS
);
774 /* Size graph in height to fit at least 10 vertical labels with space between, or at least 100 pixels */
775 graph_size
.height
= std::max(100u, 10 * (size_ms_label
.height
+ 1));
776 /* Always 2:1 graph area */
777 graph_size
.width
= 2 * graph_size
.height
;
780 size
->width
+= size_ms_label
.width
+ 2;
781 size
->height
+= size_s_label
.height
+ 2;
785 void SelectHorizontalScale(TimingMeasurement range
)
787 /* Determine horizontal scale based on period covered by 60 points
788 * (slightly less than 2 seconds at full game speed) */
789 struct ScaleDef
{ TimingMeasurement range
; int scale
; };
790 static const ScaleDef hscales
[] = {
797 for (const ScaleDef
*sc
= hscales
; sc
< hscales
+ lengthof(hscales
); sc
++) {
798 if (range
< sc
->range
) this->horizontal_scale
= sc
->scale
;
802 void SelectVerticalScale(TimingMeasurement range
)
804 /* Determine vertical scale based on peak value (within the horizontal scale + a bit) */
805 static const TimingMeasurement vscales
[] = {
806 TIMESTAMP_PRECISION
* 100,
807 TIMESTAMP_PRECISION
* 10,
808 TIMESTAMP_PRECISION
* 5,
810 TIMESTAMP_PRECISION
/ 2,
811 TIMESTAMP_PRECISION
/ 5,
812 TIMESTAMP_PRECISION
/ 10,
813 TIMESTAMP_PRECISION
/ 50,
814 TIMESTAMP_PRECISION
/ 200,
816 for (const TimingMeasurement
*sc
= vscales
; sc
< vscales
+ lengthof(vscales
); sc
++) {
817 if (range
< *sc
) this->vertical_scale
= (int)*sc
;
821 /** Recalculate the graph scaling factors based on current recorded data */
824 const TimingMeasurement
*durations
= _pf_data
[this->element
].durations
;
825 const TimingMeasurement
*timestamps
= _pf_data
[this->element
].timestamps
;
826 int num_valid
= _pf_data
[this->element
].num_valid
;
827 int point
= _pf_data
[this->element
].prev_index
;
829 TimingMeasurement lastts
= timestamps
[point
];
830 TimingMeasurement time_sum
= 0;
831 TimingMeasurement peak_value
= 0;
834 /* Sensible default for when too few measurements are available */
835 this->horizontal_scale
= 4;
837 for (int i
= 1; i
< num_valid
; i
++) {
839 if (point
< 0) point
= NUM_FRAMERATE_POINTS
- 1;
841 TimingMeasurement value
= durations
[point
];
842 if (value
== PerformanceData::INVALID_DURATION
) {
843 /* Skip gaps in data by pretending time is continuous across them */
844 lastts
= timestamps
[point
];
847 if (value
> peak_value
) peak_value
= value
;
850 /* Accumulate period of time covered by data */
851 time_sum
+= lastts
- timestamps
[point
];
852 lastts
= timestamps
[point
];
854 /* Enough data to select a range and get decent data density */
855 if (count
== 60) this->SelectHorizontalScale(time_sum
/ TIMESTAMP_PRECISION
);
857 /* End when enough points have been collected and the horizontal scale has been exceeded */
858 if (count
>= 60 && time_sum
>= (this->horizontal_scale
+ 2) * TIMESTAMP_PRECISION
/ 2) break;
861 this->SelectVerticalScale(peak_value
);
864 void OnRealtimeTick(uint delta_ms
) override
868 if (this->next_scale_update
.Elapsed(delta_ms
)) {
869 this->next_scale_update
.SetInterval(500);
874 /** Scale and interpolate a value from a source range into a destination range */
876 static inline T
Scinterlate(T dst_min
, T dst_max
, T src_min
, T src_max
, T value
)
878 T dst_diff
= dst_max
- dst_min
;
879 T src_diff
= src_max
- src_min
;
880 return (value
- src_min
) * dst_diff
/ src_diff
+ dst_min
;
883 void DrawWidget(const Rect
&r
, int widget
) const override
885 if (widget
== WID_FGW_GRAPH
) {
886 const TimingMeasurement
*durations
= _pf_data
[this->element
].durations
;
887 const TimingMeasurement
*timestamps
= _pf_data
[this->element
].timestamps
;
888 int point
= _pf_data
[this->element
].prev_index
;
890 const int x_zero
= r
.right
- (int)this->graph_size
.width
;
891 const int x_max
= r
.right
;
892 const int y_zero
= r
.top
+ (int)this->graph_size
.height
;
893 const int y_max
= r
.top
;
894 const int c_grid
= PC_DARK_GREY
;
895 const int c_lines
= PC_BLACK
;
896 const int c_peak
= PC_DARK_RED
;
898 const TimingMeasurement draw_horz_scale
= (TimingMeasurement
)this->horizontal_scale
* TIMESTAMP_PRECISION
/ 2;
899 const TimingMeasurement draw_vert_scale
= (TimingMeasurement
)this->vertical_scale
;
901 /* Number of \c horizontal_scale units in each horizontal division */
902 const uint horz_div_scl
= (this->horizontal_scale
<= 20) ? 1 : 10;
903 /* Number of divisions of the horizontal axis */
904 const uint horz_divisions
= this->horizontal_scale
/ horz_div_scl
;
905 /* Number of divisions of the vertical axis */
906 const uint vert_divisions
= 10;
908 /* Draw division lines and labels for the vertical axis */
909 for (uint division
= 0; division
< vert_divisions
; division
++) {
910 int y
= Scinterlate(y_zero
, y_max
, 0, (int)vert_divisions
, (int)division
);
911 GfxDrawLine(x_zero
, y
, x_max
, y
, c_grid
);
912 if (division
% 2 == 0) {
913 if ((TimingMeasurement
)this->vertical_scale
> TIMESTAMP_PRECISION
) {
914 SetDParam(0, this->vertical_scale
* division
/ 10 / TIMESTAMP_PRECISION
);
915 DrawString(r
.left
, x_zero
- 2, y
- FONT_HEIGHT_SMALL
, STR_FRAMERATE_GRAPH_SECONDS
, TC_GREY
, SA_RIGHT
| SA_FORCE
, false, FS_SMALL
);
917 SetDParam(0, this->vertical_scale
* division
/ 10 * 1000 / TIMESTAMP_PRECISION
);
918 DrawString(r
.left
, x_zero
- 2, y
- FONT_HEIGHT_SMALL
, STR_FRAMERATE_GRAPH_MILLISECONDS
, TC_GREY
, SA_RIGHT
| SA_FORCE
, false, FS_SMALL
);
922 /* Draw division lines and labels for the horizontal axis */
923 for (uint division
= horz_divisions
; division
> 0; division
--) {
924 int x
= Scinterlate(x_zero
, x_max
, 0, (int)horz_divisions
, (int)horz_divisions
- (int)division
);
925 GfxDrawLine(x
, y_max
, x
, y_zero
, c_grid
);
926 if (division
% 2 == 0) {
927 SetDParam(0, division
* horz_div_scl
/ 2);
928 DrawString(x
, x_max
, y_zero
+ 2, STR_FRAMERATE_GRAPH_SECONDS
, TC_GREY
, SA_LEFT
| SA_FORCE
, false, FS_SMALL
);
932 /* Position of last rendered data point */
935 (int)Scinterlate
<int64
>(y_zero
, y_max
, 0, this->vertical_scale
, durations
[point
])
937 /* Timestamp of last rendered data point */
938 TimingMeasurement lastts
= timestamps
[point
];
940 TimingMeasurement peak_value
= 0;
941 Point peak_point
= { 0, 0 };
942 TimingMeasurement value_sum
= 0;
943 TimingMeasurement time_sum
= 0;
944 int points_drawn
= 0;
946 for (int i
= 1; i
< NUM_FRAMERATE_POINTS
; i
++) {
948 if (point
< 0) point
= NUM_FRAMERATE_POINTS
- 1;
950 TimingMeasurement value
= durations
[point
];
951 if (value
== PerformanceData::INVALID_DURATION
) {
952 /* Skip gaps in measurements, pretend the data points on each side are continuous */
953 lastts
= timestamps
[point
];
957 /* Use total time period covered for value along horizontal axis */
958 time_sum
+= lastts
- timestamps
[point
];
959 lastts
= timestamps
[point
];
960 /* Stop if past the width of the graph */
961 if (time_sum
> draw_horz_scale
) break;
963 /* Draw line from previous point to new point */
965 (int)Scinterlate
<int64
>(x_zero
, x_max
, 0, (int64
)draw_horz_scale
, (int64
)draw_horz_scale
- (int64
)time_sum
),
966 (int)Scinterlate
<int64
>(y_zero
, y_max
, 0, (int64
)draw_vert_scale
, (int64
)value
)
968 assert(newpoint
.x
<= lastpoint
.x
);
969 GfxDrawLine(lastpoint
.x
, lastpoint
.y
, newpoint
.x
, newpoint
.y
, c_lines
);
970 lastpoint
= newpoint
;
972 /* Record peak and average value across graphed data */
975 if (value
> peak_value
) {
977 peak_point
= newpoint
;
981 /* If the peak value is significantly larger than the average, mark and label it */
982 if (points_drawn
> 0 && peak_value
> TIMESTAMP_PRECISION
/ 100 && 2 * peak_value
> 3 * value_sum
/ points_drawn
) {
983 TextColour tc_peak
= (TextColour
)(TC_IS_PALETTE_COLOUR
| c_peak
);
984 GfxFillRect(peak_point
.x
- 1, peak_point
.y
- 1, peak_point
.x
+ 1, peak_point
.y
+ 1, c_peak
);
985 SetDParam(0, peak_value
* 1000 / TIMESTAMP_PRECISION
);
986 int label_y
= std::max(y_max
, peak_point
.y
- FONT_HEIGHT_SMALL
);
987 if (peak_point
.x
- x_zero
> (int)this->graph_size
.width
/ 2) {
988 DrawString(x_zero
, peak_point
.x
- 2, label_y
, STR_FRAMERATE_GRAPH_MILLISECONDS
, tc_peak
, SA_RIGHT
| SA_FORCE
, false, FS_SMALL
);
990 DrawString(peak_point
.x
+ 2, x_max
, label_y
, STR_FRAMERATE_GRAPH_MILLISECONDS
, tc_peak
, SA_LEFT
| SA_FORCE
, false, FS_SMALL
);
997 static WindowDesc
_frametime_graph_window_desc(
998 WDP_AUTO
, "frametime_graph", 140, 90,
999 WC_FRAMETIME_GRAPH
, WC_NONE
,
1001 _frametime_graph_window_widgets
, lengthof(_frametime_graph_window_widgets
)
1006 /** Open the general framerate window */
1007 void ShowFramerateWindow()
1009 AllocateWindowDescFront
<FramerateWindow
>(&_framerate_display_desc
, 0);
1012 /** Open a graph window for a performance element */
1013 void ShowFrametimeGraphWindow(PerformanceElement elem
)
1015 if (elem
< PFE_FIRST
|| elem
>= PFE_MAX
) return; // maybe warn?
1016 AllocateWindowDescFront
<FrametimeGraphWindow
>(&_frametime_graph_window_desc
, elem
, true);
1019 /** Print performance statistics to game console */
1020 void ConPrintFramerate()
1022 const int count1
= NUM_FRAMERATE_POINTS
/ 8;
1023 const int count2
= NUM_FRAMERATE_POINTS
/ 4;
1024 const int count3
= NUM_FRAMERATE_POINTS
/ 1;
1026 IConsolePrint(TC_SILVER
, "Based on num. data points: {} {} {}", count1
, count2
, count3
);
1028 static const char *MEASUREMENT_NAMES
[PFE_MAX
] = {
1030 " GL station ticks",
1032 " GL road vehicle ticks",
1034 " GL aircraft ticks",
1035 " GL landscape ticks",
1036 " GL link graph delays",
1038 " Viewport drawing",
1041 "AI/GS scripts total",
1044 char ai_name_buf
[128];
1046 static const PerformanceElement rate_elements
[] = { PFE_GAMELOOP
, PFE_DRAWING
, PFE_VIDEO
};
1048 bool printed_anything
= false;
1050 for (const PerformanceElement
*e
= rate_elements
; e
< rate_elements
+ lengthof(rate_elements
); e
++) {
1051 auto &pf
= _pf_data
[*e
];
1052 if (pf
.num_valid
== 0) continue;
1053 IConsolePrint(TC_GREEN
, "{} rate: {:.2f}fps (expected: {:.2f}fps)",
1054 MEASUREMENT_NAMES
[*e
],
1057 printed_anything
= true;
1060 for (PerformanceElement e
= PFE_FIRST
; e
< PFE_MAX
; e
++) {
1061 auto &pf
= _pf_data
[e
];
1062 if (pf
.num_valid
== 0) continue;
1065 name
= MEASUREMENT_NAMES
[e
];
1067 seprintf(ai_name_buf
, lastof(ai_name_buf
), "AI %d %s", e
- PFE_AI0
+ 1, GetAIName(e
- PFE_AI0
)),
1070 IConsolePrint(TC_LIGHT_BLUE
, "{} times: {:.2f}ms {:.2f}ms {:.2f}ms",
1072 pf
.GetAverageDurationMilliseconds(count1
),
1073 pf
.GetAverageDurationMilliseconds(count2
),
1074 pf
.GetAverageDurationMilliseconds(count3
));
1075 printed_anything
= true;
1078 if (!printed_anything
) {
1079 IConsolePrint(CC_ERROR
, "No performance measurements have been taken yet.");