Update: Translations from eints
[openttd-github.git] / src / framerate_gui.cpp
blob82920015942bd053f051bfacae95692ac46d3abf
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 "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"
30 #include <atomic>
31 #include <mutex>
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;
39 /**
40 * Private declarations for performance measurement implementation
42 namespace {
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 */
58 double expected_rate;
59 /** Next index to write to in \c durations and \c timestamps */
60 int next_index;
61 /** Last index written to in \c durations and \c timestamps */
62 int prev_index;
63 /** Number of data points recorded, clamped to \c NUM_FRAMERATE_POINTS */
64 int num_valid;
66 /** Current accumulated duration */
67 TimingMeasurement acc_duration;
68 /** Start time for current accumulation cycle */
69 TimingMeasurement acc_timestamp;
71 /**
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 */
132 double sumtime = 0;
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) {
136 sumtime += d;
137 } else {
138 /* Don't count the invalid durations */
139 count--;
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 */
148 double GetRate()
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 */
156 int count = 0;
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 */
163 point--;
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];
170 count++;
172 last = this->timestamps[point];
173 if (total >= TIMESTAMP_PRECISION) break; // end after 1 second has been collected
174 point--;
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.
189 * @hideinitializer
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 ...
207 PerformanceData(1),
208 PerformanceData(1),
209 PerformanceData(1),
210 PerformanceData(1),
211 PerformanceData(1),
212 PerformanceData(1),
213 PerformanceData(1),
214 PerformanceData(1),
215 PerformanceData(1),
216 PerformanceData(1),
217 PerformanceData(1),
218 PerformanceData(1),
219 PerformanceData(1),
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);
246 this->elem = elem;
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;
257 if (!any_active) {
258 PerformanceMeasurer::SetInactive(PFE_ALLSCRIPTS);
259 return;
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);
274 return;
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);
312 this->elem = elem;
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] = {
337 PFE_GAMELOOP,
338 PFE_GL_ECONOMY,
339 PFE_GL_TRAINS,
340 PFE_GL_ROADVEHS,
341 PFE_GL_SHIPS,
342 PFE_GL_AIRCRAFT,
343 PFE_GL_LANDSCAPE,
344 PFE_ALLSCRIPTS,
345 PFE_GAMESCRIPT,
346 PFE_AI0,
347 PFE_AI1,
348 PFE_AI2,
349 PFE_AI3,
350 PFE_AI4,
351 PFE_AI5,
352 PFE_AI6,
353 PFE_AI7,
354 PFE_AI8,
355 PFE_AI9,
356 PFE_AI10,
357 PFE_AI11,
358 PFE_AI12,
359 PFE_AI13,
360 PFE_AI14,
361 PFE_GL_LINKGRAPH,
362 PFE_DRAWING,
363 PFE_DRAWWORLD,
364 PFE_VIDEO,
365 PFE_SOUND,
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),
381 EndContainer(),
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),
387 EndContainer(),
388 EndContainer(),
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),
398 EndContainer(),
399 EndContainer(),
400 NWidget(WWT_TEXT, COLOUR_GREY, WID_FRW_INFO_DATA_POINTS), SetDataTip(STR_FRAMERATE_DATA_POINTS, 0x0), SetFill(1, 0), SetResize(1, 0),
401 EndContainer(),
402 EndContainer(),
403 NWidget(NWID_VERTICAL),
404 NWidget(NWID_VSCROLLBAR, COLOUR_GREY, WID_FRW_SCROLLBAR),
405 NWidget(WWT_RESIZEBOX, COLOUR_GREY),
406 EndContainer(),
407 EndContainer(),
410 struct FramerateWindow : Window {
411 bool small;
412 bool showing_memory;
413 int num_active;
414 int num_displayed;
416 struct CachedDecimal {
417 StringID strid;
418 uint32_t value;
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);
439 SetDParam(n + 1, 2);
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;
456 this->UpdateData();
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);
469 this->UpdateData();
470 this->SetDirty();
474 /** Update the window on a regular interval. */
475 IntervalTimer<TimerWindow> update_interval = {std::chrono::milliseconds(100), [this](auto) {
476 this->UpdateData();
477 this->SetDirty();
480 void UpdateData()
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);
490 int new_active = 0;
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) {
495 new_active++;
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));
511 this->ReInit();
515 void SetStringParameters(WidgetID widget) const override
517 switch (widget) {
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);
524 break;
526 case WID_FRW_RATE_GAMELOOP:
527 SetDParam(0, this->rate_gameloop.strid);
528 this->rate_gameloop.InsertDParams(1);
529 break;
530 case WID_FRW_RATE_DRAWING:
531 SetDParam(0, this->rate_drawing.strid);
532 this->rate_drawing.InsertDParams(1);
533 break;
534 case WID_FRW_RATE_FACTOR:
535 this->speed_gameloop.InsertDParams(0);
536 break;
537 case WID_FRW_INFO_DATA_POINTS:
538 SetDParam(0, NUM_FRAMERATE_POINTS);
539 break;
543 void UpdateWidgetSize(WidgetID widget, Dimension &size, [[maybe_unused]] const Dimension &padding, [[maybe_unused]] Dimension &fill, [[maybe_unused]] Dimension &resize) override
545 switch (widget) {
546 case WID_FRW_RATE_GAMELOOP:
547 SetDParam(0, STR_FRAMERATE_FPS_GOOD);
548 SetDParamMaxDigits(1, 6);
549 SetDParam(2, 2);
550 size = GetStringBoundingBox(STR_FRAMERATE_RATE_GAMELOOP);
551 break;
552 case WID_FRW_RATE_DRAWING:
553 SetDParam(0, STR_FRAMERATE_FPS_GOOD);
554 SetDParamMaxDigits(1, 6);
555 SetDParam(2, 2);
556 size = GetStringBoundingBox(STR_FRAMERATE_RATE_BLITTER);
557 break;
558 case WID_FRW_RATE_FACTOR:
559 SetDParamMaxDigits(0, 6);
560 SetDParam(1, 2);
561 size = GetStringBoundingBox(STR_FRAMERATE_SPEED_FACTOR);
562 break;
564 case WID_FRW_TIMES_NAMES: {
565 size.width = 0;
566 size.height = GetCharacterHeight(FS_NORMAL) + WidgetDimensions::scaled.vsep_normal + MIN_ELEMENTS * GetCharacterHeight(FS_NORMAL);
567 resize.width = 0;
568 resize.height = GetCharacterHeight(FS_NORMAL);
569 for (PerformanceElement e : DISPLAY_ORDER_PFE) {
570 if (_pf_data[e].num_valid == 0) continue;
571 Dimension line_size;
572 if (e < PFE_AI0) {
573 line_size = GetStringBoundingBox(STR_FRAMERATE_GAMELOOP + e);
574 } else {
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);
581 break;
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);
589 SetDParam(1, 2);
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;
593 resize.width = 0;
594 resize.height = GetCharacterHeight(FS_NORMAL);
595 break;
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;
606 int y = r.top;
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;
611 if (skip > 0) {
612 skip--;
613 } else {
614 values[e].InsertDParams(0);
615 DrawString(r.left, r.right, y, values[e].strid, TC_FROMSTRING, SA_RIGHT);
616 y += GetCharacterHeight(FS_NORMAL);
617 drawable--;
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;
628 int y = r.top;
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;
633 if (skip > 0) {
634 skip--;
635 } else if (e == PFE_GAMESCRIPT || e >= PFE_AI0) {
636 if (e == PFE_GAMESCRIPT) {
637 SetDParam(0, Game::GetInstance()->GetAllocatedMemory());
638 } else {
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);
643 drawable--;
644 if (drawable == 0) break;
645 } else {
646 /* skip non-script */
647 y += GetCharacterHeight(FS_NORMAL);
648 drawable--;
649 if (drawable == 0) break;
654 void DrawWidget(const Rect &r, WidgetID widget) const override
656 switch (widget) {
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;
665 if (skip > 0) {
666 skip--;
667 } else {
668 if (e < PFE_AI0) {
669 DrawString(r.left, r.right, y, STR_FRAMERATE_GAMELOOP + e, TC_FROMSTRING, SA_LEFT);
670 } else {
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);
676 drawable--;
677 if (drawable == 0) break;
680 break;
682 case WID_FRW_TIMES_CURRENT:
683 /* Render short-term average values */
684 DrawElementTimesColumn(r, STR_FRAMERATE_CURRENT, this->times_shortterm);
685 break;
686 case WID_FRW_TIMES_AVERAGE:
687 /* Render averages of all recorded values */
688 DrawElementTimesColumn(r, STR_FRAMERATE_AVERAGE, this->times_longterm);
689 break;
690 case WID_FRW_ALLOCSIZE:
691 DrawElementAllocationsColumn(r);
692 break;
696 void OnClick([[maybe_unused]] Point pt, WidgetID widget, [[maybe_unused]] int click_count) override
698 switch (widget) {
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) {
706 line++;
707 /* Find the visible line that was clicked */
708 for (PerformanceElement e : DISPLAY_ORDER_PFE) {
709 if (_pf_data[e].num_valid > 0) line--;
710 if (line == 0) {
711 ShowFrametimeGraphWindow(e);
712 break;
716 break;
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),
743 EndContainer(),
744 NWidget(WWT_PANEL, COLOUR_GREY),
745 NWidget(NWID_VERTICAL), SetPadding(6),
746 NWidget(WWT_EMPTY, COLOUR_GREY, WID_FGW_GRAPH),
747 EndContainer(),
748 EndContainer(),
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);
765 this->UpdateScale();
768 void SetStringParameters(WidgetID widget) const override
770 switch (widget) {
771 case WID_FGW_CAPTION:
772 if (this->element < PFE_AI0) {
773 SetDParam(0, STR_FRAMETIME_CAPTION_GAMELOOP + this->element);
774 } else {
775 SetDParam(0, STR_FRAMETIME_CAPTION_AI);
776 SetDParam(1, this->element - PFE_AI0 + 1);
777 SetDParamStr(2, GetAIName(this->element - PFE_AI0));
779 break;
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) {
786 SetDParam(0, 100);
787 Dimension size_ms_label = GetStringBoundingBox(STR_FRAMERATE_GRAPH_MILLISECONDS);
788 SetDParam(0, 100);
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;
795 size = graph_size;
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,
832 TIMESTAMP_PRECISION,
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 */
845 void UpdateScale()
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;
855 int count = 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++) {
861 point--;
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];
868 continue;
870 if (value > peak_value) peak_value = value;
871 count++;
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) {
889 this->UpdateScale();
892 void OnRealtimeTick([[maybe_unused]] uint delta_ms) override
894 this->SetDirty();
897 /** Scale and interpolate a value from a source range into a destination range */
898 template<typename T>
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);
939 } else {
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 */
956 Point lastpoint = {
957 x_max,
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++) {
970 point--;
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];
977 continue;
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 */
987 Point newpoint = {
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 */
996 value_sum += value;
997 points_drawn++;
998 if (value > peak_value) {
999 peak_value = 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);
1012 } else {
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 = {
1052 "Game loop",
1053 " GL station ticks",
1054 " GL train ticks",
1055 " GL road vehicle ticks",
1056 " GL ship ticks",
1057 " GL aircraft ticks",
1058 " GL landscape ticks",
1059 " GL link graph delays",
1060 "Drawing",
1061 " Viewport drawing",
1062 "Video output",
1063 "Sound mixing",
1064 "AI/GS scripts total",
1065 "Game script",
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],
1076 pf.GetRate(),
1077 pf.expected_rate);
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;
1085 if (e < PFE_AI0) {
1086 name = MEASUREMENT_NAMES[e];
1087 } else {
1088 ai_name_buf = fmt::format("AI {} {}", e - PFE_AI0 + 1, GetAIName(e - PFE_AI0));
1089 name = ai_name_buf;
1091 IConsolePrint(TC_LIGHT_BLUE, "{} times: {:.2f}ms {:.2f}ms {:.2f}ms",
1092 name,
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);