File diff r24596:eddf98238034 → r24597:afde5721a3b6
src/framerate_gui.cpp
Show inline comments
 
@@ -31,136 +31,136 @@
 
/**
 
 * Private declarations for performance measurement implementation
 
 */
 
namespace {
 

	
 
	/** Number of data points to keep in buffer for each performance measurement */
 
	const int NUM_FRAMERATE_POINTS = 512;
 
	/** %Units a second is divided into in performance measurements */
 
	const TimingMeasurement TIMESTAMP_PRECISION = 1000000;
 

	
 
	struct PerformanceData {
 
		/** Duration value indicating the value is not valid should be considered a gap in measurements */
 
		static const TimingMeasurement INVALID_DURATION = UINT64_MAX;
 

	
 
		/** Time spent processing each cycle of the performance element, circular buffer */
 
		TimingMeasurement durations[NUM_FRAMERATE_POINTS];
 
		/** Start time of each cycle of the performance element, circular buffer */
 
		TimingMeasurement timestamps[NUM_FRAMERATE_POINTS];
 
		/** Expected number of cycles per second when the system is running without slowdowns */
 
		double expected_rate;
 
		/** Next index to write to in \c durations and \c timestamps */
 
		int next_index;
 
		/** Last index written to in \c durations and \c timestamps */
 
		int prev_index;
 
		/** Number of data points recorded, clamped to \c NUM_FRAMERATE_POINTS */
 
		int num_valid;
 

	
 
		/** Current accumulated duration */
 
		TimingMeasurement acc_duration;
 
		/** Start time for current accumulation cycle */
 
		TimingMeasurement acc_timestamp;
 

	
 
		/**
 
		 * Initialize a data element with an expected collection rate
 
		 * @param expected_rate
 
		 * Expected number of cycles per second of the performance element. Use 1 if unknown or not relevant.
 
		 * The rate is used for highlighting slow-running elements in the GUI.
 
		 */
 
		explicit PerformanceData(double expected_rate) : expected_rate(expected_rate), next_index(0), prev_index(0), num_valid(0) { }
 

	
 
		/** Collect a complete measurement, given start and ending times for a processing block */
 
		void Add(TimingMeasurement start_time, TimingMeasurement end_time)
 
		{
 
			this->durations[this->next_index] = end_time - start_time;
 
			this->timestamps[this->next_index] = start_time;
 
			this->prev_index = this->next_index;
 
			this->next_index += 1;
 
			if (this->next_index >= NUM_FRAMERATE_POINTS) this->next_index = 0;
 
			this->num_valid = min(NUM_FRAMERATE_POINTS, this->num_valid + 1);
 
			this->num_valid = std::min(NUM_FRAMERATE_POINTS, this->num_valid + 1);
 
		}
 

	
 
		/** Begin an accumulation of multiple measurements into a single value, from a given start time */
 
		void BeginAccumulate(TimingMeasurement start_time)
 
		{
 
			this->timestamps[this->next_index] = this->acc_timestamp;
 
			this->durations[this->next_index] = this->acc_duration;
 
			this->prev_index = this->next_index;
 
			this->next_index += 1;
 
			if (this->next_index >= NUM_FRAMERATE_POINTS) this->next_index = 0;
 
			this->num_valid = min(NUM_FRAMERATE_POINTS, this->num_valid + 1);
 
			this->num_valid = std::min(NUM_FRAMERATE_POINTS, this->num_valid + 1);
 

	
 
			this->acc_duration = 0;
 
			this->acc_timestamp = start_time;
 
		}
 

	
 
		/** Accumulate a period onto the current measurement */
 
		void AddAccumulate(TimingMeasurement duration)
 
		{
 
			this->acc_duration += duration;
 
		}
 

	
 
		/** Indicate a pause/expected discontinuity in processing the element */
 
		void AddPause(TimingMeasurement start_time)
 
		{
 
			if (this->durations[this->prev_index] != INVALID_DURATION) {
 
				this->timestamps[this->next_index] = start_time;
 
				this->durations[this->next_index] = INVALID_DURATION;
 
				this->prev_index = this->next_index;
 
				this->next_index += 1;
 
				if (this->next_index >= NUM_FRAMERATE_POINTS) this->next_index = 0;
 
				this->num_valid += 1;
 
			}
 
		}
 

	
 
		/** Get average cycle processing time over a number of data points */
 
		double GetAverageDurationMilliseconds(int count)
 
		{
 
			count = min(count, this->num_valid);
 
			count = std::min(count, this->num_valid);
 

	
 
			int first_point = this->prev_index - count;
 
			if (first_point < 0) first_point += NUM_FRAMERATE_POINTS;
 

	
 
			/* Sum durations, skipping invalid points */
 
			double sumtime = 0;
 
			for (int i = first_point; i < first_point + count; i++) {
 
				auto d = this->durations[i % NUM_FRAMERATE_POINTS];
 
				if (d != INVALID_DURATION) {
 
					sumtime += d;
 
				} else {
 
					/* Don't count the invalid durations */
 
					count--;
 
				}
 
			}
 

	
 
			if (count == 0) return 0; // avoid div by zero
 
			return sumtime * 1000 / count / TIMESTAMP_PRECISION;
 
		}
 

	
 
		/** Get current rate of a performance element, based on approximately the past one second of data */
 
		double GetRate()
 
		{
 
			/* Start at last recorded point, end at latest when reaching the earliest recorded point */
 
			int point = this->prev_index;
 
			int last_point = this->next_index - this->num_valid;
 
			if (last_point < 0) last_point += NUM_FRAMERATE_POINTS;
 

	
 
			/* Number of data points collected */
 
			int count = 0;
 
			/* Time of previous data point */
 
			TimingMeasurement last = this->timestamps[point];
 
			/* Total duration covered by collected points */
 
			TimingMeasurement total = 0;
 

	
 
			while (point != last_point) {
 
				/* Only record valid data points, but pretend the gaps in measurements aren't there */
 
				if (this->durations[point] != INVALID_DURATION) {
 
					total += last - this->timestamps[point];
 
					count++;
 
				}
 
				last = this->timestamps[point];
 
				if (total >= TIMESTAMP_PRECISION) break; // end after 1 second has been collected
 
				point--;
 
				if (point < 0) point = NUM_FRAMERATE_POINTS - 1;
 
			}
 

	
 
			if (total == 0 || count == 0) return 0;
 
@@ -350,269 +350,269 @@ static const NWidgetPart _framerate_wind
 
		NWidget(WWT_CLOSEBOX, COLOUR_GREY),
 
		NWidget(WWT_CAPTION, COLOUR_GREY, WID_FRW_CAPTION), SetDataTip(STR_FRAMERATE_CAPTION, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS),
 
		NWidget(WWT_SHADEBOX, COLOUR_GREY),
 
		NWidget(WWT_STICKYBOX, COLOUR_GREY),
 
	EndContainer(),
 
	NWidget(WWT_PANEL, COLOUR_GREY),
 
		NWidget(NWID_VERTICAL), SetPadding(6), SetPIP(0, 3, 0),
 
			NWidget(WWT_TEXT, COLOUR_GREY, WID_FRW_RATE_GAMELOOP), SetDataTip(STR_FRAMERATE_RATE_GAMELOOP, STR_FRAMERATE_RATE_GAMELOOP_TOOLTIP),
 
			NWidget(WWT_TEXT, COLOUR_GREY, WID_FRW_RATE_DRAWING),  SetDataTip(STR_FRAMERATE_RATE_BLITTER,  STR_FRAMERATE_RATE_BLITTER_TOOLTIP),
 
			NWidget(WWT_TEXT, COLOUR_GREY, WID_FRW_RATE_FACTOR),   SetDataTip(STR_FRAMERATE_SPEED_FACTOR,  STR_FRAMERATE_SPEED_FACTOR_TOOLTIP),
 
		EndContainer(),
 
	EndContainer(),
 
	NWidget(NWID_HORIZONTAL),
 
		NWidget(WWT_PANEL, COLOUR_GREY),
 
			NWidget(NWID_VERTICAL), SetPadding(6), SetPIP(0, 3, 0),
 
				NWidget(NWID_HORIZONTAL), SetPIP(0, 6, 0),
 
					NWidget(WWT_EMPTY, COLOUR_GREY, WID_FRW_TIMES_NAMES), SetScrollbar(WID_FRW_SCROLLBAR),
 
					NWidget(WWT_EMPTY, COLOUR_GREY, WID_FRW_TIMES_CURRENT), SetScrollbar(WID_FRW_SCROLLBAR),
 
					NWidget(WWT_EMPTY, COLOUR_GREY, WID_FRW_TIMES_AVERAGE), SetScrollbar(WID_FRW_SCROLLBAR),
 
					NWidget(NWID_SELECTION, INVALID_COLOUR, WID_FRW_SEL_MEMORY),
 
						NWidget(WWT_EMPTY, COLOUR_GREY, WID_FRW_ALLOCSIZE), SetScrollbar(WID_FRW_SCROLLBAR),
 
					EndContainer(),
 
				EndContainer(),
 
				NWidget(WWT_TEXT, COLOUR_GREY, WID_FRW_INFO_DATA_POINTS), SetDataTip(STR_FRAMERATE_DATA_POINTS, 0x0),
 
			EndContainer(),
 
		EndContainer(),
 
		NWidget(NWID_VERTICAL),
 
			NWidget(NWID_VSCROLLBAR, COLOUR_GREY, WID_FRW_SCROLLBAR),
 
			NWidget(WWT_RESIZEBOX, COLOUR_GREY),
 
		EndContainer(),
 
	EndContainer(),
 
};
 

	
 
struct FramerateWindow : Window {
 
	bool small;
 
	bool showing_memory;
 
	GUITimer next_update;
 
	int num_active;
 
	int num_displayed;
 

	
 
	struct CachedDecimal {
 
		StringID strid;
 
		uint32 value;
 

	
 
		inline void SetRate(double value, double target)
 
		{
 
			const double threshold_good = target * 0.95;
 
			const double threshold_bad = target * 2 / 3;
 
			value = min(9999.99, value);
 
			value = std::min(9999.99, value);
 
			this->value = (uint32)(value * 100);
 
			this->strid = (value > threshold_good) ? STR_FRAMERATE_FPS_GOOD : (value < threshold_bad) ? STR_FRAMERATE_FPS_BAD : STR_FRAMERATE_FPS_WARN;
 
		}
 

	
 
		inline void SetTime(double value, double target)
 
		{
 
			const double threshold_good = target / 3;
 
			const double threshold_bad = target;
 
			value = min(9999.99, value);
 
			value = std::min(9999.99, value);
 
			this->value = (uint32)(value * 100);
 
			this->strid = (value < threshold_good) ? STR_FRAMERATE_MS_GOOD : (value > threshold_bad) ? STR_FRAMERATE_MS_BAD : STR_FRAMERATE_MS_WARN;
 
		}
 

	
 
		inline void InsertDParams(uint n) const
 
		{
 
			SetDParam(n, this->value);
 
			SetDParam(n + 1, 2);
 
		}
 
	};
 

	
 
	CachedDecimal rate_gameloop;            ///< cached game loop tick rate
 
	CachedDecimal rate_drawing;             ///< cached drawing frame rate
 
	CachedDecimal speed_gameloop;           ///< cached game loop speed factor
 
	CachedDecimal times_shortterm[PFE_MAX]; ///< cached short term average times
 
	CachedDecimal times_longterm[PFE_MAX];  ///< cached long term average times
 

	
 
	static const int VSPACING = 3;          ///< space between column heading and values
 
	static const int MIN_ELEMENTS = 5;      ///< smallest number of elements to display
 
	static constexpr int VSPACING = 3;          ///< space between column heading and values
 
	static constexpr int MIN_ELEMENTS = 5;      ///< smallest number of elements to display
 

	
 
	FramerateWindow(WindowDesc *desc, WindowNumber number) : Window(desc)
 
	{
 
		this->InitNested(number);
 
		this->small = this->IsShaded();
 
		this->showing_memory = true;
 
		this->UpdateData();
 
		this->num_displayed = this->num_active;
 
		this->next_update.SetInterval(100);
 

	
 
		/* Window is always initialised to MIN_ELEMENTS height, resize to contain num_displayed */
 
		ResizeWindow(this, 0, (max(MIN_ELEMENTS, this->num_displayed) - MIN_ELEMENTS) * FONT_HEIGHT_NORMAL);
 
		ResizeWindow(this, 0, (std::max(MIN_ELEMENTS, this->num_displayed) - MIN_ELEMENTS) * FONT_HEIGHT_NORMAL);
 
	}
 

	
 
	void OnRealtimeTick(uint delta_ms) override
 
	{
 
		bool elapsed = this->next_update.Elapsed(delta_ms);
 

	
 
		/* Check if the shaded state has changed, switch caption text if it has */
 
		if (this->small != this->IsShaded()) {
 
			this->small = this->IsShaded();
 
			this->GetWidget<NWidgetLeaf>(WID_FRW_CAPTION)->SetDataTip(this->small ? STR_FRAMERATE_CAPTION_SMALL : STR_FRAMERATE_CAPTION, STR_TOOLTIP_WINDOW_TITLE_DRAG_THIS);
 
			elapsed = true;
 
		}
 

	
 
		if (elapsed) {
 
			this->UpdateData();
 
			this->SetDirty();
 
			this->next_update.SetInterval(100);
 
		}
 
	}
 

	
 
	void UpdateData()
 
	{
 
		double gl_rate = _pf_data[PFE_GAMELOOP].GetRate();
 
		bool have_script = false;
 
		this->rate_gameloop.SetRate(gl_rate, _pf_data[PFE_GAMELOOP].expected_rate);
 
		this->speed_gameloop.SetRate(gl_rate / _pf_data[PFE_GAMELOOP].expected_rate, 1.0);
 
		if (this->small) return; // in small mode, this is everything needed
 

	
 
		this->rate_drawing.SetRate(_pf_data[PFE_DRAWING].GetRate(), _pf_data[PFE_DRAWING].expected_rate);
 

	
 
		int new_active = 0;
 
		for (PerformanceElement e = PFE_FIRST; e < PFE_MAX; e++) {
 
			this->times_shortterm[e].SetTime(_pf_data[e].GetAverageDurationMilliseconds(8), MILLISECONDS_PER_TICK);
 
			this->times_longterm[e].SetTime(_pf_data[e].GetAverageDurationMilliseconds(NUM_FRAMERATE_POINTS), MILLISECONDS_PER_TICK);
 
			if (_pf_data[e].num_valid > 0) {
 
				new_active++;
 
				if (e == PFE_GAMESCRIPT || e >= PFE_AI0) have_script = true;
 
			}
 
		}
 

	
 
		if (this->showing_memory != have_script) {
 
			NWidgetStacked *plane = this->GetWidget<NWidgetStacked>(WID_FRW_SEL_MEMORY);
 
			plane->SetDisplayedPlane(have_script ? 0 : SZSP_VERTICAL);
 
			this->showing_memory = have_script;
 
		}
 

	
 
		if (new_active != this->num_active) {
 
			this->num_active = new_active;
 
			Scrollbar *sb = this->GetScrollbar(WID_FRW_SCROLLBAR);
 
			sb->SetCount(this->num_active);
 
			sb->SetCapacity(min(this->num_displayed, this->num_active));
 
			sb->SetCapacity(std::min(this->num_displayed, this->num_active));
 
			this->ReInit();
 
		}
 
	}
 

	
 
	void SetStringParameters(int widget) const override
 
	{
 
		switch (widget) {
 
			case WID_FRW_CAPTION:
 
				/* When the window is shaded, the caption shows game loop rate and speed factor */
 
				if (!this->small) break;
 
				SetDParam(0, this->rate_gameloop.strid);
 
				this->rate_gameloop.InsertDParams(1);
 
				this->speed_gameloop.InsertDParams(3);
 
				break;
 

	
 
			case WID_FRW_RATE_GAMELOOP:
 
				SetDParam(0, this->rate_gameloop.strid);
 
				this->rate_gameloop.InsertDParams(1);
 
				break;
 
			case WID_FRW_RATE_DRAWING:
 
				SetDParam(0, this->rate_drawing.strid);
 
				this->rate_drawing.InsertDParams(1);
 
				break;
 
			case WID_FRW_RATE_FACTOR:
 
				this->speed_gameloop.InsertDParams(0);
 
				break;
 
			case WID_FRW_INFO_DATA_POINTS:
 
				SetDParam(0, NUM_FRAMERATE_POINTS);
 
				break;
 
		}
 
	}
 

	
 
	void UpdateWidgetSize(int widget, Dimension *size, const Dimension &padding, Dimension *fill, Dimension *resize) override
 
	{
 
		switch (widget) {
 
			case WID_FRW_RATE_GAMELOOP:
 
				SetDParam(0, STR_FRAMERATE_FPS_GOOD);
 
				SetDParam(1, 999999);
 
				SetDParam(2, 2);
 
				*size = GetStringBoundingBox(STR_FRAMERATE_RATE_GAMELOOP);
 
				break;
 
			case WID_FRW_RATE_DRAWING:
 
				SetDParam(0, STR_FRAMERATE_FPS_GOOD);
 
				SetDParam(1, 999999);
 
				SetDParam(2, 2);
 
				*size = GetStringBoundingBox(STR_FRAMERATE_RATE_BLITTER);
 
				break;
 
			case WID_FRW_RATE_FACTOR:
 
				SetDParam(0, 999999);
 
				SetDParam(1, 2);
 
				*size = GetStringBoundingBox(STR_FRAMERATE_SPEED_FACTOR);
 
				break;
 

	
 
			case WID_FRW_TIMES_NAMES: {
 
				size->width = 0;
 
				size->height = FONT_HEIGHT_NORMAL + VSPACING + MIN_ELEMENTS * FONT_HEIGHT_NORMAL;
 
				resize->width = 0;
 
				resize->height = FONT_HEIGHT_NORMAL;
 
				for (PerformanceElement e : DISPLAY_ORDER_PFE) {
 
					if (_pf_data[e].num_valid == 0) continue;
 
					Dimension line_size;
 
					if (e < PFE_AI0) {
 
						line_size = GetStringBoundingBox(STR_FRAMERATE_GAMELOOP + e);
 
					} else {
 
						SetDParam(0, e - PFE_AI0 + 1);
 
						SetDParamStr(1, GetAIName(e - PFE_AI0));
 
						line_size = GetStringBoundingBox(STR_FRAMERATE_AI);
 
					}
 
					size->width = max(size->width, line_size.width);
 
					size->width = std::max(size->width, line_size.width);
 
				}
 
				break;
 
			}
 

	
 
			case WID_FRW_TIMES_CURRENT:
 
			case WID_FRW_TIMES_AVERAGE:
 
			case WID_FRW_ALLOCSIZE: {
 
				*size = GetStringBoundingBox(STR_FRAMERATE_CURRENT + (widget - WID_FRW_TIMES_CURRENT));
 
				SetDParam(0, 999999);
 
				SetDParam(1, 2);
 
				Dimension item_size = GetStringBoundingBox(STR_FRAMERATE_MS_GOOD);
 
				size->width = max(size->width, item_size.width);
 
				size->width = std::max(size->width, item_size.width);
 
				size->height += FONT_HEIGHT_NORMAL * MIN_ELEMENTS + VSPACING;
 
				resize->width = 0;
 
				resize->height = FONT_HEIGHT_NORMAL;
 
				break;
 
			}
 
		}
 
	}
 

	
 
	/** Render a column of formatted average durations */
 
	void DrawElementTimesColumn(const Rect &r, StringID heading_str, const CachedDecimal *values) const
 
	{
 
		const Scrollbar *sb = this->GetScrollbar(WID_FRW_SCROLLBAR);
 
		uint16 skip = sb->GetPosition();
 
		int drawable = this->num_displayed;
 
		int y = r.top;
 
		DrawString(r.left, r.right, y, heading_str, TC_FROMSTRING, SA_CENTER, true);
 
		y += FONT_HEIGHT_NORMAL + VSPACING;
 
		for (PerformanceElement e : DISPLAY_ORDER_PFE) {
 
			if (_pf_data[e].num_valid == 0) continue;
 
			if (skip > 0) {
 
				skip--;
 
			} else {
 
				values[e].InsertDParams(0);
 
				DrawString(r.left, r.right, y, values[e].strid, TC_FROMSTRING, SA_RIGHT);
 
				y += FONT_HEIGHT_NORMAL;
 
				drawable--;
 
				if (drawable == 0) break;
 
			}
 
		}
 
	}
 

	
 
	void DrawElementAllocationsColumn(const Rect &r) const
 
	{
 
		const Scrollbar *sb = this->GetScrollbar(WID_FRW_SCROLLBAR);
 
		uint16 skip = sb->GetPosition();
 
		int drawable = this->num_displayed;
 
		int y = r.top;
 
		DrawString(r.left, r.right, y, STR_FRAMERATE_MEMORYUSE, TC_FROMSTRING, SA_CENTER, true);
 
		y += FONT_HEIGHT_NORMAL + VSPACING;
 
		for (PerformanceElement e : DISPLAY_ORDER_PFE) {
 
			if (_pf_data[e].num_valid == 0) continue;
 
			if (skip > 0) {
 
				skip--;
 
			} else if (e == PFE_GAMESCRIPT || e >= PFE_AI0) {
 
				if (e == PFE_GAMESCRIPT) {
 
					SetDParam(0, Game::GetInstance()->GetAllocatedMemory());
 
				} else {
 
					SetDParam(0, Company::Get(e - PFE_AI0)->ai_instance->GetAllocatedMemory());
 
@@ -724,97 +724,97 @@ static const NWidgetPart _frametime_grap
 
		NWidget(NWID_VERTICAL), SetPadding(6),
 
			NWidget(WWT_EMPTY, COLOUR_GREY, WID_FGW_GRAPH),
 
		EndContainer(),
 
	EndContainer(),
 
};
 

	
 
struct FrametimeGraphWindow : Window {
 
	int vertical_scale;       ///< number of TIMESTAMP_PRECISION units vertically
 
	int horizontal_scale;     ///< number of half-second units horizontally
 
	GUITimer next_scale_update; ///< interval for next scale update
 

	
 
	PerformanceElement element; ///< what element this window renders graph for
 
	Dimension graph_size;       ///< size of the main graph area (excluding axis labels)
 

	
 
	FrametimeGraphWindow(WindowDesc *desc, WindowNumber number) : Window(desc)
 
	{
 
		this->element = (PerformanceElement)number;
 
		this->horizontal_scale = 4;
 
		this->vertical_scale = TIMESTAMP_PRECISION / 10;
 
		this->next_scale_update.SetInterval(1);
 

	
 
		this->InitNested(number);
 
	}
 

	
 
	void SetStringParameters(int widget) const override
 
	{
 
		switch (widget) {
 
			case WID_FGW_CAPTION:
 
				if (this->element < PFE_AI0) {
 
					SetDParam(0, STR_FRAMETIME_CAPTION_GAMELOOP + this->element);
 
				} else {
 
					SetDParam(0, STR_FRAMETIME_CAPTION_AI);
 
					SetDParam(1, this->element - PFE_AI0 + 1);
 
					SetDParamStr(2, GetAIName(this->element - PFE_AI0));
 
				}
 
				break;
 
		}
 
	}
 

	
 
	void UpdateWidgetSize(int widget, Dimension *size, const Dimension &padding, Dimension *fill, Dimension *resize) override
 
	{
 
		if (widget == WID_FGW_GRAPH) {
 
			SetDParam(0, 100);
 
			Dimension size_ms_label = GetStringBoundingBox(STR_FRAMERATE_GRAPH_MILLISECONDS);
 
			SetDParam(0, 100);
 
			Dimension size_s_label = GetStringBoundingBox(STR_FRAMERATE_GRAPH_SECONDS);
 

	
 
			/* Size graph in height to fit at least 10 vertical labels with space between, or at least 100 pixels */
 
			graph_size.height = max<uint>(100, 10 * (size_ms_label.height + 1));
 
			graph_size.height = std::max(100u, 10 * (size_ms_label.height + 1));
 
			/* Always 2:1 graph area */
 
			graph_size.width = 2 * graph_size.height;
 
			*size = graph_size;
 

	
 
			size->width += size_ms_label.width + 2;
 
			size->height += size_s_label.height + 2;
 
		}
 
	}
 

	
 
	void SelectHorizontalScale(TimingMeasurement range)
 
	{
 
		/* Determine horizontal scale based on period covered by 60 points
 
		* (slightly less than 2 seconds at full game speed) */
 
		struct ScaleDef { TimingMeasurement range; int scale; };
 
		static const ScaleDef hscales[] = {
 
			{ 120, 60 },
 
			{  10, 20 },
 
			{   5, 10 },
 
			{   3,  4 },
 
			{   1,  2 },
 
		};
 
		for (const ScaleDef *sc = hscales; sc < hscales + lengthof(hscales); sc++) {
 
			if (range < sc->range) this->horizontal_scale = sc->scale;
 
		}
 
	}
 

	
 
	void SelectVerticalScale(TimingMeasurement range)
 
	{
 
		/* Determine vertical scale based on peak value (within the horizontal scale + a bit) */
 
		static const TimingMeasurement vscales[] = {
 
			TIMESTAMP_PRECISION * 100,
 
			TIMESTAMP_PRECISION * 10,
 
			TIMESTAMP_PRECISION * 5,
 
			TIMESTAMP_PRECISION,
 
			TIMESTAMP_PRECISION / 2,
 
			TIMESTAMP_PRECISION / 5,
 
			TIMESTAMP_PRECISION / 10,
 
			TIMESTAMP_PRECISION / 50,
 
			TIMESTAMP_PRECISION / 200,
 
		};
 
		for (const TimingMeasurement *sc = vscales; sc < vscales + lengthof(vscales); sc++) {
 
			if (range < *sc) this->vertical_scale = (int)*sc;
 
		}
 
	}
 

	
 
	/** Recalculate the graph scaling factors based on current recorded data */
 
	void UpdateScale()
 
	{
 
@@ -935,97 +935,97 @@ struct FrametimeGraphWindow : Window {
 
			TimingMeasurement lastts = timestamps[point];
 

	
 
			TimingMeasurement peak_value = 0;
 
			Point peak_point = { 0, 0 };
 
			TimingMeasurement value_sum = 0;
 
			TimingMeasurement time_sum = 0;
 
			int points_drawn = 0;
 

	
 
			for (int i = 1; i < NUM_FRAMERATE_POINTS; i++) {
 
				point--;
 
				if (point < 0) point = NUM_FRAMERATE_POINTS - 1;
 

	
 
				TimingMeasurement value = durations[point];
 
				if (value == PerformanceData::INVALID_DURATION) {
 
					/* Skip gaps in measurements, pretend the data points on each side are continuous */
 
					lastts = timestamps[point];
 
					continue;
 
				}
 

	
 
				/* Use total time period covered for value along horizontal axis */
 
				time_sum += lastts - timestamps[point];
 
				lastts = timestamps[point];
 
				/* Stop if past the width of the graph */
 
				if (time_sum > draw_horz_scale) break;
 

	
 
				/* Draw line from previous point to new point */
 
				Point newpoint = {
 
					(int)Scinterlate<int64>(x_zero, x_max, 0, (int64)draw_horz_scale, (int64)draw_horz_scale - (int64)time_sum),
 
					(int)Scinterlate<int64>(y_zero, y_max, 0, (int64)draw_vert_scale, (int64)value)
 
				};
 
				assert(newpoint.x <= lastpoint.x);
 
				GfxDrawLine(lastpoint.x, lastpoint.y, newpoint.x, newpoint.y, c_lines);
 
				lastpoint = newpoint;
 

	
 
				/* Record peak and average value across graphed data */
 
				value_sum += value;
 
				points_drawn++;
 
				if (value > peak_value) {
 
					peak_value = value;
 
					peak_point = newpoint;
 
				}
 
			}
 

	
 
			/* If the peak value is significantly larger than the average, mark and label it */
 
			if (points_drawn > 0 && peak_value > TIMESTAMP_PRECISION / 100 && 2 * peak_value > 3 * value_sum / points_drawn) {
 
				TextColour tc_peak = (TextColour)(TC_IS_PALETTE_COLOUR | c_peak);
 
				GfxFillRect(peak_point.x - 1, peak_point.y - 1, peak_point.x + 1, peak_point.y + 1, c_peak);
 
				SetDParam(0, peak_value * 1000 / TIMESTAMP_PRECISION);
 
				int label_y = max(y_max, peak_point.y - FONT_HEIGHT_SMALL);
 
				int label_y = std::max(y_max, peak_point.y - FONT_HEIGHT_SMALL);
 
				if (peak_point.x - x_zero > (int)this->graph_size.width / 2) {
 
					DrawString(x_zero, peak_point.x - 2, label_y, STR_FRAMERATE_GRAPH_MILLISECONDS, tc_peak, SA_RIGHT | SA_FORCE, false, FS_SMALL);
 
				} else {
 
					DrawString(peak_point.x + 2, x_max, label_y, STR_FRAMERATE_GRAPH_MILLISECONDS, tc_peak, SA_LEFT | SA_FORCE, false, FS_SMALL);
 
				}
 
			}
 
		}
 
	}
 
};
 

	
 
static WindowDesc _frametime_graph_window_desc(
 
	WDP_AUTO, "frametime_graph", 140, 90,
 
	WC_FRAMETIME_GRAPH, WC_NONE,
 
	0,
 
	_frametime_graph_window_widgets, lengthof(_frametime_graph_window_widgets)
 
);
 

	
 

	
 

	
 
/** Open the general framerate window */
 
void ShowFramerateWindow()
 
{
 
	AllocateWindowDescFront<FramerateWindow>(&_framerate_display_desc, 0);
 
}
 

	
 
/** Open a graph window for a performance element */
 
void ShowFrametimeGraphWindow(PerformanceElement elem)
 
{
 
	if (elem < PFE_FIRST || elem >= PFE_MAX) return; // maybe warn?
 
	AllocateWindowDescFront<FrametimeGraphWindow>(&_frametime_graph_window_desc, elem, true);
 
}
 

	
 
/** Print performance statistics to game console */
 
void ConPrintFramerate()
 
{
 
	const int count1 = NUM_FRAMERATE_POINTS / 8;
 
	const int count2 = NUM_FRAMERATE_POINTS / 4;
 
	const int count3 = NUM_FRAMERATE_POINTS / 1;
 

	
 
	IConsolePrintF(TC_SILVER, "Based on num. data points: %d %d %d", count1, count2, count3);
 

	
 
	static const char *MEASUREMENT_NAMES[PFE_MAX] = {
 
		"Game loop",
 
		"  GL station ticks",
 
		"  GL train ticks",
 
		"  GL road vehicle ticks",
 
		"  GL ship ticks",
 
		"  GL aircraft ticks",