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