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 "company_base.h"
21 #include "ai/ai_info.hpp"
22 #include "ai/ai_instance.hpp"
23 #include "game/game.hpp"
24 #include "game/game_instance.hpp"
25 #include "timer/timer.h"
26 #include "timer/timer_window.h"
28 #include "widgets/framerate_widget.h"
33 #include "safeguards.h"
35 static std::mutex _sound_perf_lock
;
36 static std::atomic
<bool> _sound_perf_pending
;
37 static std::vector
<TimingMeasurement
> _sound_perf_measurements
;
40 * Private declarations for performance measurement implementation
44 /** Number of data points to keep in buffer for each performance measurement */
45 const int NUM_FRAMERATE_POINTS
= 512;
46 /** %Units a second is divided into in performance measurements */
47 const TimingMeasurement TIMESTAMP_PRECISION
= 1000000;
49 struct PerformanceData
{
50 /** Duration value indicating the value is not valid should be considered a gap in measurements */
51 static const TimingMeasurement INVALID_DURATION
= UINT64_MAX
;
53 /** Time spent processing each cycle of the performance element, circular buffer */
54 TimingMeasurement durations
[NUM_FRAMERATE_POINTS
];
55 /** Start time of each cycle of the performance element, circular buffer */
56 TimingMeasurement timestamps
[NUM_FRAMERATE_POINTS
];
57 /** Expected number of cycles per second when the system is running without slowdowns */
59 /** Next index to write to in \c durations and \c timestamps */
61 /** Last index written to in \c durations and \c timestamps */
63 /** Number of data points recorded, clamped to \c NUM_FRAMERATE_POINTS */
66 /** Current accumulated duration */
67 TimingMeasurement acc_duration
;
68 /** Start time for current accumulation cycle */
69 TimingMeasurement acc_timestamp
;
72 * Initialize a data element with an expected collection rate
73 * @param expected_rate
74 * Expected number of cycles per second of the performance element. Use 1 if unknown or not relevant.
75 * The rate is used for highlighting slow-running elements in the GUI.
77 explicit PerformanceData(double expected_rate
) : expected_rate(expected_rate
), next_index(0), prev_index(0), num_valid(0) { }
79 /** Collect a complete measurement, given start and ending times for a processing block */
80 void Add(TimingMeasurement start_time
, TimingMeasurement end_time
)
82 this->durations
[this->next_index
] = end_time
- start_time
;
83 this->timestamps
[this->next_index
] = start_time
;
84 this->prev_index
= this->next_index
;
85 this->next_index
+= 1;
86 if (this->next_index
>= NUM_FRAMERATE_POINTS
) this->next_index
= 0;
87 this->num_valid
= std::min(NUM_FRAMERATE_POINTS
, this->num_valid
+ 1);
90 /** Begin an accumulation of multiple measurements into a single value, from a given start time */
91 void BeginAccumulate(TimingMeasurement start_time
)
93 this->timestamps
[this->next_index
] = this->acc_timestamp
;
94 this->durations
[this->next_index
] = this->acc_duration
;
95 this->prev_index
= this->next_index
;
96 this->next_index
+= 1;
97 if (this->next_index
>= NUM_FRAMERATE_POINTS
) this->next_index
= 0;
98 this->num_valid
= std::min(NUM_FRAMERATE_POINTS
, this->num_valid
+ 1);
100 this->acc_duration
= 0;
101 this->acc_timestamp
= start_time
;
104 /** Accumulate a period onto the current measurement */
105 void AddAccumulate(TimingMeasurement duration
)
107 this->acc_duration
+= duration
;
110 /** Indicate a pause/expected discontinuity in processing the element */
111 void AddPause(TimingMeasurement start_time
)
113 if (this->durations
[this->prev_index
] != INVALID_DURATION
) {
114 this->timestamps
[this->next_index
] = start_time
;
115 this->durations
[this->next_index
] = INVALID_DURATION
;
116 this->prev_index
= this->next_index
;
117 this->next_index
+= 1;
118 if (this->next_index
>= NUM_FRAMERATE_POINTS
) this->next_index
= 0;
119 this->num_valid
+= 1;
123 /** Get average cycle processing time over a number of data points */
124 double GetAverageDurationMilliseconds(int count
)
126 count
= std::min(count
, this->num_valid
);
128 int first_point
= this->prev_index
- count
;
129 if (first_point
< 0) first_point
+= NUM_FRAMERATE_POINTS
;
131 /* Sum durations, skipping invalid points */
133 for (int i
= first_point
; i
< first_point
+ count
; i
++) {
134 auto d
= this->durations
[i
% NUM_FRAMERATE_POINTS
];
135 if (d
!= INVALID_DURATION
) {
138 /* Don't count the invalid durations */
143 if (count
== 0) return 0; // avoid div by zero
144 return sumtime
* 1000 / count
/ TIMESTAMP_PRECISION
;
147 /** Get current rate of a performance element, based on approximately the past one second of data */
150 /* Start at last recorded point, end at latest when reaching the earliest recorded point */
151 int point
= this->prev_index
;
152 int last_point
= this->next_index
- this->num_valid
;
153 if (last_point
< 0) last_point
+= NUM_FRAMERATE_POINTS
;
155 /* Number of data points collected */
157 /* Time of previous data point */
158 TimingMeasurement last
= this->timestamps
[point
];
159 /* Total duration covered by collected points */
160 TimingMeasurement total
= 0;
162 /* We have nothing to compare the first point against */
164 if (point
< 0) point
= NUM_FRAMERATE_POINTS
- 1;
166 while (point
!= last_point
) {
167 /* Only record valid data points, but pretend the gaps in measurements aren't there */
168 if (this->durations
[point
] != INVALID_DURATION
) {
169 total
+= last
- this->timestamps
[point
];
172 last
= this->timestamps
[point
];
173 if (total
>= TIMESTAMP_PRECISION
) break; // end after 1 second has been collected
175 if (point
< 0) point
= NUM_FRAMERATE_POINTS
- 1;
178 if (total
== 0 || count
== 0) return 0;
179 return (double)count
* TIMESTAMP_PRECISION
/ total
;
183 /** %Game loop rate, cycles per second */
184 static const double GL_RATE
= 1000.0 / MILLISECONDS_PER_TICK
;
187 * Storage for all performance element measurements.
188 * Elements are initialized with the expected rate in recorded values per second.
191 PerformanceData _pf_data
[PFE_MAX
] = {
192 PerformanceData(GL_RATE
), // PFE_GAMELOOP
193 PerformanceData(1), // PFE_ACC_GL_ECONOMY
194 PerformanceData(1), // PFE_ACC_GL_TRAINS
195 PerformanceData(1), // PFE_ACC_GL_ROADVEHS
196 PerformanceData(1), // PFE_ACC_GL_SHIPS
197 PerformanceData(1), // PFE_ACC_GL_AIRCRAFT
198 PerformanceData(1), // PFE_GL_LANDSCAPE
199 PerformanceData(1), // PFE_GL_LINKGRAPH
200 PerformanceData(1000.0 / 30), // PFE_DRAWING
201 PerformanceData(1), // PFE_ACC_DRAWWORLD
202 PerformanceData(60.0), // PFE_VIDEO
203 PerformanceData(1000.0 * 8192 / 44100), // PFE_SOUND
204 PerformanceData(1), // PFE_ALLSCRIPTS
205 PerformanceData(1), // PFE_GAMESCRIPT
206 PerformanceData(1), // PFE_AI0 ...
220 PerformanceData(1), // PFE_AI14
227 * Return a timestamp with \c TIMESTAMP_PRECISION ticks per second precision.
228 * The basis of the timestamp is implementation defined, but the value should be steady,
229 * so differences can be taken to reliably measure intervals.
231 static TimingMeasurement
GetPerformanceTimer()
233 using namespace std::chrono
;
234 return (TimingMeasurement
)time_point_cast
<microseconds
>(high_resolution_clock::now()).time_since_epoch().count();
239 * Begin a cycle of a measured element.
240 * @param elem The element to be measured
242 PerformanceMeasurer::PerformanceMeasurer(PerformanceElement elem
)
244 assert(elem
< PFE_MAX
);
247 this->start_time
= GetPerformanceTimer();
250 /** Finish a cycle of a measured element and store the measurement taken. */
251 PerformanceMeasurer::~PerformanceMeasurer()
253 if (this->elem
== PFE_ALLSCRIPTS
) {
254 /* Hack to not record scripts total when no scripts are active */
255 bool any_active
= _pf_data
[PFE_GAMESCRIPT
].num_valid
> 0;
256 for (uint e
= PFE_AI0
; e
< PFE_MAX
; e
++) any_active
|= _pf_data
[e
].num_valid
> 0;
258 PerformanceMeasurer::SetInactive(PFE_ALLSCRIPTS
);
262 if (this->elem
== PFE_SOUND
) {
263 /* PFE_SOUND measurements are made from the mixer thread.
264 * _pf_data cannot be concurrently accessed from the mixer thread
265 * and the main thread, so store the measurement results in a
266 * mutex-protected queue which is drained by the main thread.
267 * See: ProcessPendingPerformanceMeasurements() */
268 TimingMeasurement end
= GetPerformanceTimer();
269 std::lock_guard
lk(_sound_perf_lock
);
270 if (_sound_perf_measurements
.size() >= NUM_FRAMERATE_POINTS
* 2) return;
271 _sound_perf_measurements
.push_back(this->start_time
);
272 _sound_perf_measurements
.push_back(end
);
273 _sound_perf_pending
.store(true, std::memory_order_release
);
276 _pf_data
[this->elem
].Add(this->start_time
, GetPerformanceTimer());
279 /** Set the rate of expected cycles per second of a performance element. */
280 void PerformanceMeasurer::SetExpectedRate(double rate
)
282 _pf_data
[this->elem
].expected_rate
= rate
;
285 /** Mark a performance element as not currently in use. */
286 /* static */ void PerformanceMeasurer::SetInactive(PerformanceElement elem
)
288 _pf_data
[elem
].num_valid
= 0;
289 _pf_data
[elem
].next_index
= 0;
290 _pf_data
[elem
].prev_index
= 0;
294 * Indicate that a cycle of "pause" where no processing occurs.
295 * @param elem The element not currently being processed
297 /* static */ void PerformanceMeasurer::Paused(PerformanceElement elem
)
299 PerformanceMeasurer::SetInactive(elem
);
300 _pf_data
[elem
].AddPause(GetPerformanceTimer());
305 * Begin measuring one block of the accumulating value.
306 * @param elem The element to be measured
308 PerformanceAccumulator::PerformanceAccumulator(PerformanceElement elem
)
310 assert(elem
< PFE_MAX
);
313 this->start_time
= GetPerformanceTimer();
316 /** Finish and add one block of the accumulating value. */
317 PerformanceAccumulator::~PerformanceAccumulator()
319 _pf_data
[this->elem
].AddAccumulate(GetPerformanceTimer() - this->start_time
);
323 * Store the previous accumulator value and reset for a new cycle of accumulating measurements.
324 * @note This function must be called once per frame, otherwise measurements are not collected.
325 * @param elem The element to begin a new measurement cycle of
327 void PerformanceAccumulator::Reset(PerformanceElement elem
)
329 _pf_data
[elem
].BeginAccumulate(GetPerformanceTimer());
333 void ShowFrametimeGraphWindow(PerformanceElement elem
);
336 static const PerformanceElement DISPLAY_ORDER_PFE
[PFE_MAX
] = {
368 static const char * GetAIName(int ai_index
)
370 if (!Company::IsValidAiID(ai_index
)) return "";
371 return Company::Get(ai_index
)->ai_info
->GetName().c_str();
374 /** @hideinitializer */
375 static constexpr NWidgetPart _framerate_window_widgets
[] = {
376 NWidget(NWID_HORIZONTAL
),
377 NWidget(WWT_CLOSEBOX
, COLOUR_GREY
),
378 NWidget(WWT_CAPTION
, COLOUR_GREY
, WID_FRW_CAPTION
), SetDataTip(STR_FRAMERATE_CAPTION
, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS
),
379 NWidget(WWT_SHADEBOX
, COLOUR_GREY
),
380 NWidget(WWT_STICKYBOX
, COLOUR_GREY
),
382 NWidget(WWT_PANEL
, COLOUR_GREY
),
383 NWidget(NWID_VERTICAL
), SetPadding(WidgetDimensions::unscaled
.frametext
), SetPIP(0, WidgetDimensions::unscaled
.vsep_normal
, 0),
384 NWidget(WWT_TEXT
, COLOUR_GREY
, WID_FRW_RATE_GAMELOOP
), SetDataTip(STR_FRAMERATE_RATE_GAMELOOP
, STR_FRAMERATE_RATE_GAMELOOP_TOOLTIP
), SetFill(1, 0), SetResize(1, 0),
385 NWidget(WWT_TEXT
, COLOUR_GREY
, WID_FRW_RATE_DRAWING
), SetDataTip(STR_FRAMERATE_RATE_BLITTER
, STR_FRAMERATE_RATE_BLITTER_TOOLTIP
), SetFill(1, 0), SetResize(1, 0),
386 NWidget(WWT_TEXT
, COLOUR_GREY
, WID_FRW_RATE_FACTOR
), SetDataTip(STR_FRAMERATE_SPEED_FACTOR
, STR_FRAMERATE_SPEED_FACTOR_TOOLTIP
), SetFill(1, 0), SetResize(1, 0),
389 NWidget(NWID_HORIZONTAL
),
390 NWidget(WWT_PANEL
, COLOUR_GREY
),
391 NWidget(NWID_VERTICAL
), SetPadding(WidgetDimensions::unscaled
.frametext
), SetPIP(0, WidgetDimensions::unscaled
.vsep_wide
, 0),
392 NWidget(NWID_HORIZONTAL
), SetPIP(0, WidgetDimensions::unscaled
.hsep_wide
, 0),
393 NWidget(WWT_EMPTY
, COLOUR_GREY
, WID_FRW_TIMES_NAMES
), SetScrollbar(WID_FRW_SCROLLBAR
),
394 NWidget(WWT_EMPTY
, COLOUR_GREY
, WID_FRW_TIMES_CURRENT
), SetScrollbar(WID_FRW_SCROLLBAR
),
395 NWidget(WWT_EMPTY
, COLOUR_GREY
, WID_FRW_TIMES_AVERAGE
), SetScrollbar(WID_FRW_SCROLLBAR
),
396 NWidget(NWID_SELECTION
, INVALID_COLOUR
, WID_FRW_SEL_MEMORY
),
397 NWidget(WWT_EMPTY
, COLOUR_GREY
, WID_FRW_ALLOCSIZE
), SetScrollbar(WID_FRW_SCROLLBAR
),
400 NWidget(WWT_TEXT
, COLOUR_GREY
, WID_FRW_INFO_DATA_POINTS
), SetDataTip(STR_FRAMERATE_DATA_POINTS
, 0x0), SetFill(1, 0), SetResize(1, 0),
403 NWidget(NWID_VERTICAL
),
404 NWidget(NWID_VSCROLLBAR
, COLOUR_GREY
, WID_FRW_SCROLLBAR
),
405 NWidget(WWT_RESIZEBOX
, COLOUR_GREY
),
410 struct FramerateWindow
: Window
{
416 struct CachedDecimal
{
420 inline void SetRate(double value
, double target
)
422 const double threshold_good
= target
* 0.95;
423 const double threshold_bad
= target
* 2 / 3;
424 this->value
= (uint32_t)(value
* 100);
425 this->strid
= (value
> threshold_good
) ? STR_FRAMERATE_FPS_GOOD
: (value
< threshold_bad
) ? STR_FRAMERATE_FPS_BAD
: STR_FRAMERATE_FPS_WARN
;
428 inline void SetTime(double value
, double target
)
430 const double threshold_good
= target
/ 3;
431 const double threshold_bad
= target
;
432 this->value
= (uint32_t)(value
* 100);
433 this->strid
= (value
< threshold_good
) ? STR_FRAMERATE_MS_GOOD
: (value
> threshold_bad
) ? STR_FRAMERATE_MS_BAD
: STR_FRAMERATE_MS_WARN
;
436 inline void InsertDParams(uint n
) const
438 SetDParam(n
, this->value
);
443 CachedDecimal rate_gameloop
; ///< cached game loop tick rate
444 CachedDecimal rate_drawing
; ///< cached drawing frame rate
445 CachedDecimal speed_gameloop
; ///< cached game loop speed factor
446 CachedDecimal times_shortterm
[PFE_MAX
]; ///< cached short term average times
447 CachedDecimal times_longterm
[PFE_MAX
]; ///< cached long term average times
449 static constexpr int MIN_ELEMENTS
= 5; ///< smallest number of elements to display
451 FramerateWindow(WindowDesc
&desc
, WindowNumber number
) : Window(desc
)
453 this->InitNested(number
);
454 this->small
= this->IsShaded();
455 this->showing_memory
= true;
457 this->num_displayed
= this->num_active
;
459 /* Window is always initialised to MIN_ELEMENTS height, resize to contain num_displayed */
460 ResizeWindow(this, 0, (std::max(MIN_ELEMENTS
, this->num_displayed
) - MIN_ELEMENTS
) * GetCharacterHeight(FS_NORMAL
));
463 void OnRealtimeTick([[maybe_unused
]] uint delta_ms
) override
465 /* Check if the shaded state has changed, switch caption text if it has */
466 if (this->small
!= this->IsShaded()) {
467 this->small
= this->IsShaded();
468 this->GetWidget
<NWidgetLeaf
>(WID_FRW_CAPTION
)->SetDataTip(this->small
? STR_FRAMERATE_CAPTION_SMALL
: STR_FRAMERATE_CAPTION
, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS
);
474 /** Update the window on a regular interval. */
475 IntervalTimer
<TimerWindow
> update_interval
= {std::chrono::milliseconds(100), [this](auto) {
482 double gl_rate
= _pf_data
[PFE_GAMELOOP
].GetRate();
483 bool have_script
= false;
484 this->rate_gameloop
.SetRate(gl_rate
, _pf_data
[PFE_GAMELOOP
].expected_rate
);
485 this->speed_gameloop
.SetRate(gl_rate
/ _pf_data
[PFE_GAMELOOP
].expected_rate
, 1.0);
486 if (this->small
) return; // in small mode, this is everything needed
488 this->rate_drawing
.SetRate(_pf_data
[PFE_DRAWING
].GetRate(), _settings_client
.gui
.refresh_rate
);
491 for (PerformanceElement e
= PFE_FIRST
; e
< PFE_MAX
; e
++) {
492 this->times_shortterm
[e
].SetTime(_pf_data
[e
].GetAverageDurationMilliseconds(8), MILLISECONDS_PER_TICK
);
493 this->times_longterm
[e
].SetTime(_pf_data
[e
].GetAverageDurationMilliseconds(NUM_FRAMERATE_POINTS
), MILLISECONDS_PER_TICK
);
494 if (_pf_data
[e
].num_valid
> 0) {
496 if (e
== PFE_GAMESCRIPT
|| e
>= PFE_AI0
) have_script
= true;
500 if (this->showing_memory
!= have_script
) {
501 NWidgetStacked
*plane
= this->GetWidget
<NWidgetStacked
>(WID_FRW_SEL_MEMORY
);
502 plane
->SetDisplayedPlane(have_script
? 0 : SZSP_VERTICAL
);
503 this->showing_memory
= have_script
;
506 if (new_active
!= this->num_active
) {
507 this->num_active
= new_active
;
508 Scrollbar
*sb
= this->GetScrollbar(WID_FRW_SCROLLBAR
);
509 sb
->SetCount(this->num_active
);
510 sb
->SetCapacity(std::min(this->num_displayed
, this->num_active
));
515 void SetStringParameters(WidgetID widget
) const override
518 case WID_FRW_CAPTION
:
519 /* When the window is shaded, the caption shows game loop rate and speed factor */
520 if (!this->small
) break;
521 SetDParam(0, this->rate_gameloop
.strid
);
522 this->rate_gameloop
.InsertDParams(1);
523 this->speed_gameloop
.InsertDParams(3);
526 case WID_FRW_RATE_GAMELOOP
:
527 SetDParam(0, this->rate_gameloop
.strid
);
528 this->rate_gameloop
.InsertDParams(1);
530 case WID_FRW_RATE_DRAWING
:
531 SetDParam(0, this->rate_drawing
.strid
);
532 this->rate_drawing
.InsertDParams(1);
534 case WID_FRW_RATE_FACTOR
:
535 this->speed_gameloop
.InsertDParams(0);
537 case WID_FRW_INFO_DATA_POINTS
:
538 SetDParam(0, NUM_FRAMERATE_POINTS
);
543 void UpdateWidgetSize(WidgetID widget
, Dimension
&size
, [[maybe_unused
]] const Dimension
&padding
, [[maybe_unused
]] Dimension
&fill
, [[maybe_unused
]] Dimension
&resize
) override
546 case WID_FRW_RATE_GAMELOOP
:
547 SetDParam(0, STR_FRAMERATE_FPS_GOOD
);
548 SetDParamMaxDigits(1, 6);
550 size
= GetStringBoundingBox(STR_FRAMERATE_RATE_GAMELOOP
);
552 case WID_FRW_RATE_DRAWING
:
553 SetDParam(0, STR_FRAMERATE_FPS_GOOD
);
554 SetDParamMaxDigits(1, 6);
556 size
= GetStringBoundingBox(STR_FRAMERATE_RATE_BLITTER
);
558 case WID_FRW_RATE_FACTOR
:
559 SetDParamMaxDigits(0, 6);
561 size
= GetStringBoundingBox(STR_FRAMERATE_SPEED_FACTOR
);
564 case WID_FRW_TIMES_NAMES
: {
566 size
.height
= GetCharacterHeight(FS_NORMAL
) + WidgetDimensions::scaled
.vsep_normal
+ MIN_ELEMENTS
* GetCharacterHeight(FS_NORMAL
);
568 resize
.height
= GetCharacterHeight(FS_NORMAL
);
569 for (PerformanceElement e
: DISPLAY_ORDER_PFE
) {
570 if (_pf_data
[e
].num_valid
== 0) continue;
573 line_size
= GetStringBoundingBox(STR_FRAMERATE_GAMELOOP
+ e
);
575 SetDParam(0, e
- PFE_AI0
+ 1);
576 SetDParamStr(1, GetAIName(e
- PFE_AI0
));
577 line_size
= GetStringBoundingBox(STR_FRAMERATE_AI
);
579 size
.width
= std::max(size
.width
, line_size
.width
);
584 case WID_FRW_TIMES_CURRENT
:
585 case WID_FRW_TIMES_AVERAGE
:
586 case WID_FRW_ALLOCSIZE
: {
587 size
= GetStringBoundingBox(STR_FRAMERATE_CURRENT
+ (widget
- WID_FRW_TIMES_CURRENT
));
588 SetDParamMaxDigits(0, 6);
590 Dimension item_size
= GetStringBoundingBox(STR_FRAMERATE_MS_GOOD
);
591 size
.width
= std::max(size
.width
, item_size
.width
);
592 size
.height
+= GetCharacterHeight(FS_NORMAL
) * MIN_ELEMENTS
+ WidgetDimensions::scaled
.vsep_normal
;
594 resize
.height
= GetCharacterHeight(FS_NORMAL
);
600 /** Render a column of formatted average durations */
601 void DrawElementTimesColumn(const Rect
&r
, StringID heading_str
, const CachedDecimal
*values
) const
603 const Scrollbar
*sb
= this->GetScrollbar(WID_FRW_SCROLLBAR
);
604 int32_t skip
= sb
->GetPosition();
605 int drawable
= this->num_displayed
;
607 DrawString(r
.left
, r
.right
, y
, heading_str
, TC_FROMSTRING
, SA_CENTER
, true);
608 y
+= GetCharacterHeight(FS_NORMAL
) + WidgetDimensions::scaled
.vsep_normal
;
609 for (PerformanceElement e
: DISPLAY_ORDER_PFE
) {
610 if (_pf_data
[e
].num_valid
== 0) continue;
614 values
[e
].InsertDParams(0);
615 DrawString(r
.left
, r
.right
, y
, values
[e
].strid
, TC_FROMSTRING
, SA_RIGHT
);
616 y
+= GetCharacterHeight(FS_NORMAL
);
618 if (drawable
== 0) break;
623 void DrawElementAllocationsColumn(const Rect
&r
) const
625 const Scrollbar
*sb
= this->GetScrollbar(WID_FRW_SCROLLBAR
);
626 int32_t skip
= sb
->GetPosition();
627 int drawable
= this->num_displayed
;
629 DrawString(r
.left
, r
.right
, y
, STR_FRAMERATE_MEMORYUSE
, TC_FROMSTRING
, SA_CENTER
, true);
630 y
+= GetCharacterHeight(FS_NORMAL
) + WidgetDimensions::scaled
.vsep_normal
;
631 for (PerformanceElement e
: DISPLAY_ORDER_PFE
) {
632 if (_pf_data
[e
].num_valid
== 0) continue;
635 } else if (e
== PFE_GAMESCRIPT
|| e
>= PFE_AI0
) {
636 if (e
== PFE_GAMESCRIPT
) {
637 SetDParam(0, Game::GetInstance()->GetAllocatedMemory());
639 SetDParam(0, Company::Get(e
- PFE_AI0
)->ai_instance
->GetAllocatedMemory());
641 DrawString(r
.left
, r
.right
, y
, STR_FRAMERATE_BYTES_GOOD
, TC_FROMSTRING
, SA_RIGHT
);
642 y
+= GetCharacterHeight(FS_NORMAL
);
644 if (drawable
== 0) break;
646 /* skip non-script */
647 y
+= GetCharacterHeight(FS_NORMAL
);
649 if (drawable
== 0) break;
654 void DrawWidget(const Rect
&r
, WidgetID widget
) const override
657 case WID_FRW_TIMES_NAMES
: {
658 /* Render a column of titles for performance element names */
659 const Scrollbar
*sb
= this->GetScrollbar(WID_FRW_SCROLLBAR
);
660 int32_t skip
= sb
->GetPosition();
661 int drawable
= this->num_displayed
;
662 int y
= r
.top
+ GetCharacterHeight(FS_NORMAL
) + WidgetDimensions::scaled
.vsep_normal
; // first line contains headings in the value columns
663 for (PerformanceElement e
: DISPLAY_ORDER_PFE
) {
664 if (_pf_data
[e
].num_valid
== 0) continue;
669 DrawString(r
.left
, r
.right
, y
, STR_FRAMERATE_GAMELOOP
+ e
, TC_FROMSTRING
, SA_LEFT
);
671 SetDParam(0, e
- PFE_AI0
+ 1);
672 SetDParamStr(1, GetAIName(e
- PFE_AI0
));
673 DrawString(r
.left
, r
.right
, y
, STR_FRAMERATE_AI
, TC_FROMSTRING
, SA_LEFT
);
675 y
+= GetCharacterHeight(FS_NORMAL
);
677 if (drawable
== 0) break;
682 case WID_FRW_TIMES_CURRENT
:
683 /* Render short-term average values */
684 DrawElementTimesColumn(r
, STR_FRAMERATE_CURRENT
, this->times_shortterm
);
686 case WID_FRW_TIMES_AVERAGE
:
687 /* Render averages of all recorded values */
688 DrawElementTimesColumn(r
, STR_FRAMERATE_AVERAGE
, this->times_longterm
);
690 case WID_FRW_ALLOCSIZE
:
691 DrawElementAllocationsColumn(r
);
696 void OnClick([[maybe_unused
]] Point pt
, WidgetID widget
, [[maybe_unused
]] int click_count
) override
699 case WID_FRW_TIMES_NAMES
:
700 case WID_FRW_TIMES_CURRENT
:
701 case WID_FRW_TIMES_AVERAGE
: {
702 /* Open time graph windows when clicking detail measurement lines */
703 const Scrollbar
*sb
= this->GetScrollbar(WID_FRW_SCROLLBAR
);
704 int32_t line
= sb
->GetScrolledRowFromWidget(pt
.y
, this, widget
, WidgetDimensions::scaled
.vsep_normal
+ GetCharacterHeight(FS_NORMAL
));
705 if (line
!= INT32_MAX
) {
707 /* Find the visible line that was clicked */
708 for (PerformanceElement e
: DISPLAY_ORDER_PFE
) {
709 if (_pf_data
[e
].num_valid
> 0) line
--;
711 ShowFrametimeGraphWindow(e
);
721 void OnResize() override
723 auto *wid
= this->GetWidget
<NWidgetResizeBase
>(WID_FRW_TIMES_NAMES
);
724 this->num_displayed
= (wid
->current_y
- wid
->min_y
- WidgetDimensions::scaled
.vsep_normal
) / GetCharacterHeight(FS_NORMAL
) - 1; // subtract 1 for headings
725 this->GetScrollbar(WID_FRW_SCROLLBAR
)->SetCapacity(this->num_displayed
);
729 static WindowDesc
_framerate_display_desc(
730 WDP_AUTO
, "framerate_display", 0, 0,
731 WC_FRAMERATE_DISPLAY
, WC_NONE
,
733 _framerate_window_widgets
737 /** @hideinitializer */
738 static constexpr NWidgetPart _frametime_graph_window_widgets
[] = {
739 NWidget(NWID_HORIZONTAL
),
740 NWidget(WWT_CLOSEBOX
, COLOUR_GREY
),
741 NWidget(WWT_CAPTION
, COLOUR_GREY
, WID_FGW_CAPTION
), SetDataTip(STR_JUST_STRING2
, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS
), SetTextStyle(TC_WHITE
),
742 NWidget(WWT_STICKYBOX
, COLOUR_GREY
),
744 NWidget(WWT_PANEL
, COLOUR_GREY
),
745 NWidget(NWID_VERTICAL
), SetPadding(6),
746 NWidget(WWT_EMPTY
, COLOUR_GREY
, WID_FGW_GRAPH
),
751 struct FrametimeGraphWindow
: Window
{
752 int vertical_scale
; ///< number of TIMESTAMP_PRECISION units vertically
753 int horizontal_scale
; ///< number of half-second units horizontally
755 PerformanceElement element
; ///< what element this window renders graph for
756 Dimension graph_size
; ///< size of the main graph area (excluding axis labels)
758 FrametimeGraphWindow(WindowDesc
&desc
, WindowNumber number
) : Window(desc
)
760 this->element
= (PerformanceElement
)number
;
761 this->horizontal_scale
= 4;
762 this->vertical_scale
= TIMESTAMP_PRECISION
/ 10;
764 this->InitNested(number
);
768 void SetStringParameters(WidgetID widget
) const override
771 case WID_FGW_CAPTION
:
772 if (this->element
< PFE_AI0
) {
773 SetDParam(0, STR_FRAMETIME_CAPTION_GAMELOOP
+ this->element
);
775 SetDParam(0, STR_FRAMETIME_CAPTION_AI
);
776 SetDParam(1, this->element
- PFE_AI0
+ 1);
777 SetDParamStr(2, GetAIName(this->element
- PFE_AI0
));
783 void UpdateWidgetSize(WidgetID widget
, Dimension
&size
, [[maybe_unused
]] const Dimension
&padding
, [[maybe_unused
]] Dimension
&fill
, [[maybe_unused
]] Dimension
&resize
) override
785 if (widget
== WID_FGW_GRAPH
) {
787 Dimension size_ms_label
= GetStringBoundingBox(STR_FRAMERATE_GRAPH_MILLISECONDS
);
789 Dimension size_s_label
= GetStringBoundingBox(STR_FRAMERATE_GRAPH_SECONDS
);
791 /* Size graph in height to fit at least 10 vertical labels with space between, or at least 100 pixels */
792 graph_size
.height
= std::max(100u, 10 * (size_ms_label
.height
+ 1));
793 /* Always 2:1 graph area */
794 graph_size
.width
= 2 * graph_size
.height
;
797 size
.width
+= size_ms_label
.width
+ 2;
798 size
.height
+= size_s_label
.height
+ 2;
802 void SelectHorizontalScale(TimingMeasurement range
)
804 /* 60 Hz graphical drawing results in a value of approximately TIMESTAMP_PRECISION,
805 * this lands exactly on the scale = 2 vs scale = 4 boundary.
806 * To avoid excessive switching of the horizontal scale, bias these performance
807 * categories away from this scale boundary. */
808 if (this->element
== PFE_DRAWING
|| this->element
== PFE_DRAWWORLD
) range
+= (range
/ 2);
810 /* Determine horizontal scale based on period covered by 60 points
811 * (slightly less than 2 seconds at full game speed) */
812 struct ScaleDef
{ TimingMeasurement range
; int scale
; };
813 static const std::initializer_list
<ScaleDef
> hscales
= {
814 { TIMESTAMP_PRECISION
* 120, 60 },
815 { TIMESTAMP_PRECISION
* 10, 20 },
816 { TIMESTAMP_PRECISION
* 5, 10 },
817 { TIMESTAMP_PRECISION
* 3, 4 },
818 { TIMESTAMP_PRECISION
* 1, 2 },
820 for (const auto &sc
: hscales
) {
821 if (range
< sc
.range
) this->horizontal_scale
= sc
.scale
;
825 void SelectVerticalScale(TimingMeasurement range
)
827 /* Determine vertical scale based on peak value (within the horizontal scale + a bit) */
828 static const std::initializer_list
<TimingMeasurement
> vscales
= {
829 TIMESTAMP_PRECISION
* 100,
830 TIMESTAMP_PRECISION
* 10,
831 TIMESTAMP_PRECISION
* 5,
833 TIMESTAMP_PRECISION
/ 2,
834 TIMESTAMP_PRECISION
/ 5,
835 TIMESTAMP_PRECISION
/ 10,
836 TIMESTAMP_PRECISION
/ 50,
837 TIMESTAMP_PRECISION
/ 200,
839 for (const auto &sc
: vscales
) {
840 if (range
< sc
) this->vertical_scale
= (int)sc
;
844 /** Recalculate the graph scaling factors based on current recorded data */
847 const TimingMeasurement
*durations
= _pf_data
[this->element
].durations
;
848 const TimingMeasurement
*timestamps
= _pf_data
[this->element
].timestamps
;
849 int num_valid
= _pf_data
[this->element
].num_valid
;
850 int point
= _pf_data
[this->element
].prev_index
;
852 TimingMeasurement lastts
= timestamps
[point
];
853 TimingMeasurement time_sum
= 0;
854 TimingMeasurement peak_value
= 0;
857 /* Sensible default for when too few measurements are available */
858 this->horizontal_scale
= 4;
860 for (int i
= 1; i
< num_valid
; i
++) {
862 if (point
< 0) point
= NUM_FRAMERATE_POINTS
- 1;
864 TimingMeasurement value
= durations
[point
];
865 if (value
== PerformanceData::INVALID_DURATION
) {
866 /* Skip gaps in data by pretending time is continuous across them */
867 lastts
= timestamps
[point
];
870 if (value
> peak_value
) peak_value
= value
;
873 /* Accumulate period of time covered by data */
874 time_sum
+= lastts
- timestamps
[point
];
875 lastts
= timestamps
[point
];
877 /* Enough data to select a range and get decent data density */
878 if (count
== 60) this->SelectHorizontalScale(time_sum
);
880 /* End when enough points have been collected and the horizontal scale has been exceeded */
881 if (count
>= 60 && time_sum
>= (this->horizontal_scale
+ 2) * TIMESTAMP_PRECISION
/ 2) break;
884 this->SelectVerticalScale(peak_value
);
887 /** Update the scaling on a regular interval. */
888 IntervalTimer
<TimerWindow
> update_interval
= {std::chrono::milliseconds(500), [this](auto) {
892 void OnRealtimeTick([[maybe_unused
]] uint delta_ms
) override
897 /** Scale and interpolate a value from a source range into a destination range */
899 static inline T
Scinterlate(T dst_min
, T dst_max
, T src_min
, T src_max
, T value
)
901 T dst_diff
= dst_max
- dst_min
;
902 T src_diff
= src_max
- src_min
;
903 return (value
- src_min
) * dst_diff
/ src_diff
+ dst_min
;
906 void DrawWidget(const Rect
&r
, WidgetID widget
) const override
908 if (widget
== WID_FGW_GRAPH
) {
909 const TimingMeasurement
*durations
= _pf_data
[this->element
].durations
;
910 const TimingMeasurement
*timestamps
= _pf_data
[this->element
].timestamps
;
911 int point
= _pf_data
[this->element
].prev_index
;
913 const int x_zero
= r
.right
- (int)this->graph_size
.width
;
914 const int x_max
= r
.right
;
915 const int y_zero
= r
.top
+ (int)this->graph_size
.height
;
916 const int y_max
= r
.top
;
917 const int c_grid
= PC_DARK_GREY
;
918 const int c_lines
= PC_BLACK
;
919 const int c_peak
= PC_DARK_RED
;
921 const TimingMeasurement draw_horz_scale
= (TimingMeasurement
)this->horizontal_scale
* TIMESTAMP_PRECISION
/ 2;
922 const TimingMeasurement draw_vert_scale
= (TimingMeasurement
)this->vertical_scale
;
924 /* Number of \c horizontal_scale units in each horizontal division */
925 const uint horz_div_scl
= (this->horizontal_scale
<= 20) ? 1 : 10;
926 /* Number of divisions of the horizontal axis */
927 const uint horz_divisions
= this->horizontal_scale
/ horz_div_scl
;
928 /* Number of divisions of the vertical axis */
929 const uint vert_divisions
= 10;
931 /* Draw division lines and labels for the vertical axis */
932 for (uint division
= 0; division
< vert_divisions
; division
++) {
933 int y
= Scinterlate(y_zero
, y_max
, 0, (int)vert_divisions
, (int)division
);
934 GfxDrawLine(x_zero
, y
, x_max
, y
, c_grid
);
935 if (division
% 2 == 0) {
936 if ((TimingMeasurement
)this->vertical_scale
> TIMESTAMP_PRECISION
) {
937 SetDParam(0, this->vertical_scale
* division
/ 10 / TIMESTAMP_PRECISION
);
938 DrawString(r
.left
, x_zero
- 2, y
- GetCharacterHeight(FS_SMALL
), STR_FRAMERATE_GRAPH_SECONDS
, TC_GREY
, SA_RIGHT
| SA_FORCE
, false, FS_SMALL
);
940 SetDParam(0, this->vertical_scale
* division
/ 10 * 1000 / TIMESTAMP_PRECISION
);
941 DrawString(r
.left
, x_zero
- 2, y
- GetCharacterHeight(FS_SMALL
), STR_FRAMERATE_GRAPH_MILLISECONDS
, TC_GREY
, SA_RIGHT
| SA_FORCE
, false, FS_SMALL
);
945 /* Draw division lines and labels for the horizontal axis */
946 for (uint division
= horz_divisions
; division
> 0; division
--) {
947 int x
= Scinterlate(x_zero
, x_max
, 0, (int)horz_divisions
, (int)horz_divisions
- (int)division
);
948 GfxDrawLine(x
, y_max
, x
, y_zero
, c_grid
);
949 if (division
% 2 == 0) {
950 SetDParam(0, division
* horz_div_scl
/ 2);
951 DrawString(x
, x_max
, y_zero
+ 2, STR_FRAMERATE_GRAPH_SECONDS
, TC_GREY
, SA_LEFT
| SA_FORCE
, false, FS_SMALL
);
955 /* Position of last rendered data point */
958 (int)Scinterlate
<int64_t>(y_zero
, y_max
, 0, this->vertical_scale
, durations
[point
])
960 /* Timestamp of last rendered data point */
961 TimingMeasurement lastts
= timestamps
[point
];
963 TimingMeasurement peak_value
= 0;
964 Point peak_point
= { 0, 0 };
965 TimingMeasurement value_sum
= 0;
966 TimingMeasurement time_sum
= 0;
967 int points_drawn
= 0;
969 for (int i
= 1; i
< NUM_FRAMERATE_POINTS
; i
++) {
971 if (point
< 0) point
= NUM_FRAMERATE_POINTS
- 1;
973 TimingMeasurement value
= durations
[point
];
974 if (value
== PerformanceData::INVALID_DURATION
) {
975 /* Skip gaps in measurements, pretend the data points on each side are continuous */
976 lastts
= timestamps
[point
];
980 /* Use total time period covered for value along horizontal axis */
981 time_sum
+= lastts
- timestamps
[point
];
982 lastts
= timestamps
[point
];
983 /* Stop if past the width of the graph */
984 if (time_sum
> draw_horz_scale
) break;
986 /* Draw line from previous point to new point */
988 (int)Scinterlate
<int64_t>(x_zero
, x_max
, 0, (int64_t)draw_horz_scale
, (int64_t)draw_horz_scale
- (int64_t)time_sum
),
989 (int)Scinterlate
<int64_t>(y_zero
, y_max
, 0, (int64_t)draw_vert_scale
, (int64_t)value
)
991 if (newpoint
.x
> lastpoint
.x
) continue; // don't draw backwards
992 GfxDrawLine(lastpoint
.x
, lastpoint
.y
, newpoint
.x
, newpoint
.y
, c_lines
);
993 lastpoint
= newpoint
;
995 /* Record peak and average value across graphed data */
998 if (value
> peak_value
) {
1000 peak_point
= newpoint
;
1004 /* If the peak value is significantly larger than the average, mark and label it */
1005 if (points_drawn
> 0 && peak_value
> TIMESTAMP_PRECISION
/ 100 && 2 * peak_value
> 3 * value_sum
/ points_drawn
) {
1006 TextColour tc_peak
= (TextColour
)(TC_IS_PALETTE_COLOUR
| c_peak
);
1007 GfxFillRect(peak_point
.x
- 1, peak_point
.y
- 1, peak_point
.x
+ 1, peak_point
.y
+ 1, c_peak
);
1008 SetDParam(0, peak_value
* 1000 / TIMESTAMP_PRECISION
);
1009 int label_y
= std::max(y_max
, peak_point
.y
- GetCharacterHeight(FS_SMALL
));
1010 if (peak_point
.x
- x_zero
> (int)this->graph_size
.width
/ 2) {
1011 DrawString(x_zero
, peak_point
.x
- 2, label_y
, STR_FRAMERATE_GRAPH_MILLISECONDS
, tc_peak
, SA_RIGHT
| SA_FORCE
, false, FS_SMALL
);
1013 DrawString(peak_point
.x
+ 2, x_max
, label_y
, STR_FRAMERATE_GRAPH_MILLISECONDS
, tc_peak
, SA_LEFT
| SA_FORCE
, false, FS_SMALL
);
1020 static WindowDesc
_frametime_graph_window_desc(
1021 WDP_AUTO
, "frametime_graph", 140, 90,
1022 WC_FRAMETIME_GRAPH
, WC_NONE
,
1024 _frametime_graph_window_widgets
1029 /** Open the general framerate window */
1030 void ShowFramerateWindow()
1032 AllocateWindowDescFront
<FramerateWindow
>(_framerate_display_desc
, 0);
1035 /** Open a graph window for a performance element */
1036 void ShowFrametimeGraphWindow(PerformanceElement elem
)
1038 if (elem
< PFE_FIRST
|| elem
>= PFE_MAX
) return; // maybe warn?
1039 AllocateWindowDescFront
<FrametimeGraphWindow
>(_frametime_graph_window_desc
, elem
, true);
1042 /** Print performance statistics to game console */
1043 void ConPrintFramerate()
1045 const int count1
= NUM_FRAMERATE_POINTS
/ 8;
1046 const int count2
= NUM_FRAMERATE_POINTS
/ 4;
1047 const int count3
= NUM_FRAMERATE_POINTS
/ 1;
1049 IConsolePrint(TC_SILVER
, "Based on num. data points: {} {} {}", count1
, count2
, count3
);
1051 static const std::array
<std::string_view
, PFE_MAX
> MEASUREMENT_NAMES
= {
1053 " GL station ticks",
1055 " GL road vehicle ticks",
1057 " GL aircraft ticks",
1058 " GL landscape ticks",
1059 " GL link graph delays",
1061 " Viewport drawing",
1064 "AI/GS scripts total",
1067 std::string ai_name_buf
;
1069 bool printed_anything
= false;
1071 for (const auto &e
: { PFE_GAMELOOP
, PFE_DRAWING
, PFE_VIDEO
}) {
1072 auto &pf
= _pf_data
[e
];
1073 if (pf
.num_valid
== 0) continue;
1074 IConsolePrint(TC_GREEN
, "{} rate: {:.2f}fps (expected: {:.2f}fps)",
1075 MEASUREMENT_NAMES
[e
],
1078 printed_anything
= true;
1081 for (PerformanceElement e
= PFE_FIRST
; e
< PFE_MAX
; e
++) {
1082 auto &pf
= _pf_data
[e
];
1083 if (pf
.num_valid
== 0) continue;
1084 std::string_view name
;
1086 name
= MEASUREMENT_NAMES
[e
];
1088 ai_name_buf
= fmt::format("AI {} {}", e
- PFE_AI0
+ 1, GetAIName(e
- PFE_AI0
));
1091 IConsolePrint(TC_LIGHT_BLUE
, "{} times: {:.2f}ms {:.2f}ms {:.2f}ms",
1093 pf
.GetAverageDurationMilliseconds(count1
),
1094 pf
.GetAverageDurationMilliseconds(count2
),
1095 pf
.GetAverageDurationMilliseconds(count3
));
1096 printed_anything
= true;
1099 if (!printed_anything
) {
1100 IConsolePrint(CC_ERROR
, "No performance measurements have been taken yet.");
1105 * This drains the PFE_SOUND measurement data queue into _pf_data.
1106 * PFE_SOUND measurements are made by the mixer thread and so cannot be stored
1107 * into _pf_data directly, because this would not be thread safe and would violate
1108 * the invariants of the FPS and frame graph windows.
1109 * @see PerformanceMeasurement::~PerformanceMeasurement()
1111 void ProcessPendingPerformanceMeasurements()
1113 if (_sound_perf_pending
.load(std::memory_order_acquire
)) {
1114 std::lock_guard
lk(_sound_perf_lock
);
1115 for (size_t i
= 0; i
< _sound_perf_measurements
.size(); i
+= 2) {
1116 _pf_data
[PFE_SOUND
].Add(_sound_perf_measurements
[i
], _sound_perf_measurements
[i
+ 1]);
1118 _sound_perf_measurements
.clear();
1119 _sound_perf_pending
.store(false, std::memory_order_relaxed
);