Fix cc39fa9: New orders are non-stop by default (#8689)
[openttd-github.git] / src / framerate_gui.cpp
blob0afb533f6aa1f28e5799ab9fef0db4e373a5b371
1 /*
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/>.
6 */
8 /** @file framerate_gui.cpp GUI for displaying framerate/game speed information. */
10 #include "framerate_type.h"
11 #include <chrono>
12 #include "gfx_func.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"
31 /**
32 * Private declarations for performance measurement implementation
34 namespace {
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 */
50 double expected_rate;
51 /** Next index to write to in \c durations and \c timestamps */
52 int next_index;
53 /** Last index written to in \c durations and \c timestamps */
54 int prev_index;
55 /** Number of data points recorded, clamped to \c NUM_FRAMERATE_POINTS */
56 int num_valid;
58 /** Current accumulated duration */
59 TimingMeasurement acc_duration;
60 /** Start time for current accumulation cycle */
61 TimingMeasurement acc_timestamp;
63 /**
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 */
124 double sumtime = 0;
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) {
128 sumtime += d;
129 } else {
130 /* Don't count the invalid durations */
131 count--;
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 */
140 double GetRate()
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 */
148 int count = 0;
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 */
155 point--;
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];
162 count++;
164 last = this->timestamps[point];
165 if (total >= TIMESTAMP_PRECISION) break; // end after 1 second has been collected
166 point--;
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.
181 * @hideinitializer
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(GL_RATE), // 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 ...
199 PerformanceData(1),
200 PerformanceData(1),
201 PerformanceData(1),
202 PerformanceData(1),
203 PerformanceData(1),
204 PerformanceData(1),
205 PerformanceData(1),
206 PerformanceData(1),
207 PerformanceData(1),
208 PerformanceData(1),
209 PerformanceData(1),
210 PerformanceData(1),
211 PerformanceData(1),
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);
238 this->elem = elem;
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;
249 if (!any_active) {
250 PerformanceMeasurer::SetInactive(PFE_ALLSCRIPTS);
251 return;
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 _pf_data[elem].AddPause(GetPerformanceTimer());
282 * Begin measuring one block of the accumulating value.
283 * @param elem The element to be measured
285 PerformanceAccumulator::PerformanceAccumulator(PerformanceElement elem)
287 assert(elem < PFE_MAX);
289 this->elem = elem;
290 this->start_time = GetPerformanceTimer();
293 /** Finish and add one block of the accumulating value. */
294 PerformanceAccumulator::~PerformanceAccumulator()
296 _pf_data[this->elem].AddAccumulate(GetPerformanceTimer() - this->start_time);
300 * Store the previous accumulator value and reset for a new cycle of accumulating measurements.
301 * @note This function must be called once per frame, otherwise measurements are not collected.
302 * @param elem The element to begin a new measurement cycle of
304 void PerformanceAccumulator::Reset(PerformanceElement elem)
306 _pf_data[elem].BeginAccumulate(GetPerformanceTimer());
310 void ShowFrametimeGraphWindow(PerformanceElement elem);
313 static const PerformanceElement DISPLAY_ORDER_PFE[PFE_MAX] = {
314 PFE_GAMELOOP,
315 PFE_GL_ECONOMY,
316 PFE_GL_TRAINS,
317 PFE_GL_ROADVEHS,
318 PFE_GL_SHIPS,
319 PFE_GL_AIRCRAFT,
320 PFE_GL_LANDSCAPE,
321 PFE_ALLSCRIPTS,
322 PFE_GAMESCRIPT,
323 PFE_AI0,
324 PFE_AI1,
325 PFE_AI2,
326 PFE_AI3,
327 PFE_AI4,
328 PFE_AI5,
329 PFE_AI6,
330 PFE_AI7,
331 PFE_AI8,
332 PFE_AI9,
333 PFE_AI10,
334 PFE_AI11,
335 PFE_AI12,
336 PFE_AI13,
337 PFE_AI14,
338 PFE_GL_LINKGRAPH,
339 PFE_DRAWING,
340 PFE_DRAWWORLD,
341 PFE_VIDEO,
342 PFE_SOUND,
345 static const char * GetAIName(int ai_index)
347 if (!Company::IsValidAiID(ai_index)) return "";
348 return Company::Get(ai_index)->ai_info->GetName();
351 /** @hideinitializer */
352 static const NWidgetPart _framerate_window_widgets[] = {
353 NWidget(NWID_HORIZONTAL),
354 NWidget(WWT_CLOSEBOX, COLOUR_GREY),
355 NWidget(WWT_CAPTION, COLOUR_GREY, WID_FRW_CAPTION), SetDataTip(STR_FRAMERATE_CAPTION, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS),
356 NWidget(WWT_SHADEBOX, COLOUR_GREY),
357 NWidget(WWT_STICKYBOX, COLOUR_GREY),
358 EndContainer(),
359 NWidget(WWT_PANEL, COLOUR_GREY),
360 NWidget(NWID_VERTICAL), SetPadding(6), SetPIP(0, 3, 0),
361 NWidget(WWT_TEXT, COLOUR_GREY, WID_FRW_RATE_GAMELOOP), SetDataTip(STR_FRAMERATE_RATE_GAMELOOP, STR_FRAMERATE_RATE_GAMELOOP_TOOLTIP),
362 NWidget(WWT_TEXT, COLOUR_GREY, WID_FRW_RATE_DRAWING), SetDataTip(STR_FRAMERATE_RATE_BLITTER, STR_FRAMERATE_RATE_BLITTER_TOOLTIP),
363 NWidget(WWT_TEXT, COLOUR_GREY, WID_FRW_RATE_FACTOR), SetDataTip(STR_FRAMERATE_SPEED_FACTOR, STR_FRAMERATE_SPEED_FACTOR_TOOLTIP),
364 EndContainer(),
365 EndContainer(),
366 NWidget(NWID_HORIZONTAL),
367 NWidget(WWT_PANEL, COLOUR_GREY),
368 NWidget(NWID_VERTICAL), SetPadding(6), SetPIP(0, 3, 0),
369 NWidget(NWID_HORIZONTAL), SetPIP(0, 6, 0),
370 NWidget(WWT_EMPTY, COLOUR_GREY, WID_FRW_TIMES_NAMES), SetScrollbar(WID_FRW_SCROLLBAR),
371 NWidget(WWT_EMPTY, COLOUR_GREY, WID_FRW_TIMES_CURRENT), SetScrollbar(WID_FRW_SCROLLBAR),
372 NWidget(WWT_EMPTY, COLOUR_GREY, WID_FRW_TIMES_AVERAGE), SetScrollbar(WID_FRW_SCROLLBAR),
373 NWidget(NWID_SELECTION, INVALID_COLOUR, WID_FRW_SEL_MEMORY),
374 NWidget(WWT_EMPTY, COLOUR_GREY, WID_FRW_ALLOCSIZE), SetScrollbar(WID_FRW_SCROLLBAR),
375 EndContainer(),
376 EndContainer(),
377 NWidget(WWT_TEXT, COLOUR_GREY, WID_FRW_INFO_DATA_POINTS), SetDataTip(STR_FRAMERATE_DATA_POINTS, 0x0),
378 EndContainer(),
379 EndContainer(),
380 NWidget(NWID_VERTICAL),
381 NWidget(NWID_VSCROLLBAR, COLOUR_GREY, WID_FRW_SCROLLBAR),
382 NWidget(WWT_RESIZEBOX, COLOUR_GREY),
383 EndContainer(),
384 EndContainer(),
387 struct FramerateWindow : Window {
388 bool small;
389 bool showing_memory;
390 GUITimer next_update;
391 int num_active;
392 int num_displayed;
394 struct CachedDecimal {
395 StringID strid;
396 uint32 value;
398 inline void SetRate(double value, double target)
400 const double threshold_good = target * 0.95;
401 const double threshold_bad = target * 2 / 3;
402 value = std::min(9999.99, value);
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 value = std::min(9999.99, value);
412 this->value = (uint32)(value * 100);
413 this->strid = (value < threshold_good) ? STR_FRAMERATE_MS_GOOD : (value > threshold_bad) ? STR_FRAMERATE_MS_BAD : STR_FRAMERATE_MS_WARN;
416 inline void InsertDParams(uint n) const
418 SetDParam(n, this->value);
419 SetDParam(n + 1, 2);
423 CachedDecimal rate_gameloop; ///< cached game loop tick rate
424 CachedDecimal rate_drawing; ///< cached drawing frame rate
425 CachedDecimal speed_gameloop; ///< cached game loop speed factor
426 CachedDecimal times_shortterm[PFE_MAX]; ///< cached short term average times
427 CachedDecimal times_longterm[PFE_MAX]; ///< cached long term average times
429 static constexpr int VSPACING = 3; ///< space between column heading and values
430 static constexpr int MIN_ELEMENTS = 5; ///< smallest number of elements to display
432 FramerateWindow(WindowDesc *desc, WindowNumber number) : Window(desc)
434 this->InitNested(number);
435 this->small = this->IsShaded();
436 this->showing_memory = true;
437 this->UpdateData();
438 this->num_displayed = this->num_active;
439 this->next_update.SetInterval(100);
441 /* Window is always initialised to MIN_ELEMENTS height, resize to contain num_displayed */
442 ResizeWindow(this, 0, (std::max(MIN_ELEMENTS, this->num_displayed) - MIN_ELEMENTS) * FONT_HEIGHT_NORMAL);
445 void OnRealtimeTick(uint delta_ms) override
447 bool elapsed = this->next_update.Elapsed(delta_ms);
449 /* Check if the shaded state has changed, switch caption text if it has */
450 if (this->small != this->IsShaded()) {
451 this->small = this->IsShaded();
452 this->GetWidget<NWidgetLeaf>(WID_FRW_CAPTION)->SetDataTip(this->small ? STR_FRAMERATE_CAPTION_SMALL : STR_FRAMERATE_CAPTION, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS);
453 elapsed = true;
456 if (elapsed) {
457 this->UpdateData();
458 this->SetDirty();
459 this->next_update.SetInterval(100);
463 void UpdateData()
465 double gl_rate = _pf_data[PFE_GAMELOOP].GetRate();
466 bool have_script = false;
467 this->rate_gameloop.SetRate(gl_rate, _pf_data[PFE_GAMELOOP].expected_rate);
468 this->speed_gameloop.SetRate(gl_rate / _pf_data[PFE_GAMELOOP].expected_rate, 1.0);
469 if (this->small) return; // in small mode, this is everything needed
471 this->rate_drawing.SetRate(_pf_data[PFE_DRAWING].GetRate(), _pf_data[PFE_DRAWING].expected_rate);
473 int new_active = 0;
474 for (PerformanceElement e = PFE_FIRST; e < PFE_MAX; e++) {
475 this->times_shortterm[e].SetTime(_pf_data[e].GetAverageDurationMilliseconds(8), MILLISECONDS_PER_TICK);
476 this->times_longterm[e].SetTime(_pf_data[e].GetAverageDurationMilliseconds(NUM_FRAMERATE_POINTS), MILLISECONDS_PER_TICK);
477 if (_pf_data[e].num_valid > 0) {
478 new_active++;
479 if (e == PFE_GAMESCRIPT || e >= PFE_AI0) have_script = true;
483 if (this->showing_memory != have_script) {
484 NWidgetStacked *plane = this->GetWidget<NWidgetStacked>(WID_FRW_SEL_MEMORY);
485 plane->SetDisplayedPlane(have_script ? 0 : SZSP_VERTICAL);
486 this->showing_memory = have_script;
489 if (new_active != this->num_active) {
490 this->num_active = new_active;
491 Scrollbar *sb = this->GetScrollbar(WID_FRW_SCROLLBAR);
492 sb->SetCount(this->num_active);
493 sb->SetCapacity(std::min(this->num_displayed, this->num_active));
494 this->ReInit();
498 void SetStringParameters(int widget) const override
500 switch (widget) {
501 case WID_FRW_CAPTION:
502 /* When the window is shaded, the caption shows game loop rate and speed factor */
503 if (!this->small) break;
504 SetDParam(0, this->rate_gameloop.strid);
505 this->rate_gameloop.InsertDParams(1);
506 this->speed_gameloop.InsertDParams(3);
507 break;
509 case WID_FRW_RATE_GAMELOOP:
510 SetDParam(0, this->rate_gameloop.strid);
511 this->rate_gameloop.InsertDParams(1);
512 break;
513 case WID_FRW_RATE_DRAWING:
514 SetDParam(0, this->rate_drawing.strid);
515 this->rate_drawing.InsertDParams(1);
516 break;
517 case WID_FRW_RATE_FACTOR:
518 this->speed_gameloop.InsertDParams(0);
519 break;
520 case WID_FRW_INFO_DATA_POINTS:
521 SetDParam(0, NUM_FRAMERATE_POINTS);
522 break;
526 void UpdateWidgetSize(int widget, Dimension *size, const Dimension &padding, Dimension *fill, Dimension *resize) override
528 switch (widget) {
529 case WID_FRW_RATE_GAMELOOP:
530 SetDParam(0, STR_FRAMERATE_FPS_GOOD);
531 SetDParam(1, 999999);
532 SetDParam(2, 2);
533 *size = GetStringBoundingBox(STR_FRAMERATE_RATE_GAMELOOP);
534 break;
535 case WID_FRW_RATE_DRAWING:
536 SetDParam(0, STR_FRAMERATE_FPS_GOOD);
537 SetDParam(1, 999999);
538 SetDParam(2, 2);
539 *size = GetStringBoundingBox(STR_FRAMERATE_RATE_BLITTER);
540 break;
541 case WID_FRW_RATE_FACTOR:
542 SetDParam(0, 999999);
543 SetDParam(1, 2);
544 *size = GetStringBoundingBox(STR_FRAMERATE_SPEED_FACTOR);
545 break;
547 case WID_FRW_TIMES_NAMES: {
548 size->width = 0;
549 size->height = FONT_HEIGHT_NORMAL + VSPACING + MIN_ELEMENTS * FONT_HEIGHT_NORMAL;
550 resize->width = 0;
551 resize->height = FONT_HEIGHT_NORMAL;
552 for (PerformanceElement e : DISPLAY_ORDER_PFE) {
553 if (_pf_data[e].num_valid == 0) continue;
554 Dimension line_size;
555 if (e < PFE_AI0) {
556 line_size = GetStringBoundingBox(STR_FRAMERATE_GAMELOOP + e);
557 } else {
558 SetDParam(0, e - PFE_AI0 + 1);
559 SetDParamStr(1, GetAIName(e - PFE_AI0));
560 line_size = GetStringBoundingBox(STR_FRAMERATE_AI);
562 size->width = std::max(size->width, line_size.width);
564 break;
567 case WID_FRW_TIMES_CURRENT:
568 case WID_FRW_TIMES_AVERAGE:
569 case WID_FRW_ALLOCSIZE: {
570 *size = GetStringBoundingBox(STR_FRAMERATE_CURRENT + (widget - WID_FRW_TIMES_CURRENT));
571 SetDParam(0, 999999);
572 SetDParam(1, 2);
573 Dimension item_size = GetStringBoundingBox(STR_FRAMERATE_MS_GOOD);
574 size->width = std::max(size->width, item_size.width);
575 size->height += FONT_HEIGHT_NORMAL * MIN_ELEMENTS + VSPACING;
576 resize->width = 0;
577 resize->height = FONT_HEIGHT_NORMAL;
578 break;
583 /** Render a column of formatted average durations */
584 void DrawElementTimesColumn(const Rect &r, StringID heading_str, const CachedDecimal *values) const
586 const Scrollbar *sb = this->GetScrollbar(WID_FRW_SCROLLBAR);
587 uint16 skip = sb->GetPosition();
588 int drawable = this->num_displayed;
589 int y = r.top;
590 DrawString(r.left, r.right, y, heading_str, TC_FROMSTRING, SA_CENTER, true);
591 y += FONT_HEIGHT_NORMAL + VSPACING;
592 for (PerformanceElement e : DISPLAY_ORDER_PFE) {
593 if (_pf_data[e].num_valid == 0) continue;
594 if (skip > 0) {
595 skip--;
596 } else {
597 values[e].InsertDParams(0);
598 DrawString(r.left, r.right, y, values[e].strid, TC_FROMSTRING, SA_RIGHT);
599 y += FONT_HEIGHT_NORMAL;
600 drawable--;
601 if (drawable == 0) break;
606 void DrawElementAllocationsColumn(const Rect &r) const
608 const Scrollbar *sb = this->GetScrollbar(WID_FRW_SCROLLBAR);
609 uint16 skip = sb->GetPosition();
610 int drawable = this->num_displayed;
611 int y = r.top;
612 DrawString(r.left, r.right, y, STR_FRAMERATE_MEMORYUSE, TC_FROMSTRING, SA_CENTER, true);
613 y += FONT_HEIGHT_NORMAL + VSPACING;
614 for (PerformanceElement e : DISPLAY_ORDER_PFE) {
615 if (_pf_data[e].num_valid == 0) continue;
616 if (skip > 0) {
617 skip--;
618 } else if (e == PFE_GAMESCRIPT || e >= PFE_AI0) {
619 if (e == PFE_GAMESCRIPT) {
620 SetDParam(0, Game::GetInstance()->GetAllocatedMemory());
621 } else {
622 SetDParam(0, Company::Get(e - PFE_AI0)->ai_instance->GetAllocatedMemory());
624 DrawString(r.left, r.right, y, STR_FRAMERATE_BYTES_GOOD, TC_FROMSTRING, SA_RIGHT);
625 y += FONT_HEIGHT_NORMAL;
626 drawable--;
627 if (drawable == 0) break;
628 } else {
629 /* skip non-script */
630 y += FONT_HEIGHT_NORMAL;
631 drawable--;
632 if (drawable == 0) break;
637 void DrawWidget(const Rect &r, int widget) const override
639 switch (widget) {
640 case WID_FRW_TIMES_NAMES: {
641 /* Render a column of titles for performance element names */
642 const Scrollbar *sb = this->GetScrollbar(WID_FRW_SCROLLBAR);
643 uint16 skip = sb->GetPosition();
644 int drawable = this->num_displayed;
645 int y = r.top + FONT_HEIGHT_NORMAL + VSPACING; // first line contains headings in the value columns
646 for (PerformanceElement e : DISPLAY_ORDER_PFE) {
647 if (_pf_data[e].num_valid == 0) continue;
648 if (skip > 0) {
649 skip--;
650 } else {
651 if (e < PFE_AI0) {
652 DrawString(r.left, r.right, y, STR_FRAMERATE_GAMELOOP + e, TC_FROMSTRING, SA_LEFT);
653 } else {
654 SetDParam(0, e - PFE_AI0 + 1);
655 SetDParamStr(1, GetAIName(e - PFE_AI0));
656 DrawString(r.left, r.right, y, STR_FRAMERATE_AI, TC_FROMSTRING, SA_LEFT);
658 y += FONT_HEIGHT_NORMAL;
659 drawable--;
660 if (drawable == 0) break;
663 break;
665 case WID_FRW_TIMES_CURRENT:
666 /* Render short-term average values */
667 DrawElementTimesColumn(r, STR_FRAMERATE_CURRENT, this->times_shortterm);
668 break;
669 case WID_FRW_TIMES_AVERAGE:
670 /* Render averages of all recorded values */
671 DrawElementTimesColumn(r, STR_FRAMERATE_AVERAGE, this->times_longterm);
672 break;
673 case WID_FRW_ALLOCSIZE:
674 DrawElementAllocationsColumn(r);
675 break;
679 void OnClick(Point pt, int widget, int click_count) override
681 switch (widget) {
682 case WID_FRW_TIMES_NAMES:
683 case WID_FRW_TIMES_CURRENT:
684 case WID_FRW_TIMES_AVERAGE: {
685 /* Open time graph windows when clicking detail measurement lines */
686 const Scrollbar *sb = this->GetScrollbar(WID_FRW_SCROLLBAR);
687 int line = sb->GetScrolledRowFromWidget(pt.y - FONT_HEIGHT_NORMAL - VSPACING, this, widget, VSPACING, FONT_HEIGHT_NORMAL);
688 if (line != INT_MAX) {
689 line++;
690 /* Find the visible line that was clicked */
691 for (PerformanceElement e : DISPLAY_ORDER_PFE) {
692 if (_pf_data[e].num_valid > 0) line--;
693 if (line == 0) {
694 ShowFrametimeGraphWindow(e);
695 break;
699 break;
704 void OnResize() override
706 auto *wid = this->GetWidget<NWidgetResizeBase>(WID_FRW_TIMES_NAMES);
707 this->num_displayed = (wid->current_y - wid->min_y - VSPACING) / FONT_HEIGHT_NORMAL - 1; // subtract 1 for headings
708 this->GetScrollbar(WID_FRW_SCROLLBAR)->SetCapacity(this->num_displayed);
712 static WindowDesc _framerate_display_desc(
713 WDP_AUTO, "framerate_display", 0, 0,
714 WC_FRAMERATE_DISPLAY, WC_NONE,
716 _framerate_window_widgets, lengthof(_framerate_window_widgets)
720 /** @hideinitializer */
721 static const NWidgetPart _frametime_graph_window_widgets[] = {
722 NWidget(NWID_HORIZONTAL),
723 NWidget(WWT_CLOSEBOX, COLOUR_GREY),
724 NWidget(WWT_CAPTION, COLOUR_GREY, WID_FGW_CAPTION), SetDataTip(STR_WHITE_STRING, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS),
725 NWidget(WWT_STICKYBOX, COLOUR_GREY),
726 EndContainer(),
727 NWidget(WWT_PANEL, COLOUR_GREY),
728 NWidget(NWID_VERTICAL), SetPadding(6),
729 NWidget(WWT_EMPTY, COLOUR_GREY, WID_FGW_GRAPH),
730 EndContainer(),
731 EndContainer(),
734 struct FrametimeGraphWindow : Window {
735 int vertical_scale; ///< number of TIMESTAMP_PRECISION units vertically
736 int horizontal_scale; ///< number of half-second units horizontally
737 GUITimer next_scale_update; ///< interval for next scale update
739 PerformanceElement element; ///< what element this window renders graph for
740 Dimension graph_size; ///< size of the main graph area (excluding axis labels)
742 FrametimeGraphWindow(WindowDesc *desc, WindowNumber number) : Window(desc)
744 this->element = (PerformanceElement)number;
745 this->horizontal_scale = 4;
746 this->vertical_scale = TIMESTAMP_PRECISION / 10;
747 this->next_scale_update.SetInterval(1);
749 this->InitNested(number);
752 void SetStringParameters(int widget) const override
754 switch (widget) {
755 case WID_FGW_CAPTION:
756 if (this->element < PFE_AI0) {
757 SetDParam(0, STR_FRAMETIME_CAPTION_GAMELOOP + this->element);
758 } else {
759 SetDParam(0, STR_FRAMETIME_CAPTION_AI);
760 SetDParam(1, this->element - PFE_AI0 + 1);
761 SetDParamStr(2, GetAIName(this->element - PFE_AI0));
763 break;
767 void UpdateWidgetSize(int widget, Dimension *size, const Dimension &padding, Dimension *fill, Dimension *resize) override
769 if (widget == WID_FGW_GRAPH) {
770 SetDParam(0, 100);
771 Dimension size_ms_label = GetStringBoundingBox(STR_FRAMERATE_GRAPH_MILLISECONDS);
772 SetDParam(0, 100);
773 Dimension size_s_label = GetStringBoundingBox(STR_FRAMERATE_GRAPH_SECONDS);
775 /* Size graph in height to fit at least 10 vertical labels with space between, or at least 100 pixels */
776 graph_size.height = std::max(100u, 10 * (size_ms_label.height + 1));
777 /* Always 2:1 graph area */
778 graph_size.width = 2 * graph_size.height;
779 *size = graph_size;
781 size->width += size_ms_label.width + 2;
782 size->height += size_s_label.height + 2;
786 void SelectHorizontalScale(TimingMeasurement range)
788 /* Determine horizontal scale based on period covered by 60 points
789 * (slightly less than 2 seconds at full game speed) */
790 struct ScaleDef { TimingMeasurement range; int scale; };
791 static const ScaleDef hscales[] = {
792 { 120, 60 },
793 { 10, 20 },
794 { 5, 10 },
795 { 3, 4 },
796 { 1, 2 },
798 for (const ScaleDef *sc = hscales; sc < hscales + lengthof(hscales); sc++) {
799 if (range < sc->range) this->horizontal_scale = sc->scale;
803 void SelectVerticalScale(TimingMeasurement range)
805 /* Determine vertical scale based on peak value (within the horizontal scale + a bit) */
806 static const TimingMeasurement vscales[] = {
807 TIMESTAMP_PRECISION * 100,
808 TIMESTAMP_PRECISION * 10,
809 TIMESTAMP_PRECISION * 5,
810 TIMESTAMP_PRECISION,
811 TIMESTAMP_PRECISION / 2,
812 TIMESTAMP_PRECISION / 5,
813 TIMESTAMP_PRECISION / 10,
814 TIMESTAMP_PRECISION / 50,
815 TIMESTAMP_PRECISION / 200,
817 for (const TimingMeasurement *sc = vscales; sc < vscales + lengthof(vscales); sc++) {
818 if (range < *sc) this->vertical_scale = (int)*sc;
822 /** Recalculate the graph scaling factors based on current recorded data */
823 void UpdateScale()
825 const TimingMeasurement *durations = _pf_data[this->element].durations;
826 const TimingMeasurement *timestamps = _pf_data[this->element].timestamps;
827 int num_valid = _pf_data[this->element].num_valid;
828 int point = _pf_data[this->element].prev_index;
830 TimingMeasurement lastts = timestamps[point];
831 TimingMeasurement time_sum = 0;
832 TimingMeasurement peak_value = 0;
833 int count = 0;
835 /* Sensible default for when too few measurements are available */
836 this->horizontal_scale = 4;
838 for (int i = 1; i < num_valid; i++) {
839 point--;
840 if (point < 0) point = NUM_FRAMERATE_POINTS - 1;
842 TimingMeasurement value = durations[point];
843 if (value == PerformanceData::INVALID_DURATION) {
844 /* Skip gaps in data by pretending time is continuous across them */
845 lastts = timestamps[point];
846 continue;
848 if (value > peak_value) peak_value = value;
849 count++;
851 /* Accumulate period of time covered by data */
852 time_sum += lastts - timestamps[point];
853 lastts = timestamps[point];
855 /* Enough data to select a range and get decent data density */
856 if (count == 60) this->SelectHorizontalScale(time_sum / TIMESTAMP_PRECISION);
858 /* End when enough points have been collected and the horizontal scale has been exceeded */
859 if (count >= 60 && time_sum >= (this->horizontal_scale + 2) * TIMESTAMP_PRECISION / 2) break;
862 this->SelectVerticalScale(peak_value);
865 void OnRealtimeTick(uint delta_ms) override
867 this->SetDirty();
869 if (this->next_scale_update.Elapsed(delta_ms)) {
870 this->next_scale_update.SetInterval(500);
871 this->UpdateScale();
875 /** Scale and interpolate a value from a source range into a destination range */
876 template<typename T>
877 static inline T Scinterlate(T dst_min, T dst_max, T src_min, T src_max, T value)
879 T dst_diff = dst_max - dst_min;
880 T src_diff = src_max - src_min;
881 return (value - src_min) * dst_diff / src_diff + dst_min;
884 void DrawWidget(const Rect &r, int widget) const override
886 if (widget == WID_FGW_GRAPH) {
887 const TimingMeasurement *durations = _pf_data[this->element].durations;
888 const TimingMeasurement *timestamps = _pf_data[this->element].timestamps;
889 int point = _pf_data[this->element].prev_index;
891 const int x_zero = r.right - (int)this->graph_size.width;
892 const int x_max = r.right;
893 const int y_zero = r.top + (int)this->graph_size.height;
894 const int y_max = r.top;
895 const int c_grid = PC_DARK_GREY;
896 const int c_lines = PC_BLACK;
897 const int c_peak = PC_DARK_RED;
899 const TimingMeasurement draw_horz_scale = (TimingMeasurement)this->horizontal_scale * TIMESTAMP_PRECISION / 2;
900 const TimingMeasurement draw_vert_scale = (TimingMeasurement)this->vertical_scale;
902 /* Number of \c horizontal_scale units in each horizontal division */
903 const uint horz_div_scl = (this->horizontal_scale <= 20) ? 1 : 10;
904 /* Number of divisions of the horizontal axis */
905 const uint horz_divisions = this->horizontal_scale / horz_div_scl;
906 /* Number of divisions of the vertical axis */
907 const uint vert_divisions = 10;
909 /* Draw division lines and labels for the vertical axis */
910 for (uint division = 0; division < vert_divisions; division++) {
911 int y = Scinterlate(y_zero, y_max, 0, (int)vert_divisions, (int)division);
912 GfxDrawLine(x_zero, y, x_max, y, c_grid);
913 if (division % 2 == 0) {
914 if ((TimingMeasurement)this->vertical_scale > TIMESTAMP_PRECISION) {
915 SetDParam(0, this->vertical_scale * division / 10 / TIMESTAMP_PRECISION);
916 DrawString(r.left, x_zero - 2, y - FONT_HEIGHT_SMALL, STR_FRAMERATE_GRAPH_SECONDS, TC_GREY, SA_RIGHT | SA_FORCE, false, FS_SMALL);
917 } else {
918 SetDParam(0, this->vertical_scale * division / 10 * 1000 / TIMESTAMP_PRECISION);
919 DrawString(r.left, x_zero - 2, y - FONT_HEIGHT_SMALL, STR_FRAMERATE_GRAPH_MILLISECONDS, TC_GREY, SA_RIGHT | SA_FORCE, false, FS_SMALL);
923 /* Draw division lines and labels for the horizontal axis */
924 for (uint division = horz_divisions; division > 0; division--) {
925 int x = Scinterlate(x_zero, x_max, 0, (int)horz_divisions, (int)horz_divisions - (int)division);
926 GfxDrawLine(x, y_max, x, y_zero, c_grid);
927 if (division % 2 == 0) {
928 SetDParam(0, division * horz_div_scl / 2);
929 DrawString(x, x_max, y_zero + 2, STR_FRAMERATE_GRAPH_SECONDS, TC_GREY, SA_LEFT | SA_FORCE, false, FS_SMALL);
933 /* Position of last rendered data point */
934 Point lastpoint = {
935 x_max,
936 (int)Scinterlate<int64>(y_zero, y_max, 0, this->vertical_scale, durations[point])
938 /* Timestamp of last rendered data point */
939 TimingMeasurement lastts = timestamps[point];
941 TimingMeasurement peak_value = 0;
942 Point peak_point = { 0, 0 };
943 TimingMeasurement value_sum = 0;
944 TimingMeasurement time_sum = 0;
945 int points_drawn = 0;
947 for (int i = 1; i < NUM_FRAMERATE_POINTS; i++) {
948 point--;
949 if (point < 0) point = NUM_FRAMERATE_POINTS - 1;
951 TimingMeasurement value = durations[point];
952 if (value == PerformanceData::INVALID_DURATION) {
953 /* Skip gaps in measurements, pretend the data points on each side are continuous */
954 lastts = timestamps[point];
955 continue;
958 /* Use total time period covered for value along horizontal axis */
959 time_sum += lastts - timestamps[point];
960 lastts = timestamps[point];
961 /* Stop if past the width of the graph */
962 if (time_sum > draw_horz_scale) break;
964 /* Draw line from previous point to new point */
965 Point newpoint = {
966 (int)Scinterlate<int64>(x_zero, x_max, 0, (int64)draw_horz_scale, (int64)draw_horz_scale - (int64)time_sum),
967 (int)Scinterlate<int64>(y_zero, y_max, 0, (int64)draw_vert_scale, (int64)value)
969 assert(newpoint.x <= lastpoint.x);
970 GfxDrawLine(lastpoint.x, lastpoint.y, newpoint.x, newpoint.y, c_lines);
971 lastpoint = newpoint;
973 /* Record peak and average value across graphed data */
974 value_sum += value;
975 points_drawn++;
976 if (value > peak_value) {
977 peak_value = value;
978 peak_point = newpoint;
982 /* If the peak value is significantly larger than the average, mark and label it */
983 if (points_drawn > 0 && peak_value > TIMESTAMP_PRECISION / 100 && 2 * peak_value > 3 * value_sum / points_drawn) {
984 TextColour tc_peak = (TextColour)(TC_IS_PALETTE_COLOUR | c_peak);
985 GfxFillRect(peak_point.x - 1, peak_point.y - 1, peak_point.x + 1, peak_point.y + 1, c_peak);
986 SetDParam(0, peak_value * 1000 / TIMESTAMP_PRECISION);
987 int label_y = std::max(y_max, peak_point.y - FONT_HEIGHT_SMALL);
988 if (peak_point.x - x_zero > (int)this->graph_size.width / 2) {
989 DrawString(x_zero, peak_point.x - 2, label_y, STR_FRAMERATE_GRAPH_MILLISECONDS, tc_peak, SA_RIGHT | SA_FORCE, false, FS_SMALL);
990 } else {
991 DrawString(peak_point.x + 2, x_max, label_y, STR_FRAMERATE_GRAPH_MILLISECONDS, tc_peak, SA_LEFT | SA_FORCE, false, FS_SMALL);
998 static WindowDesc _frametime_graph_window_desc(
999 WDP_AUTO, "frametime_graph", 140, 90,
1000 WC_FRAMETIME_GRAPH, WC_NONE,
1002 _frametime_graph_window_widgets, lengthof(_frametime_graph_window_widgets)
1007 /** Open the general framerate window */
1008 void ShowFramerateWindow()
1010 AllocateWindowDescFront<FramerateWindow>(&_framerate_display_desc, 0);
1013 /** Open a graph window for a performance element */
1014 void ShowFrametimeGraphWindow(PerformanceElement elem)
1016 if (elem < PFE_FIRST || elem >= PFE_MAX) return; // maybe warn?
1017 AllocateWindowDescFront<FrametimeGraphWindow>(&_frametime_graph_window_desc, elem, true);
1020 /** Print performance statistics to game console */
1021 void ConPrintFramerate()
1023 const int count1 = NUM_FRAMERATE_POINTS / 8;
1024 const int count2 = NUM_FRAMERATE_POINTS / 4;
1025 const int count3 = NUM_FRAMERATE_POINTS / 1;
1027 IConsolePrintF(TC_SILVER, "Based on num. data points: %d %d %d", count1, count2, count3);
1029 static const char *MEASUREMENT_NAMES[PFE_MAX] = {
1030 "Game loop",
1031 " GL station ticks",
1032 " GL train ticks",
1033 " GL road vehicle ticks",
1034 " GL ship ticks",
1035 " GL aircraft ticks",
1036 " GL landscape ticks",
1037 " GL link graph delays",
1038 "Drawing",
1039 " Viewport drawing",
1040 "Video output",
1041 "Sound mixing",
1042 "AI/GS scripts total",
1043 "Game script",
1045 char ai_name_buf[128];
1047 static const PerformanceElement rate_elements[] = { PFE_GAMELOOP, PFE_DRAWING, PFE_VIDEO };
1049 bool printed_anything = false;
1051 for (const PerformanceElement *e = rate_elements; e < rate_elements + lengthof(rate_elements); e++) {
1052 auto &pf = _pf_data[*e];
1053 if (pf.num_valid == 0) continue;
1054 IConsolePrintF(TC_GREEN, "%s rate: %.2ffps (expected: %.2ffps)",
1055 MEASUREMENT_NAMES[*e],
1056 pf.GetRate(),
1057 pf.expected_rate);
1058 printed_anything = true;
1061 for (PerformanceElement e = PFE_FIRST; e < PFE_MAX; e++) {
1062 auto &pf = _pf_data[e];
1063 if (pf.num_valid == 0) continue;
1064 const char *name;
1065 if (e < PFE_AI0) {
1066 name = MEASUREMENT_NAMES[e];
1067 } else {
1068 seprintf(ai_name_buf, lastof(ai_name_buf), "AI %d %s", e - PFE_AI0 + 1, GetAIName(e - PFE_AI0)),
1069 name = ai_name_buf;
1071 IConsolePrintF(TC_LIGHT_BLUE, "%s times: %.2fms %.2fms %.2fms",
1072 name,
1073 pf.GetAverageDurationMilliseconds(count1),
1074 pf.GetAverageDurationMilliseconds(count2),
1075 pf.GetAverageDurationMilliseconds(count3));
1076 printed_anything = true;
1079 if (!printed_anything) {
1080 IConsoleWarning("No performance measurements have been taken yet");