glsCalibration.cpp revision 5f40053fac3cbba8a029ce5685d8519c3668e89e
1/*-------------------------------------------------------------------------
2 * drawElements Quality Program OpenGL (ES) Module
3 * -----------------------------------------------
4 *
5 * Copyright 2014 The Android Open Source Project
6 *
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
10 *
11 *      http://www.apache.org/licenses/LICENSE-2.0
12 *
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 *
19 *//*!
20 * \file
21 * \brief Calibration tools.
22 *//*--------------------------------------------------------------------*/
23
24#include "glsCalibration.hpp"
25#include "tcuTestLog.hpp"
26#include "tcuVectorUtil.hpp"
27#include "deStringUtil.hpp"
28#include "deMath.h"
29#include "deClock.h"
30
31#include <algorithm>
32#include <limits>
33
34using std::string;
35using std::vector;
36using tcu::Vec2;
37using tcu::TestLog;
38using tcu::TestNode;
39using namespace glu;
40
41namespace deqp
42{
43namespace gls
44{
45
46// Reorders input arbitrarily, linear complexity and no allocations
47template<typename T>
48float destructiveMedian (vector<T>& data)
49{
50	const typename vector<T>::iterator mid = data.begin()+data.size()/2;
51
52	std::nth_element(data.begin(), mid, data.end());
53
54	if (data.size()%2 == 0) // Even number of elements, need average of two centermost elements
55		return (*mid + *std::max_element(data.begin(), mid))*0.5f; // Data is partially sorted around mid, mid is half an item after center
56	else
57		return *mid;
58}
59
60LineParameters theilSenLinearRegression (const std::vector<tcu::Vec2>& dataPoints)
61{
62	const float		epsilon					= 1e-6f;
63
64	const int		numDataPoints			= (int)dataPoints.size();
65	vector<float>	pairwiseCoefficients;
66	vector<float>	pointwiseOffsets;
67	LineParameters	result					(0.0f, 0.0f);
68
69	// Compute the pairwise coefficients.
70	for (int i = 0; i < numDataPoints; i++)
71	{
72		const Vec2& ptA = dataPoints[i];
73
74		for (int j = 0; j < i; j++)
75		{
76			const Vec2& ptB = dataPoints[j];
77
78			if (de::abs(ptA.x() - ptB.x()) > epsilon)
79				pairwiseCoefficients.push_back((ptA.y() - ptB.y()) / (ptA.x() - ptB.x()));
80		}
81	}
82
83	// Find the median of the pairwise coefficients.
84	// \note If there are no data point pairs with differing x values, the coefficient variable will stay zero as initialized.
85	if (!pairwiseCoefficients.empty())
86		result.coefficient = destructiveMedian(pairwiseCoefficients);
87
88	// Compute the offsets corresponding to the median coefficient, for all data points.
89	for (int i = 0; i < numDataPoints; i++)
90		pointwiseOffsets.push_back(dataPoints[i].y() - result.coefficient*dataPoints[i].x());
91
92	// Find the median of the offsets.
93	// \note If there are no data points, the offset variable will stay zero as initialized.
94	if (!pointwiseOffsets.empty())
95		result.offset = destructiveMedian(pointwiseOffsets);
96
97	return result;
98}
99
100// Sample from given values using linear interpolation at a given position as if values were laid to range [0, 1]
101template <typename T>
102static float linearSample (const std::vector<T>& values, float position)
103{
104	DE_ASSERT(position >= 0.0f);
105	DE_ASSERT(position <= 1.0f);
106
107	const int	maxNdx				= (int)values.size() - 1;
108	const float	floatNdx			= (float)maxNdx * position;
109	const int	lowerNdx			= (int)deFloatFloor(floatNdx);
110	const int	higherNdx			= lowerNdx + (lowerNdx == maxNdx ? 0 : 1); // Use only last element if position is 1.0
111	const float	interpolationFactor = floatNdx - (float)lowerNdx;
112
113	DE_ASSERT(lowerNdx >= 0 && lowerNdx < (int)values.size());
114	DE_ASSERT(higherNdx >= 0 && higherNdx < (int)values.size());
115	DE_ASSERT(interpolationFactor >= 0 && interpolationFactor < 1.0f);
116
117	return tcu::mix((float)values[lowerNdx], (float)values[higherNdx], interpolationFactor);
118}
119
120LineParametersWithConfidence theilSenSiegelLinearRegression (const std::vector<tcu::Vec2>& dataPoints, float reportedConfidence)
121{
122	DE_ASSERT(!dataPoints.empty());
123
124	// Siegel's variation
125
126	const float						epsilon				= 1e-6f;
127	const int						numDataPoints		= (int)dataPoints.size();
128	std::vector<float>				medianSlopes;
129	std::vector<float>				pointwiseOffsets;
130	LineParametersWithConfidence	result;
131
132	// Compute the median slope via each element
133	for (int i = 0; i < numDataPoints; i++)
134	{
135		const tcu::Vec2&	ptA		= dataPoints[i];
136		std::vector<float>	slopes;
137
138		slopes.reserve(numDataPoints);
139
140		for (int j = 0; j < numDataPoints; j++)
141		{
142			const tcu::Vec2& ptB = dataPoints[j];
143
144			if (de::abs(ptA.x() - ptB.x()) > epsilon)
145				slopes.push_back((ptA.y() - ptB.y()) / (ptA.x() - ptB.x()));
146		}
147
148		// Add median of slopes through point i
149		medianSlopes.push_back(destructiveMedian(slopes));
150	}
151
152	DE_ASSERT(!medianSlopes.empty());
153
154	// Find the median of the pairwise coefficients.
155	std::sort(medianSlopes.begin(), medianSlopes.end());
156	result.coefficient = linearSample(medianSlopes, 0.5f);
157
158	// Compute the offsets corresponding to the median coefficient, for all data points.
159	for (int i = 0; i < numDataPoints; i++)
160		pointwiseOffsets.push_back(dataPoints[i].y() - result.coefficient*dataPoints[i].x());
161
162	// Find the median of the offsets.
163	std::sort(pointwiseOffsets.begin(), pointwiseOffsets.end());
164	result.offset = linearSample(pointwiseOffsets, 0.5f);
165
166	// calculate confidence intervals
167	result.coefficientConfidenceLower = linearSample(medianSlopes, 0.5f - reportedConfidence*0.5f);
168	result.coefficientConfidenceUpper = linearSample(medianSlopes, 0.5f + reportedConfidence*0.5f);
169
170	result.offsetConfidenceLower = linearSample(pointwiseOffsets, 0.5f - reportedConfidence*0.5f);
171	result.offsetConfidenceUpper = linearSample(pointwiseOffsets, 0.5f + reportedConfidence*0.5f);
172
173	result.confidence = reportedConfidence;
174
175	return result;
176}
177
178bool MeasureState::isDone (void) const
179{
180	return (int)frameTimes.size() >= maxNumFrames || (frameTimes.size() >= 2 &&
181													  frameTimes[frameTimes.size()-2] >= (deUint64)frameShortcutTime &&
182													  frameTimes[frameTimes.size()-1] >= (deUint64)frameShortcutTime);
183}
184
185deUint64 MeasureState::getTotalTime (void) const
186{
187	deUint64 time = 0;
188	for (int i = 0; i < (int)frameTimes.size(); i++)
189		time += frameTimes[i];
190	return time;
191}
192
193void MeasureState::clear (void)
194{
195	maxNumFrames		= 0;
196	frameShortcutTime	= std::numeric_limits<float>::infinity();
197	numDrawCalls		= 0;
198	frameTimes.clear();
199}
200
201void MeasureState::start (int maxNumFrames_, float frameShortcutTime_, int numDrawCalls_)
202{
203	frameTimes.clear();
204	frameTimes.reserve(maxNumFrames_);
205	maxNumFrames		= maxNumFrames_;
206	frameShortcutTime	= frameShortcutTime_;
207	numDrawCalls		= numDrawCalls_;
208}
209
210TheilSenCalibrator::TheilSenCalibrator (void)
211	: m_params	(1 /* initial calls */, 10 /* calibrate iter frames */, 2000.0f /* calibrate iter shortcut threshold */, 31 /* max calibration iterations */,
212				 1000.0f/30.0f /* target frame time */, 1000.0f/60.0f /* frame time cap */, 1000.0f /* target measure duration */)
213	, m_state	(INTERNALSTATE_LAST)
214{
215	clear();
216}
217
218TheilSenCalibrator::TheilSenCalibrator (const CalibratorParameters& params)
219	: m_params	(params)
220	, m_state	(INTERNALSTATE_LAST)
221{
222	clear();
223}
224
225TheilSenCalibrator::~TheilSenCalibrator()
226{
227}
228
229void TheilSenCalibrator::clear (void)
230{
231	m_measureState.clear();
232	m_calibrateIterations.clear();
233	m_state = INTERNALSTATE_CALIBRATING;
234}
235
236void TheilSenCalibrator::clear (const CalibratorParameters& params)
237{
238	m_params = params;
239	clear();
240}
241
242TheilSenCalibrator::State TheilSenCalibrator::getState (void) const
243{
244	if (m_state == INTERNALSTATE_FINISHED)
245		return STATE_FINISHED;
246	else
247	{
248		DE_ASSERT(m_state == INTERNALSTATE_CALIBRATING || !m_measureState.isDone());
249		return m_measureState.isDone() ? STATE_RECOMPUTE_PARAMS : STATE_MEASURE;
250	}
251}
252
253void TheilSenCalibrator::recordIteration (deUint64 iterationTime)
254{
255	DE_ASSERT((m_state == INTERNALSTATE_CALIBRATING || m_state == INTERNALSTATE_RUNNING) && !m_measureState.isDone());
256	m_measureState.frameTimes.push_back(iterationTime);
257
258	if (m_state == INTERNALSTATE_RUNNING && m_measureState.isDone())
259		m_state = INTERNALSTATE_FINISHED;
260}
261
262void TheilSenCalibrator::recomputeParameters (void)
263{
264	DE_ASSERT(m_state == INTERNALSTATE_CALIBRATING);
265	DE_ASSERT(m_measureState.isDone());
266
267	// Minimum and maximum acceptable frame times.
268	const float		minGoodFrameTimeUs	= m_params.targetFrameTimeUs * 0.95f;
269	const float		maxGoodFrameTimeUs	= m_params.targetFrameTimeUs * 1.15f;
270
271	const int		numIterations		= (int)m_calibrateIterations.size();
272
273	// Record frame time.
274	if (numIterations > 0)
275	{
276		m_calibrateIterations.back().frameTime = (float)((double)m_measureState.getTotalTime() / (double)m_measureState.frameTimes.size());
277
278		// Check if we're good enough to stop calibrating.
279		{
280			bool endCalibration = false;
281
282			// Is the maximum calibration iteration limit reached?
283			endCalibration = endCalibration || (int)m_calibrateIterations.size() >= m_params.maxCalibrateIterations;
284
285			// Do a few past iterations have frame time in acceptable range?
286			{
287				const int numRelevantPastIterations = 2;
288
289				if (!endCalibration && (int)m_calibrateIterations.size() >= numRelevantPastIterations)
290				{
291					const CalibrateIteration* const		past			= &m_calibrateIterations[m_calibrateIterations.size() - numRelevantPastIterations];
292					bool								allInGoodRange	= true;
293
294					for (int i = 0; i < numRelevantPastIterations && allInGoodRange; i++)
295					{
296						const float frameTimeUs = past[i].frameTime;
297						if (!de::inRange(frameTimeUs, minGoodFrameTimeUs, maxGoodFrameTimeUs))
298							allInGoodRange = false;
299					}
300
301					endCalibration = endCalibration || allInGoodRange;
302				}
303			}
304
305			// Do a few past iterations have similar-enough call counts?
306			{
307				const int numRelevantPastIterations = 3;
308				if (!endCalibration && (int)m_calibrateIterations.size() >= numRelevantPastIterations)
309				{
310					const CalibrateIteration* const		past			= &m_calibrateIterations[m_calibrateIterations.size() - numRelevantPastIterations];
311					int									minCallCount	= std::numeric_limits<int>::max();
312					int									maxCallCount	= std::numeric_limits<int>::min();
313
314					for (int i = 0; i < numRelevantPastIterations; i++)
315					{
316						minCallCount = de::min(minCallCount, past[i].numDrawCalls);
317						maxCallCount = de::max(maxCallCount, past[i].numDrawCalls);
318					}
319
320					if ((float)(maxCallCount - minCallCount) <= (float)minCallCount * 0.1f)
321						endCalibration = true;
322				}
323			}
324
325			// Is call count just 1, and frame time still way too high?
326			endCalibration = endCalibration || (m_calibrateIterations.back().numDrawCalls == 1 && m_calibrateIterations.back().frameTime > m_params.targetFrameTimeUs*2.0f);
327
328			if (endCalibration)
329			{
330				const int	minFrames			= 10;
331				const int	maxFrames			= 60;
332				int			numMeasureFrames	= deClamp32(deRoundFloatToInt32(m_params.targetMeasureDurationUs / m_calibrateIterations.back().frameTime), minFrames, maxFrames);
333
334				m_state = INTERNALSTATE_RUNNING;
335				m_measureState.start(numMeasureFrames, m_params.calibrateIterationShortcutThreshold, m_calibrateIterations.back().numDrawCalls);
336				return;
337			}
338		}
339	}
340
341	DE_ASSERT(m_state == INTERNALSTATE_CALIBRATING);
342
343	// Estimate new call count.
344	{
345		int newCallCount;
346
347		if (numIterations == 0)
348			newCallCount = m_params.numInitialCalls;
349		else
350		{
351			vector<Vec2> dataPoints;
352			for (int i = 0; i < numIterations; i++)
353			{
354				if (m_calibrateIterations[i].numDrawCalls == 1 || m_calibrateIterations[i].frameTime > m_params.frameTimeCapUs*1.05f) // Only account for measurements not too near the cap.
355					dataPoints.push_back(Vec2((float)m_calibrateIterations[i].numDrawCalls, m_calibrateIterations[i].frameTime));
356			}
357
358			if (numIterations == 1)
359				dataPoints.push_back(Vec2(0.0f, 0.0f)); // If there's just one measurement so far, this will help in getting the next estimate.
360
361			{
362				const float				targetFrameTimeUs	= m_params.targetFrameTimeUs;
363				const float				coeffEpsilon		= 0.001f; // Coefficient must be large enough (and positive) to be considered sensible.
364
365				const LineParameters	estimatorLine		= theilSenLinearRegression(dataPoints);
366
367				int						prevMaxCalls		= 0;
368
369				// Find the maximum of the past call counts.
370				for (int i = 0; i < numIterations; i++)
371					prevMaxCalls = de::max(prevMaxCalls, m_calibrateIterations[i].numDrawCalls);
372
373				if (estimatorLine.coefficient < coeffEpsilon) // Coefficient not good for sensible estimation; increase call count enough to get a reasonably different value.
374					newCallCount = 2*prevMaxCalls;
375				else
376				{
377					// Solve newCallCount such that approximately targetFrameTime = offset + coefficient*newCallCount.
378					newCallCount = (int)((targetFrameTimeUs - estimatorLine.offset) / estimatorLine.coefficient + 0.5f);
379
380					// We should generally prefer FPS counts below the target rather than above (i.e. higher frame times rather than lower).
381					if (estimatorLine.offset + estimatorLine.coefficient*(float)newCallCount < minGoodFrameTimeUs)
382						newCallCount++;
383				}
384
385				// Make sure we have at least minimum amount of calls, and don't allow increasing call count too much in one iteration.
386				newCallCount = de::clamp(newCallCount, 1, prevMaxCalls*10);
387			}
388		}
389
390		m_measureState.start(m_params.maxCalibrateIterationFrames, m_params.calibrateIterationShortcutThreshold, newCallCount);
391		m_calibrateIterations.push_back(CalibrateIteration(newCallCount, 0.0f));
392	}
393}
394
395void logCalibrationInfo (tcu::TestLog& log, const TheilSenCalibrator& calibrator)
396{
397	const CalibratorParameters&				params				= calibrator.getParameters();
398	const std::vector<CalibrateIteration>&	calibrateIterations	= calibrator.getCalibrationInfo();
399
400	// Write out default calibration info.
401
402	log << TestLog::Section("CalibrationInfo", "Calibration Info")
403		<< TestLog::Message  << "Target frame time: " << params.targetFrameTimeUs << " us (" << 1000000 / params.targetFrameTimeUs << " fps)" << TestLog::EndMessage;
404
405	for (int iterNdx = 0; iterNdx < (int)calibrateIterations.size(); iterNdx++)
406	{
407		log << TestLog::Message << "  iteration " << iterNdx << ": " << calibrateIterations[iterNdx].numDrawCalls << " calls => "
408								<< de::floatToString(calibrateIterations[iterNdx].frameTime, 2) << " us ("
409								<< de::floatToString(1000000.0f / calibrateIterations[iterNdx].frameTime, 2) << " fps)" << TestLog::EndMessage;
410	}
411	log << TestLog::Integer("CallCount",	"Calibrated call count",	"",	QP_KEY_TAG_NONE, calibrator.getMeasureState().numDrawCalls)
412		<< TestLog::Integer("FrameCount",	"Calibrated frame count",	"", QP_KEY_TAG_NONE, (int)calibrator.getMeasureState().frameTimes.size());
413	log << TestLog::EndSection;
414}
415
416} // gls
417} // deqp
418