OpenTTD
framerate_gui.cpp
Go to the documentation of this file.
1 /* $Id$ */
2 
3 /*
4 * This file is part of OpenTTD.
5 * 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.
6 * 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.
7 * 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 */
9 
12 #include "framerate_type.h"
13 #include <chrono>
14 #include "gfx_func.h"
15 #include "window_gui.h"
16 #include "table/sprites.h"
17 #include "strings_func.h"
18 #include "console_func.h"
19 #include "console_type.h"
20 #include "guitimer_func.h"
21 
23 
24 
28 namespace {
29 
31  const int NUM_FRAMERATE_POINTS = 512;
34 
35  struct PerformanceData {
37  static const TimingMeasurement INVALID_DURATION = UINT64_MAX;
38 
44  double expected_rate;
50  int num_valid;
51 
56 
63  explicit PerformanceData(double expected_rate) : expected_rate(expected_rate), next_index(0), prev_index(0), num_valid(0) { }
64 
66  void Add(TimingMeasurement start_time, TimingMeasurement end_time)
67  {
68  this->durations[this->next_index] = end_time - start_time;
69  this->timestamps[this->next_index] = start_time;
70  this->prev_index = this->next_index;
71  this->next_index += 1;
72  if (this->next_index >= NUM_FRAMERATE_POINTS) this->next_index = 0;
73  this->num_valid = min(NUM_FRAMERATE_POINTS, this->num_valid + 1);
74  }
75 
78  {
79  this->timestamps[this->next_index] = this->acc_timestamp;
80  this->durations[this->next_index] = this->acc_duration;
81  this->prev_index = this->next_index;
82  this->next_index += 1;
83  if (this->next_index >= NUM_FRAMERATE_POINTS) this->next_index = 0;
84  this->num_valid = min(NUM_FRAMERATE_POINTS, this->num_valid + 1);
85 
86  this->acc_duration = 0;
87  this->acc_timestamp = start_time;
88  }
89 
92  {
93  this->acc_duration += duration;
94  }
95 
97  void AddPause(TimingMeasurement start_time)
98  {
99  if (this->durations[this->prev_index] != INVALID_DURATION) {
100  this->timestamps[this->next_index] = start_time;
101  this->durations[this->next_index] = INVALID_DURATION;
102  this->prev_index = this->next_index;
103  this->next_index += 1;
104  if (this->next_index >= NUM_FRAMERATE_POINTS) this->next_index = 0;
105  this->num_valid += 1;
106  }
107  }
108 
111  {
112  count = min(count, this->num_valid);
113 
114  int first_point = this->prev_index - count;
115  if (first_point < 0) first_point += NUM_FRAMERATE_POINTS;
116 
117  /* Sum durations, skipping invalid points */
118  double sumtime = 0;
119  for (int i = first_point; i < first_point + count; i++) {
120  auto d = this->durations[i % NUM_FRAMERATE_POINTS];
121  if (d != INVALID_DURATION) {
122  sumtime += d;
123  } else {
124  /* Don't count the invalid durations */
125  count--;
126  }
127  }
128 
129  if (count == 0) return 0; // avoid div by zero
130  return sumtime * 1000 / count / TIMESTAMP_PRECISION;
131  }
132 
134  double GetRate()
135  {
136  /* Start at last recorded point, end at latest when reaching the earliest recorded point */
137  int point = this->prev_index;
138  int last_point = this->next_index - this->num_valid;
139  if (last_point < 0) last_point += NUM_FRAMERATE_POINTS;
140 
141  /* Number of data points collected */
142  int count = 0;
143  /* Time of previous data point */
144  TimingMeasurement last = this->timestamps[point];
145  /* Total duration covered by collected points */
146  TimingMeasurement total = 0;
147 
148  while (point != last_point) {
149  /* Only record valid data points, but pretend the gaps in measurements aren't there */
150  if (this->durations[point] != INVALID_DURATION) {
151  total += last - this->timestamps[point];
152  count++;
153  }
154  last = this->timestamps[point];
155  if (total >= TIMESTAMP_PRECISION) break; // end after 1 second has been collected
156  point--;
157  if (point < 0) point = NUM_FRAMERATE_POINTS - 1;
158  }
159 
160  if (total == 0 || count == 0) return 0;
161  return (double)count * TIMESTAMP_PRECISION / total;
162  }
163  };
164 
166  static const double GL_RATE = 1000.0 / MILLISECONDS_PER_TICK;
167 
174  PerformanceData(GL_RATE), // PFE_GAMELOOP
175  PerformanceData(1), // PFE_ACC_GL_ECONOMY
176  PerformanceData(1), // PFE_ACC_GL_TRAINS
177  PerformanceData(1), // PFE_ACC_GL_ROADVEHS
178  PerformanceData(1), // PFE_ACC_GL_SHIPS
179  PerformanceData(1), // PFE_ACC_GL_AIRCRAFT
180  PerformanceData(1), // PFE_GL_LANDSCAPE
181  PerformanceData(1), // PFE_GL_LINKGRAPH
182  PerformanceData(GL_RATE), // PFE_DRAWING
183  PerformanceData(1), // PFE_ACC_DRAWWORLD
184  PerformanceData(60.0), // PFE_VIDEO
185  PerformanceData(1000.0 * 8192 / 44100), // PFE_SOUND
186  };
187 
188 }
189 
190 
197 {
198  using namespace std::chrono;
199  return (TimingMeasurement)time_point_cast<microseconds>(high_resolution_clock::now()).time_since_epoch().count();
200 }
201 
202 
208 {
209  assert(elem < PFE_MAX);
210 
211  this->elem = elem;
212  this->start_time = GetPerformanceTimer();
213 }
214 
217 {
218  _pf_data[this->elem].Add(this->start_time, GetPerformanceTimer());
219 }
220 
223 {
224  _pf_data[this->elem].expected_rate = rate;
225 }
226 
232 {
234 }
235 
236 
242 {
243  assert(elem < PFE_MAX);
244 
245  this->elem = elem;
246  this->start_time = GetPerformanceTimer();
247 }
248 
251 {
252  _pf_data[this->elem].AddAccumulate(GetPerformanceTimer() - this->start_time);
253 }
254 
261 {
263 }
264 
265 
267 
268 
270 static const NWidgetPart _framerate_window_widgets[] = {
272  NWidget(WWT_CLOSEBOX, COLOUR_GREY),
273  NWidget(WWT_CAPTION, COLOUR_GREY, WID_FRW_CAPTION), SetDataTip(STR_FRAMERATE_CAPTION, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS),
274  NWidget(WWT_SHADEBOX, COLOUR_GREY),
275  NWidget(WWT_STICKYBOX, COLOUR_GREY),
276  EndContainer(),
277  NWidget(WWT_PANEL, COLOUR_GREY),
278  NWidget(NWID_VERTICAL), SetPadding(6), SetPIP(0, 3, 0),
279  NWidget(WWT_TEXT, COLOUR_GREY, WID_FRW_RATE_GAMELOOP), SetDataTip(STR_FRAMERATE_RATE_GAMELOOP, STR_FRAMERATE_RATE_GAMELOOP_TOOLTIP),
280  NWidget(WWT_TEXT, COLOUR_GREY, WID_FRW_RATE_DRAWING), SetDataTip(STR_FRAMERATE_RATE_BLITTER, STR_FRAMERATE_RATE_BLITTER_TOOLTIP),
281  NWidget(WWT_TEXT, COLOUR_GREY, WID_FRW_RATE_FACTOR), SetDataTip(STR_FRAMERATE_SPEED_FACTOR, STR_FRAMERATE_SPEED_FACTOR_TOOLTIP),
282  EndContainer(),
283  EndContainer(),
284  NWidget(WWT_PANEL, COLOUR_GREY),
285  NWidget(NWID_VERTICAL), SetPadding(6), SetPIP(0, 3, 0),
286  NWidget(NWID_HORIZONTAL), SetPIP(0, 6, 0),
287  NWidget(WWT_EMPTY, COLOUR_GREY, WID_FRW_TIMES_NAMES),
288  NWidget(WWT_EMPTY, COLOUR_GREY, WID_FRW_TIMES_CURRENT),
289  NWidget(WWT_EMPTY, COLOUR_GREY, WID_FRW_TIMES_AVERAGE),
290  EndContainer(),
291  NWidget(WWT_TEXT, COLOUR_GREY, WID_FRW_INFO_DATA_POINTS), SetDataTip(STR_FRAMERATE_DATA_POINTS, 0x0),
292  EndContainer(),
293  EndContainer(),
294 };
295 
297  bool small;
298  GUITimer next_update;
299 
300  struct CachedDecimal {
301  StringID strid;
302  uint32 value;
303 
304  inline void SetRate(double value, double target)
305  {
306  const double threshold_good = target * 0.95;
307  const double threshold_bad = target * 2 / 3;
308  value = min(9999.99, value);
309  this->value = (uint32)(value * 100);
310  this->strid = (value > threshold_good) ? STR_FRAMERATE_FPS_GOOD : (value < threshold_bad) ? STR_FRAMERATE_FPS_BAD : STR_FRAMERATE_FPS_WARN;
311  }
312 
313  inline void SetTime(double value, double target)
314  {
315  const double threshold_good = target / 3;
316  const double threshold_bad = target;
317  value = min(9999.99, value);
318  this->value = (uint32)(value * 100);
319  this->strid = (value < threshold_good) ? STR_FRAMERATE_MS_GOOD : (value > threshold_bad) ? STR_FRAMERATE_MS_BAD : STR_FRAMERATE_MS_WARN;
320  }
321 
322  inline void InsertDParams(uint n) const
323  {
324  SetDParam(n, this->value);
325  SetDParam(n + 1, 2);
326  }
327  };
328 
332  CachedDecimal times_shortterm[PFE_MAX];
333  CachedDecimal times_longterm[PFE_MAX];
334 
335  static const int VSPACING = 3;
336 
337  FramerateWindow(WindowDesc *desc, WindowNumber number) : Window(desc)
338  {
339  this->InitNested(number);
340  this->small = this->IsShaded();
341  this->UpdateData();
342  this->next_update.SetInterval(100);
343  }
344 
345  virtual void OnRealtimeTick(uint delta_ms)
346  {
347  bool elapsed = this->next_update.Elapsed(delta_ms);
348 
349  /* Check if the shaded state has changed, switch caption text if it has */
350  if (this->small != this->IsShaded()) {
351  this->small = this->IsShaded();
352  this->GetWidget<NWidgetLeaf>(WID_FRW_CAPTION)->SetDataTip(this->small ? STR_FRAMERATE_CAPTION_SMALL : STR_FRAMERATE_CAPTION, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS);
353  elapsed = true;
354  }
355 
356  if (elapsed) {
357  this->UpdateData();
358  this->SetDirty();
359  this->next_update.SetInterval(100);
360  }
361  }
362 
363  void UpdateData()
364  {
365  double gl_rate = _pf_data[PFE_GAMELOOP].GetRate();
366  this->rate_gameloop.SetRate(gl_rate, _pf_data[PFE_GAMELOOP].expected_rate);
367  this->speed_gameloop.SetRate(gl_rate / _pf_data[PFE_GAMELOOP].expected_rate, 1.0);
368  if (this->small) return; // in small mode, this is everything needed
369 
370  this->rate_drawing.SetRate(_pf_data[PFE_DRAWING].GetRate(), _pf_data[PFE_DRAWING].expected_rate);
371 
372  for (PerformanceElement e = PFE_FIRST; e < PFE_MAX; e++) {
373  this->times_shortterm[e].SetTime(_pf_data[e].GetAverageDurationMilliseconds(8), MILLISECONDS_PER_TICK);
374  this->times_longterm[e].SetTime(_pf_data[e].GetAverageDurationMilliseconds(NUM_FRAMERATE_POINTS), MILLISECONDS_PER_TICK);
375  }
376  }
377 
378  virtual void SetStringParameters(int widget) const
379  {
380  switch (widget) {
381  case WID_FRW_CAPTION:
382  /* When the window is shaded, the caption shows game loop rate and speed factor */
383  if (!this->small) break;
384  SetDParam(0, this->rate_gameloop.strid);
385  this->rate_gameloop.InsertDParams(1);
386  this->speed_gameloop.InsertDParams(3);
387  break;
388 
389  case WID_FRW_RATE_GAMELOOP:
390  SetDParam(0, this->rate_gameloop.strid);
391  this->rate_gameloop.InsertDParams(1);
392  break;
393  case WID_FRW_RATE_DRAWING:
394  SetDParam(0, this->rate_drawing.strid);
395  this->rate_drawing.InsertDParams(1);
396  break;
397  case WID_FRW_RATE_FACTOR:
398  this->speed_gameloop.InsertDParams(0);
399  break;
400  case WID_FRW_INFO_DATA_POINTS:
402  break;
403  }
404  }
405 
406  virtual void UpdateWidgetSize(int widget, Dimension *size, const Dimension &padding, Dimension *fill, Dimension *resize)
407  {
408  switch (widget) {
409  case WID_FRW_RATE_GAMELOOP:
410  SetDParam(0, STR_FRAMERATE_FPS_GOOD);
411  SetDParam(1, 999999);
412  SetDParam(2, 2);
413  *size = GetStringBoundingBox(STR_FRAMERATE_RATE_GAMELOOP);
414  break;
415  case WID_FRW_RATE_DRAWING:
416  SetDParam(0, STR_FRAMERATE_FPS_GOOD);
417  SetDParam(1, 999999);
418  SetDParam(2, 2);
419  *size = GetStringBoundingBox(STR_FRAMERATE_RATE_BLITTER);
420  break;
421  case WID_FRW_RATE_FACTOR:
422  SetDParam(0, 999999);
423  SetDParam(1, 2);
424  *size = GetStringBoundingBox(STR_FRAMERATE_SPEED_FACTOR);
425  break;
426 
427  case WID_FRW_TIMES_NAMES: {
428  int linecount = PFE_MAX - PFE_FIRST;
429  size->width = 0;
430  size->height = FONT_HEIGHT_NORMAL * (linecount + 1) + VSPACING;
431  for (int line = 0; line < linecount; line++) {
432  Dimension line_size = GetStringBoundingBox(STR_FRAMERATE_GAMELOOP + line);
433  size->width = max(size->width, line_size.width);
434  }
435  break;
436  }
437 
438  case WID_FRW_TIMES_CURRENT:
439  case WID_FRW_TIMES_AVERAGE: {
440  int linecount = PFE_MAX - PFE_FIRST;
441  *size = GetStringBoundingBox(STR_FRAMERATE_CURRENT + (widget - WID_FRW_TIMES_CURRENT));
442  SetDParam(0, 999999);
443  SetDParam(1, 2);
444  Dimension item_size = GetStringBoundingBox(STR_FRAMERATE_MS_GOOD);
445  size->width = max(size->width, item_size.width);
446  size->height += FONT_HEIGHT_NORMAL * linecount + VSPACING;
447  break;
448  }
449  }
450  }
451 
453  void DrawElementTimesColumn(const Rect &r, StringID heading_str, const CachedDecimal *values) const
454  {
455  int y = r.top;
456  DrawString(r.left, r.right, y, heading_str, TC_FROMSTRING, SA_CENTER, true);
457  y += FONT_HEIGHT_NORMAL + VSPACING;
458 
459  for (PerformanceElement e = PFE_FIRST; e < PFE_MAX; e++) {
460  values[e].InsertDParams(0);
461  DrawString(r.left, r.right, y, values[e].strid, TC_FROMSTRING, SA_RIGHT);
462  y += FONT_HEIGHT_NORMAL;
463  }
464  }
465 
466  virtual void DrawWidget(const Rect &r, int widget) const
467  {
468  switch (widget) {
469  case WID_FRW_TIMES_NAMES: {
470  /* Render a column of titles for performance element names */
471  int linecount = PFE_MAX - PFE_FIRST;
472  int y = r.top + FONT_HEIGHT_NORMAL + VSPACING; // first line contains headings in the value columns
473  for (int i = 0; i < linecount; i++) {
474  DrawString(r.left, r.right, y, STR_FRAMERATE_GAMELOOP + i, TC_FROMSTRING, SA_LEFT);
475  y += FONT_HEIGHT_NORMAL;
476  }
477  break;
478  }
479  case WID_FRW_TIMES_CURRENT:
480  /* Render short-term average values */
481  DrawElementTimesColumn(r, STR_FRAMERATE_CURRENT, this->times_shortterm);
482  break;
483  case WID_FRW_TIMES_AVERAGE:
484  /* Render averages of all recorded values */
485  DrawElementTimesColumn(r, STR_FRAMERATE_AVERAGE, this->times_longterm);
486  break;
487  }
488  }
489 
490  virtual void OnClick(Point pt, int widget, int click_count)
491  {
492  switch (widget) {
493  case WID_FRW_TIMES_NAMES:
494  case WID_FRW_TIMES_CURRENT:
495  case WID_FRW_TIMES_AVERAGE: {
496  /* Open time graph windows when clicking detail measurement lines */
497  int line = this->GetRowFromWidget(pt.y, widget, VSPACING, FONT_HEIGHT_NORMAL);
498  if (line > 0) {
499  line -= 1;
501  }
502  break;
503  }
504  }
505  }
506 };
507 
508 static WindowDesc _framerate_display_desc(
509  WDP_AUTO, "framerate_display", 60, 40,
511  0,
512  _framerate_window_widgets, lengthof(_framerate_window_widgets)
513 );
514 
515 
517 static const NWidgetPart _frametime_graph_window_widgets[] = {
519  NWidget(WWT_CLOSEBOX, COLOUR_GREY),
520  NWidget(WWT_CAPTION, COLOUR_GREY, WID_FGW_CAPTION), SetDataTip(STR_WHITE_STRING, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS),
521  NWidget(WWT_STICKYBOX, COLOUR_GREY),
522  EndContainer(),
523  NWidget(WWT_PANEL, COLOUR_GREY),
525  NWidget(WWT_EMPTY, COLOUR_GREY, WID_FGW_GRAPH),
526  EndContainer(),
527  EndContainer(),
528 };
529 
534 
537 
538  FrametimeGraphWindow(WindowDesc *desc, WindowNumber number) : Window(desc)
539  {
540  this->element = (PerformanceElement)number;
541  this->horizontal_scale = 4;
542  this->vertical_scale = TIMESTAMP_PRECISION / 10;
543  this->next_scale_update.SetInterval(1);
544 
545  this->InitNested(number);
546  }
547 
548  virtual void SetStringParameters(int widget) const
549  {
550  switch (widget) {
551  case WID_FGW_CAPTION:
552  SetDParam(0, STR_FRAMETIME_CAPTION_GAMELOOP + this->element);
553  break;
554  }
555  }
556 
557  virtual void UpdateWidgetSize(int widget, Dimension *size, const Dimension &padding, Dimension *fill, Dimension *resize)
558  {
559  if (widget == WID_FGW_GRAPH) {
560  SetDParam(0, 100);
561  Dimension size_ms_label = GetStringBoundingBox(STR_FRAMERATE_GRAPH_MILLISECONDS);
562  SetDParam(0, 100);
563  Dimension size_s_label = GetStringBoundingBox(STR_FRAMERATE_GRAPH_SECONDS);
564 
565  /* Size graph in height to fit at least 10 vertical labels with space between, or at least 100 pixels */
566  graph_size.height = max<uint>(100, 10 * (size_ms_label.height + 1));
567  /* Always 2:1 graph area */
568  graph_size.width = 2 * graph_size.height;
569  *size = graph_size;
570 
571  size->width += size_ms_label.width + 2;
572  size->height += size_s_label.height + 2;
573  }
574  }
575 
576  void SelectHorizontalScale(TimingMeasurement range)
577  {
578  /* Determine horizontal scale based on period covered by 60 points
579  * (slightly less than 2 seconds at full game speed) */
580  struct ScaleDef { TimingMeasurement range; int scale; };
581  static const ScaleDef hscales[] = {
582  { 120, 60 },
583  { 10, 20 },
584  { 5, 10 },
585  { 3, 4 },
586  { 1, 2 },
587  };
588  for (const ScaleDef *sc = hscales; sc < hscales + lengthof(hscales); sc++) {
589  if (range < sc->range) this->horizontal_scale = sc->scale;
590  }
591  }
592 
593  void SelectVerticalScale(TimingMeasurement range)
594  {
595  /* Determine vertical scale based on peak value (within the horizontal scale + a bit) */
596  static const TimingMeasurement vscales[] = {
597  TIMESTAMP_PRECISION * 100,
598  TIMESTAMP_PRECISION * 10,
599  TIMESTAMP_PRECISION * 5,
601  TIMESTAMP_PRECISION / 2,
602  TIMESTAMP_PRECISION / 5,
603  TIMESTAMP_PRECISION / 10,
604  TIMESTAMP_PRECISION / 50,
605  TIMESTAMP_PRECISION / 200,
606  };
607  for (const TimingMeasurement *sc = vscales; sc < vscales + lengthof(vscales); sc++) {
608  if (range < *sc) this->vertical_scale = (int)*sc;
609  }
610  }
611 
613  void UpdateScale()
614  {
615  const TimingMeasurement *durations = _pf_data[this->element].durations;
616  const TimingMeasurement *timestamps = _pf_data[this->element].timestamps;
617  int num_valid = _pf_data[this->element].num_valid;
618  int point = _pf_data[this->element].prev_index;
619 
620  TimingMeasurement lastts = timestamps[point];
621  TimingMeasurement time_sum = 0;
622  TimingMeasurement peak_value = 0;
623  int count = 0;
624 
625  /* Sensible default for when too few measurements are available */
626  this->horizontal_scale = 4;
627 
628  for (int i = 1; i < num_valid; i++) {
629  point--;
630  if (point < 0) point = NUM_FRAMERATE_POINTS - 1;
631 
632  TimingMeasurement value = durations[point];
633  if (value == PerformanceData::INVALID_DURATION) {
634  /* Skip gaps in data by pretending time is continuous across them */
635  lastts = timestamps[point];
636  continue;
637  }
638  if (value > peak_value) peak_value = value;
639  count++;
640 
641  /* Accumulate period of time covered by data */
642  time_sum += lastts - timestamps[point];
643  lastts = timestamps[point];
644 
645  /* Enough data to select a range and get decent data density */
646  if (count == 60) this->SelectHorizontalScale(time_sum / TIMESTAMP_PRECISION);
647 
648  /* End when enough points have been collected and the horizontal scale has been exceeded */
649  if (count >= 60 && time_sum >= (this->horizontal_scale + 2) * TIMESTAMP_PRECISION / 2) break;
650  }
651 
652  this->SelectVerticalScale(peak_value);
653  }
654 
655  virtual void OnRealtimeTick(uint delta_ms)
656  {
657  this->SetDirty();
658 
659  if (this->next_scale_update.Elapsed(delta_ms)) {
660  this->next_scale_update.SetInterval(500);
661  this->UpdateScale();
662  }
663  }
664 
666  template<typename T>
667  static inline T Scinterlate(T dst_min, T dst_max, T src_min, T src_max, T value)
668  {
669  T dst_diff = dst_max - dst_min;
670  T src_diff = src_max - src_min;
671  return (value - src_min) * dst_diff / src_diff + dst_min;
672  }
673 
674  virtual void DrawWidget(const Rect &r, int widget) const
675  {
676  if (widget == WID_FGW_GRAPH) {
677  const TimingMeasurement *durations = _pf_data[this->element].durations;
678  const TimingMeasurement *timestamps = _pf_data[this->element].timestamps;
679  int point = _pf_data[this->element].prev_index;
680 
681  const int x_zero = r.right - (int)this->graph_size.width;
682  const int x_max = r.right;
683  const int y_zero = r.top + (int)this->graph_size.height;
684  const int y_max = r.top;
685  const int c_grid = PC_DARK_GREY;
686  const int c_lines = PC_BLACK;
687  const int c_peak = PC_DARK_RED;
688 
689  const TimingMeasurement draw_horz_scale = (TimingMeasurement)this->horizontal_scale * TIMESTAMP_PRECISION / 2;
690  const TimingMeasurement draw_vert_scale = (TimingMeasurement)this->vertical_scale;
691 
692  /* Number of \c horizontal_scale units in each horizontal division */
693  const uint horz_div_scl = (this->horizontal_scale <= 20) ? 1 : 10;
694  /* Number of divisions of the horizontal axis */
695  const uint horz_divisions = this->horizontal_scale / horz_div_scl;
696  /* Number of divisions of the vertical axis */
697  const uint vert_divisions = 10;
698 
699  /* Draw division lines and labels for the vertical axis */
700  for (uint division = 0; division < vert_divisions; division++) {
701  int y = Scinterlate(y_zero, y_max, 0, (int)vert_divisions, (int)division);
702  GfxDrawLine(x_zero, y, x_max, y, c_grid);
703  if (division % 2 == 0) {
704  if ((TimingMeasurement)this->vertical_scale > TIMESTAMP_PRECISION) {
705  SetDParam(0, this->vertical_scale * division / 10 / TIMESTAMP_PRECISION);
706  DrawString(r.left, x_zero - 2, y - FONT_HEIGHT_SMALL, STR_FRAMERATE_GRAPH_SECONDS, TC_GREY, SA_RIGHT | SA_FORCE, false, FS_SMALL);
707  } else {
708  SetDParam(0, this->vertical_scale * division / 10 * 1000 / TIMESTAMP_PRECISION);
709  DrawString(r.left, x_zero - 2, y - FONT_HEIGHT_SMALL, STR_FRAMERATE_GRAPH_MILLISECONDS, TC_GREY, SA_RIGHT | SA_FORCE, false, FS_SMALL);
710  }
711  }
712  }
713  /* Draw divison lines and labels for the horizontal axis */
714  for (uint division = horz_divisions; division > 0; division--) {
715  int x = Scinterlate(x_zero, x_max, 0, (int)horz_divisions, (int)horz_divisions - (int)division);
716  GfxDrawLine(x, y_max, x, y_zero, c_grid);
717  if (division % 2 == 0) {
718  SetDParam(0, division * horz_div_scl / 2);
719  DrawString(x, x_max, y_zero + 2, STR_FRAMERATE_GRAPH_SECONDS, TC_GREY, SA_LEFT | SA_FORCE, false, FS_SMALL);
720  }
721  }
722 
723  /* Position of last rendered data point */
724  Point lastpoint = {
725  x_max,
726  (int)Scinterlate<int64>(y_zero, y_max, 0, this->vertical_scale, durations[point])
727  };
728  /* Timestamp of last rendered data point */
729  TimingMeasurement lastts = timestamps[point];
730 
731  TimingMeasurement peak_value = 0;
732  Point peak_point = { 0, 0 };
733  TimingMeasurement value_sum = 0;
734  TimingMeasurement time_sum = 0;
735  int points_drawn = 0;
736 
737  for (int i = 1; i < NUM_FRAMERATE_POINTS; i++) {
738  point--;
739  if (point < 0) point = NUM_FRAMERATE_POINTS - 1;
740 
741  TimingMeasurement value = durations[point];
742  if (value == PerformanceData::INVALID_DURATION) {
743  /* Skip gaps in measurements, pretend the data points on each side are continuous */
744  lastts = timestamps[point];
745  continue;
746  }
747 
748  /* Use total time period covered for value along horizontal axis */
749  time_sum += lastts - timestamps[point];
750  lastts = timestamps[point];
751  /* Stop if past the width of the graph */
752  if (time_sum > draw_horz_scale) break;
753 
754  /* Draw line from previous point to new point */
755  Point newpoint = {
756  (int)Scinterlate<int64>(x_zero, x_max, 0, (int64)draw_horz_scale, (int64)draw_horz_scale - (int64)time_sum),
757  (int)Scinterlate<int64>(y_zero, y_max, 0, (int64)draw_vert_scale, (int64)value)
758  };
759  assert(newpoint.x <= lastpoint.x);
760  GfxDrawLine(lastpoint.x, lastpoint.y, newpoint.x, newpoint.y, c_lines);
761  lastpoint = newpoint;
762 
763  /* Record peak and average value across graphed data */
764  value_sum += value;
765  points_drawn++;
766  if (value > peak_value) {
767  peak_value = value;
768  peak_point = newpoint;
769  }
770  }
771 
772  /* If the peak value is significantly larger than the average, mark and label it */
773  if (points_drawn > 0 && peak_value > TIMESTAMP_PRECISION / 100 && 2 * peak_value > 3 * value_sum / points_drawn) {
774  TextColour tc_peak = (TextColour)(TC_IS_PALETTE_COLOUR | c_peak);
775  GfxFillRect(peak_point.x - 1, peak_point.y - 1, peak_point.x + 1, peak_point.y + 1, c_peak);
776  SetDParam(0, peak_value * 1000 / TIMESTAMP_PRECISION);
777  int label_y = max(y_max, peak_point.y - FONT_HEIGHT_SMALL);
778  if (peak_point.x - x_zero > (int)this->graph_size.width / 2) {
779  DrawString(x_zero, peak_point.x - 2, label_y, STR_FRAMERATE_GRAPH_MILLISECONDS, tc_peak, SA_RIGHT | SA_FORCE, false, FS_SMALL);
780  } else {
781  DrawString(peak_point.x + 2, x_max, label_y, STR_FRAMERATE_GRAPH_MILLISECONDS, tc_peak, SA_LEFT | SA_FORCE, false, FS_SMALL);
782  }
783  }
784  }
785  }
786 };
787 
788 static WindowDesc _frametime_graph_window_desc(
789  WDP_AUTO, "frametime_graph", 140, 90,
791  0,
792  _frametime_graph_window_widgets, lengthof(_frametime_graph_window_widgets)
793 );
794 
795 
796 
799 {
800  AllocateWindowDescFront<FramerateWindow>(&_framerate_display_desc, 0);
801 }
802 
805 {
806  if (elem < PFE_FIRST || elem >= PFE_MAX) return; // maybe warn?
807  AllocateWindowDescFront<FrametimeGraphWindow>(&_frametime_graph_window_desc, elem, true);
808 }
809 
812 {
813  const int count1 = NUM_FRAMERATE_POINTS / 8;
814  const int count2 = NUM_FRAMERATE_POINTS / 4;
815  const int count3 = NUM_FRAMERATE_POINTS / 1;
816 
817  IConsolePrintF(TC_SILVER, "Based on num. data points: %d %d %d", count1, count2, count3);
818 
819  static const char *MEASUREMENT_NAMES[PFE_MAX] = {
820  "Game loop",
821  " GL station ticks",
822  " GL train ticks",
823  " GL road vehicle ticks",
824  " GL ship ticks",
825  " GL aircraft ticks",
826  " GL landscape ticks",
827  " GL link graph delays",
828  "Drawing",
829  " Viewport drawing",
830  "Video output",
831  "Sound mixing",
832  };
833 
834  static const PerformanceElement rate_elements[] = { PFE_GAMELOOP, PFE_DRAWING, PFE_VIDEO };
835 
836  bool printed_anything = false;
837 
838  for (const PerformanceElement *e = rate_elements; e < rate_elements + lengthof(rate_elements); e++) {
839  auto &pf = _pf_data[*e];
840  if (pf.num_valid == 0) continue;
841  IConsolePrintF(TC_GREEN, "%s rate: %.2ffps (expected: %.2ffps)",
842  MEASUREMENT_NAMES[*e],
843  pf.GetRate(),
844  pf.expected_rate);
845  printed_anything = true;
846  }
847 
848  for (PerformanceElement e = PFE_FIRST; e < PFE_MAX; e++) {
849  auto &pf = _pf_data[e];
850  if (pf.num_valid == 0) continue;
851  IConsolePrintF(TC_LIGHT_BLUE, "%s times: %.2fms %.2fms %.2fms",
852  MEASUREMENT_NAMES[e],
853  pf.GetAverageDurationMilliseconds(count1),
854  pf.GetAverageDurationMilliseconds(count2),
855  pf.GetAverageDurationMilliseconds(count3));
856  printed_anything = true;
857  }
858 
859  if (!printed_anything) {
860  IConsoleWarning("No performance measurements have been taken yet");
861  }
862 }
Functions related to OTTD&#39;s strings.
Empty widget, place holder to reserve space in widget array.
Definition: widget_type.h:48
void IConsoleWarning(const char *string)
It is possible to print warnings to the console.
Definition: console.cpp:165
virtual void UpdateWidgetSize(int widget, Dimension *size, const Dimension &padding, Dimension *fill, Dimension *resize)
Update size and resize step of a widget in the window.
void GfxFillRect(int left, int top, int right, int bottom, int colour, FillRectMode mode)
Applies a certain FillRectMode-operation to a rectangle [left, right] x [top, bottom] on the screen...
Definition: gfx.cpp:112
PerformanceElement element
what element this window renders graph for
void BeginAccumulate(TimingMeasurement start_time)
Begin an accumulation of multiple measurements into a single value, from a given start time...
int vertical_scale
number of TIMESTAMP_PRECISION units vertically
Dimension graph_size
size of the main graph area (excluding axis labels)
High level window description.
Definition: window_gui.h:168
int horizontal_scale
number of half-second units horizontally
Framerate display; Window numbers:
Definition: window_type.h:688
const TimingMeasurement TIMESTAMP_PRECISION
Units a second is divided into in performance measurements
PerformanceMeasurer(PerformanceElement elem)
Begin a cycle of a measured element.
Horizontal container.
Definition: widget_type.h:75
TimingMeasurement timestamps[NUM_FRAMERATE_POINTS]
Start time of each cycle of the performance element, circular buffer.
bool Elapsed(uint delta)
Test if a timer has elapsed.
Definition: guitimer_func.h:57
Types for recording game performance data.
PerformanceElement
Elements of game performance that can be measured.
static void Paused(PerformanceElement elem)
Indicate that a cycle of "pause" where no processing occurs.
void UpdateScale()
Recalculate the graph scaling factors based on current recorded data.
End of enum, must be last.
void SetExpectedRate(double rate)
Set the rate of expected cycles per second of a performance element.
Close box (at top-left of a window)
Definition: widget_type.h:69
void AddPause(TimingMeasurement start_time)
Indicate a pause/expected discontinuity in processing the element.
static T max(const T a, const T b)
Returns the maximum of two values.
Definition: math_func.hpp:26
PerformanceAccumulator(PerformanceElement elem)
Begin measuring one block of the accumulating value.
Speed of drawing world and GUI.
CachedDecimal speed_gameloop
cached game loop speed factor
static void Reset(PerformanceElement elem)
Store the previous accumulator value and reset for a new cycle of accumulating measurements.
Pure simple text.
Definition: widget_type.h:58
Speed of gameloop processing.
virtual void SetStringParameters(int widget) const
Initialize string parameters for a widget.
static TimingMeasurement GetPerformanceTimer()
Return a timestamp with TIMESTAMP_PRECISION ticks per second precision.
Functions, definitions and such used only by the GUI.
Force the alignment, i.e. don&#39;t swap for RTL languages.
Definition: gfx_func.h:110
Partial widget specification to allow NWidgets to be written nested.
Definition: widget_type.h:910
Data structure for an opened window.
Definition: window_gui.h:271
static NWidgetPart SetPadding(uint8 top, uint8 right, uint8 bottom, uint8 left)
Widget part function for setting additional space around a widget.
Definition: widget_type.h:1046
int num_valid
Number of data points recorded, clamped to NUM_FRAMERATE_POINTS.
static const uint8 PC_DARK_GREY
Dark grey palette colour.
Definition: gfx_func.h:208
GUI Timers.
#define FONT_HEIGHT_SMALL
Height of characters in the small (FS_SMALL) font.
Definition: gfx_func.h:177
virtual void OnClick(Point pt, int widget, int click_count)
A click with the left mouse button has been made on the window.
#define FONT_HEIGHT_NORMAL
Height of characters in the normal (FS_NORMAL) font.
Definition: gfx_func.h:180
static NWidgetPart SetDataTip(uint32 data, StringID tip)
Widget part function for setting the data and tooltip.
Definition: widget_type.h:1014
Functions related to the gfx engine.
void CDECL IConsolePrintF(TextColour colour_code, const char *format,...)
Handle the printing of text entered into the console or redirected there by any other means...
Definition: console.cpp:132
Center both horizontally and vertically.
Definition: gfx_func.h:108
virtual void DrawWidget(const Rect &r, int widget) const
Draw the contents of a nested widget.
TextColour
Colour of the strings, see _string_colourmap in table/string_colours.h or docs/ottd-colourtext-palett...
Definition: gfx_type.h:247
Simple depressed panel.
Definition: widget_type.h:50
void ShowFramerateWindow()
Open the general framerate window.
static const uint MILLISECONDS_PER_TICK
The number of milliseconds per game tick.
Definition: gfx_type.h:306
~PerformanceMeasurer()
Finish a cycle of a measured element and store the measurement taken.
Console functions used outside of the console code.
void ConPrintFramerate()
Print performance statistics to game console.
PerformanceData(double expected_rate)
Initialize a data element with an expected collection rate.
void ShowFrametimeGraphWindow(PerformanceElement elem)
Open a graph window for a performance element.
virtual void DrawWidget(const Rect &r, int widget) const
Draw the contents of a nested widget.
const int NUM_FRAMERATE_POINTS
Number of data points to keep in buffer for each performance measurement.
static NWidgetPart NWidget(WidgetType tp, Colours col, int16 idx=-1)
Widget part function for starting a new &#39;real&#39; widget.
Definition: widget_type.h:1114
int DrawString(int left, int right, int top, const char *str, TextColour colour, StringAlignment align, bool underline, FontSize fontsize)
Draw string, possibly truncated to make it fit in its allocated space.
Definition: gfx.cpp:499
#define lengthof(x)
Return the length of an fixed size array.
Definition: depend.cpp:42
static T min(const T a, const T b)
Returns the minimum of two values.
Definition: math_func.hpp:42
Frame time graph; Window numbers:
Definition: window_type.h:694
GUITimer next_scale_update
interval for next scale update
uint32 StringID
Numeric value that represents a string, independent of the selected language.
Definition: strings_type.h:18
static const uint8 PC_BLACK
Black palette colour.
Definition: gfx_func.h:207
double expected_rate
Expected number of cycles per second when the system is running without slowdowns.
int prev_index
Last index written to in durations and timestamps.
uint64 TimingMeasurement
Type used to hold a performance timing measurement.
void AddAccumulate(TimingMeasurement duration)
Accumulate a period onto the current measurement.
Dimension GetStringBoundingBox(const char *str, FontSize start_fontsize)
Return the string dimension in pixels.
Definition: gfx.cpp:699
CachedDecimal rate_drawing
cached drawing frame rate
No window, redirects to WC_MAIN_WINDOW.
Definition: window_type.h:40
static const double GL_RATE
Game loop rate, cycles per second
static const uint8 PC_DARK_RED
Dark red palette colour.
Definition: gfx_func.h:213
void DrawElementTimesColumn(const Rect &r, StringID heading_str, const CachedDecimal *values) const
Render a column of formatted average durations.
Window caption (window title between closebox and stickybox)
Definition: widget_type.h:61
Speed of painting drawn video buffer.
virtual void UpdateWidgetSize(int widget, Dimension *size, const Dimension &padding, Dimension *fill, Dimension *resize)
Update size and resize step of a widget in the window.
Vertical container.
Definition: widget_type.h:77
static NWidgetPart EndContainer()
Widget part function for denoting the end of a container (horizontal, vertical, WWT_FRAME, WWT_INSET, or WWT_PANEL).
Definition: widget_type.h:999
~PerformanceAccumulator()
Finish and add one block of the accumulating value.
Coordinates of a point in 2D.
Index of the small font in the font tables.
Definition: gfx_type.h:205
Globally used console related types.
Types related to the framerate windows widgets.
Colour value is already a real palette colour index, not an index of a StringColour.
Definition: gfx_type.h:270
void Add(TimingMeasurement start_time, TimingMeasurement end_time)
Collect a complete measurement, given start and ending times for a processing block.
double GetAverageDurationMilliseconds(int count)
Get average cycle processing time over a number of data points.
Sticky box (at top-right of a window, after WWT_DEFSIZEBOX)
Definition: widget_type.h:66
int32 WindowNumber
Number to differentiate different windows of the same class.
Definition: window_type.h:707
Specification of a rectangle with absolute coordinates of all edges.
Right align the text (must be a single bit).
Definition: gfx_func.h:100
Left align the text.
Definition: gfx_func.h:98
int next_index
Next index to write to in durations and timestamps.
TimingMeasurement durations[NUM_FRAMERATE_POINTS]
Time spent processing each cycle of the performance element, circular buffer.
Find a place automatically.
Definition: window_gui.h:156
PerformanceData _pf_data[PFE_MAX]
Storage for all performance element measurements.
virtual void OnRealtimeTick(uint delta_ms)
Called periodically.
Dimensions (a width and height) of a rectangle in 2D.
This file contains all sprite-related enums and defines.
Shade box (at top-right of a window, between WWT_DEBUGBOX and WWT_DEFSIZEBOX)
Definition: widget_type.h:64
virtual void SetStringParameters(int widget) const
Initialize string parameters for a widget.
double GetRate()
Get current rate of a performance element, based on approximately the past one second of data...
TimingMeasurement acc_timestamp
Start time for current accumulation cycle.
static NWidgetPart SetPIP(uint8 pre, uint8 inter, uint8 post)
Widget part function for setting a pre/inter/post spaces.
Definition: widget_type.h:1076
TimingMeasurement acc_duration
Current accumulated duration.
CachedDecimal rate_gameloop
cached game loop tick rate
virtual void OnRealtimeTick(uint delta_ms)
Called periodically.
static void SetDParam(uint n, uint64 v)
Set a string parameter v at index n in the global string parameter array.
Definition: strings_func.h:201