1 //
2 // Copyright 2014 The ANGLE Project Authors. All rights reserved.
3 // Use of this source code is governed by a BSD-style license that can be
4 // found in the LICENSE file.
5 //
6 // DrawCallPerf:
7 // Performance tests for ANGLE draw call overhead.
8 //
9
10 #include "ANGLEPerfTest.h"
11 #include "DrawCallPerfParams.h"
12 #include "common/PackedEnums.h"
13 #include "test_utils/draw_call_perf_utils.h"
14 #include "util/shader_utils.h"
15
16 namespace
17 {
18 enum class StateChange
19 {
20 NoChange,
21 VertexAttrib,
22 VertexBuffer,
23 ManyVertexBuffers,
24 Texture,
25 Program,
26 VertexBufferCycle,
27 Scissor,
28 ManyTextureDraw,
29 Uniform,
30 InvalidEnum,
31 EnumCount = InvalidEnum,
32 };
33
34 constexpr size_t kCycleVBOPoolSize = 200;
35 constexpr size_t kManyTexturesCount = 8;
36
37 struct DrawArraysPerfParams : public DrawCallPerfParams
38 {
39 DrawArraysPerfParams() = default;
DrawArraysPerfParams__anon4ef89e1b0111::DrawArraysPerfParams40 DrawArraysPerfParams(const DrawCallPerfParams &base) : DrawCallPerfParams(base) {}
41
42 std::string story() const override;
43
44 StateChange stateChange = StateChange::NoChange;
45 };
46
story() const47 std::string DrawArraysPerfParams::story() const
48 {
49 std::stringstream strstr;
50
51 strstr << DrawCallPerfParams::story();
52
53 switch (stateChange)
54 {
55 case StateChange::VertexAttrib:
56 strstr << "_attrib_change";
57 break;
58 case StateChange::VertexBuffer:
59 strstr << "_vbo_change";
60 break;
61 case StateChange::ManyVertexBuffers:
62 strstr << "_manyvbos_change";
63 break;
64 case StateChange::Texture:
65 strstr << "_tex_change";
66 break;
67 case StateChange::Program:
68 strstr << "_prog_change";
69 break;
70 case StateChange::VertexBufferCycle:
71 strstr << "_vbo_cycle";
72 break;
73 case StateChange::Scissor:
74 strstr << "_scissor_change";
75 break;
76 case StateChange::ManyTextureDraw:
77 strstr << "_many_tex_draw";
78 break;
79 case StateChange::Uniform:
80 strstr << "_uniform";
81 break;
82 default:
83 break;
84 }
85
86 return strstr.str();
87 }
88
operator <<(std::ostream & os,const DrawArraysPerfParams & params)89 std::ostream &operator<<(std::ostream &os, const DrawArraysPerfParams ¶ms)
90 {
91 os << params.backendAndStory().substr(1);
92 return os;
93 }
94
CreateSimpleTexture2D()95 GLuint CreateSimpleTexture2D()
96 {
97 // Use tightly packed data
98 glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
99
100 // Generate a texture object
101 GLuint texture;
102 glGenTextures(1, &texture);
103
104 // Bind the texture object
105 glBindTexture(GL_TEXTURE_2D, texture);
106
107 // Load the texture: 2x2 Image, 3 bytes per pixel (R, G, B)
108 constexpr size_t width = 2;
109 constexpr size_t height = 2;
110 GLubyte pixels[width * height * 4] = {
111 255, 0, 0, 0, // Red
112 0, 255, 0, 0, // Green
113 0, 0, 255, 0, // Blue
114 255, 255, 0, 0 // Yellow
115 };
116 glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
117
118 // Set the filtering mode
119 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
120 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
121
122 return texture;
123 }
124
125 class DrawCallPerfBenchmark : public ANGLERenderTest,
126 public ::testing::WithParamInterface<DrawArraysPerfParams>
127 {
128 public:
129 DrawCallPerfBenchmark();
130
131 void initializeBenchmark() override;
132 void destroyBenchmark() override;
133 void drawBenchmark() override;
134
135 private:
136 GLuint mProgram1 = 0;
137 GLuint mProgram2 = 0;
138 GLuint mProgram3 = 0;
139 GLuint mBuffer1 = 0;
140 GLuint mBuffer2 = 0;
141 GLuint mFBO = 0;
142 GLuint mFBOTexture = 0;
143 std::vector<GLuint> mTextures;
144 int mNumTris = GetParam().numTris;
145 std::vector<GLuint> mVBOPool;
146 size_t mCurrentVBO = 0;
147 };
148
DrawCallPerfBenchmark()149 DrawCallPerfBenchmark::DrawCallPerfBenchmark() : ANGLERenderTest("DrawCallPerf", GetParam()) {}
150
initializeBenchmark()151 void DrawCallPerfBenchmark::initializeBenchmark()
152 {
153 const auto ¶ms = GetParam();
154
155 if (params.stateChange == StateChange::Texture)
156 {
157 mProgram1 = SetupSimpleTextureProgram();
158 ASSERT_NE(0u, mProgram1);
159 }
160 else if (params.stateChange == StateChange::ManyTextureDraw)
161 {
162 mProgram3 = SetupEightTextureProgram();
163 ASSERT_NE(0u, mProgram3);
164 }
165 else if (params.stateChange == StateChange::Program)
166 {
167 mProgram1 = SetupSimpleTextureProgram();
168 mProgram2 = SetupDoubleTextureProgram();
169 ASSERT_NE(0u, mProgram1);
170 ASSERT_NE(0u, mProgram2);
171 }
172 else if (params.stateChange == StateChange::ManyVertexBuffers)
173 {
174 constexpr char kVS[] = R"(attribute vec2 vPosition;
175 attribute vec2 v0;
176 attribute vec2 v1;
177 attribute vec2 v2;
178 attribute vec2 v3;
179 const float scale = 0.5;
180 const float offset = -0.5;
181
182 varying vec2 v;
183
184 void main()
185 {
186 gl_Position = vec4(vPosition * vec2(scale) + vec2(offset), 0, 1);
187 v = (v0 + v1 + v2 + v3) * 0.25;
188 })";
189
190 constexpr char kFS[] = R"(precision mediump float;
191 varying vec2 v;
192 void main()
193 {
194 gl_FragColor = vec4(v, 0, 1);
195 })";
196
197 mProgram1 = CompileProgram(kVS, kFS);
198 ASSERT_NE(0u, mProgram1);
199 glBindAttribLocation(mProgram1, 1, "v0");
200 glBindAttribLocation(mProgram1, 2, "v1");
201 glBindAttribLocation(mProgram1, 3, "v2");
202 glBindAttribLocation(mProgram1, 4, "v3");
203 glEnableVertexAttribArray(1);
204 glEnableVertexAttribArray(2);
205 glEnableVertexAttribArray(3);
206 glEnableVertexAttribArray(4);
207 }
208 else if (params.stateChange == StateChange::VertexBufferCycle)
209 {
210 mProgram1 = SetupSimpleDrawProgram();
211 ASSERT_NE(0u, mProgram1);
212
213 for (size_t bufferIndex = 0; bufferIndex < kCycleVBOPoolSize; ++bufferIndex)
214 {
215 GLuint buffer = Create2DTriangleBuffer(mNumTris, GL_STATIC_DRAW);
216 mVBOPool.push_back(buffer);
217 }
218 }
219 else if (params.stateChange == StateChange::Uniform)
220 {
221 constexpr char kVS[] = R"(attribute vec2 vPosition;
222 void main()
223 {
224 gl_Position = vec4(vPosition, 0, 1);
225 })";
226
227 constexpr char kFS[] = R"(precision mediump float;
228 uniform vec4 uni;
229 void main()
230 {
231 gl_FragColor = uni;
232 })";
233
234 mProgram1 = CompileProgram(kVS, kFS);
235 ASSERT_NE(0u, mProgram1);
236 }
237 else
238 {
239 mProgram1 = SetupSimpleDrawProgram();
240 ASSERT_NE(0u, mProgram1);
241 }
242
243 // Re-link program to ensure the attrib bindings are used.
244 if (mProgram1)
245 {
246 glBindAttribLocation(mProgram1, 0, "vPosition");
247 glLinkProgram(mProgram1);
248 glUseProgram(mProgram1);
249 }
250
251 if (mProgram2)
252 {
253 glBindAttribLocation(mProgram2, 0, "vPosition");
254 glLinkProgram(mProgram2);
255 }
256
257 if (mProgram3)
258 {
259 glBindAttribLocation(mProgram3, 0, "vPosition");
260 glLinkProgram(mProgram3);
261 }
262
263 glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
264
265 mBuffer1 = Create2DTriangleBuffer(mNumTris, GL_STATIC_DRAW);
266 mBuffer2 = Create2DTriangleBuffer(mNumTris, GL_STATIC_DRAW);
267
268 glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, 0);
269 glEnableVertexAttribArray(0);
270
271 // Set the viewport
272 glViewport(0, 0, getWindow()->getWidth(), getWindow()->getHeight());
273
274 if (params.surfaceType == SurfaceType::Offscreen)
275 {
276 CreateColorFBO(getWindow()->getWidth(), getWindow()->getHeight(), &mFBOTexture, &mFBO);
277 }
278
279 for (size_t i = 0; i < kManyTexturesCount; ++i)
280 {
281 mTextures.emplace_back(CreateSimpleTexture2D());
282 }
283
284 if (params.stateChange == StateChange::Program)
285 {
286 // Bind the textures as appropriate, they are not modified during the test.
287 GLint program1Tex1Loc = glGetUniformLocation(mProgram1, "tex");
288 GLint program2Tex1Loc = glGetUniformLocation(mProgram2, "tex1");
289 GLint program2Tex2Loc = glGetUniformLocation(mProgram2, "tex2");
290
291 glUseProgram(mProgram1);
292 glUniform1i(program1Tex1Loc, 0);
293
294 glUseProgram(mProgram2);
295 glUniform1i(program2Tex1Loc, 0);
296 glUniform1i(program2Tex2Loc, 1);
297 }
298
299 if (params.stateChange == StateChange::ManyTextureDraw)
300 {
301 GLint program3TexLocs[kManyTexturesCount];
302
303 for (size_t i = 0; i < mTextures.size(); ++i)
304 {
305 char stringBuffer[8];
306 snprintf(stringBuffer, sizeof(stringBuffer), "tex%zu", i);
307 program3TexLocs[i] = glGetUniformLocation(mProgram3, stringBuffer);
308 }
309
310 glUseProgram(mProgram3);
311 for (size_t i = 0; i < mTextures.size(); ++i)
312 {
313 glUniform1i(program3TexLocs[i], i);
314 }
315
316 for (size_t i = 0; i < mTextures.size(); ++i)
317 {
318 glActiveTexture(GL_TEXTURE0 + i);
319 glBindTexture(GL_TEXTURE_2D, mTextures[i]);
320 }
321 }
322
323 ASSERT_GL_NO_ERROR();
324 }
325
destroyBenchmark()326 void DrawCallPerfBenchmark::destroyBenchmark()
327 {
328 glDeleteProgram(mProgram1);
329 glDeleteProgram(mProgram2);
330 glDeleteProgram(mProgram3);
331 glDeleteBuffers(1, &mBuffer1);
332 glDeleteBuffers(1, &mBuffer2);
333 glDeleteTextures(1, &mFBOTexture);
334 glDeleteTextures(mTextures.size(), mTextures.data());
335 glDeleteFramebuffers(1, &mFBO);
336
337 if (!mVBOPool.empty())
338 {
339 glDeleteBuffers(mVBOPool.size(), mVBOPool.data());
340 }
341 }
342
ClearThenDraw(unsigned int iterations,GLsizei numElements)343 void ClearThenDraw(unsigned int iterations, GLsizei numElements)
344 {
345 glClear(GL_COLOR_BUFFER_BIT);
346
347 for (unsigned int it = 0; it < iterations; it++)
348 {
349 glDrawArrays(GL_TRIANGLES, 0, numElements);
350 }
351 }
352
JustDraw(unsigned int iterations,GLsizei numElements)353 void JustDraw(unsigned int iterations, GLsizei numElements)
354 {
355 for (unsigned int it = 0; it < iterations; it++)
356 {
357 glDrawArrays(GL_TRIANGLES, 0, numElements);
358 }
359 }
360
361 template <int kArrayBufferCount>
ChangeVertexAttribThenDraw(unsigned int iterations,GLsizei numElements,GLuint buffer)362 void ChangeVertexAttribThenDraw(unsigned int iterations, GLsizei numElements, GLuint buffer)
363 {
364 glBindBuffer(GL_ARRAY_BUFFER, buffer);
365 for (unsigned int it = 0; it < iterations; it++)
366 {
367 for (int arrayIndex = 0; arrayIndex < kArrayBufferCount; ++arrayIndex)
368 {
369 glVertexAttribPointer(arrayIndex, 2, GL_FLOAT, GL_FALSE, 0, 0);
370 }
371 glDrawArrays(GL_TRIANGLES, 0, numElements);
372
373 for (int arrayIndex = 0; arrayIndex < kArrayBufferCount; ++arrayIndex)
374 {
375 glVertexAttribPointer(arrayIndex, 4, GL_UNSIGNED_BYTE, GL_TRUE, 0, 0);
376 }
377 glDrawArrays(GL_TRIANGLES, 0, numElements);
378 }
379 }
380 template <int kArrayBufferCount>
ChangeArrayBuffersThenDraw(unsigned int iterations,GLsizei numElements,GLuint buffer1,GLuint buffer2)381 void ChangeArrayBuffersThenDraw(unsigned int iterations,
382 GLsizei numElements,
383 GLuint buffer1,
384 GLuint buffer2)
385 {
386 for (unsigned int it = 0; it < iterations; it++)
387 {
388 glBindBuffer(GL_ARRAY_BUFFER, buffer1);
389 for (int arrayIndex = 0; arrayIndex < kArrayBufferCount; ++arrayIndex)
390 {
391 glVertexAttribPointer(arrayIndex, 2, GL_FLOAT, GL_FALSE, 0, 0);
392 }
393 glDrawArrays(GL_TRIANGLES, 0, numElements);
394
395 glBindBuffer(GL_ARRAY_BUFFER, buffer2);
396 for (int arrayIndex = 0; arrayIndex < kArrayBufferCount; ++arrayIndex)
397 {
398 glVertexAttribPointer(arrayIndex, 2, GL_FLOAT, GL_FALSE, 0, 0);
399 }
400 glDrawArrays(GL_TRIANGLES, 0, numElements);
401 }
402 }
403
ChangeTextureThenDraw(unsigned int iterations,GLsizei numElements,GLuint texture1,GLuint texture2)404 void ChangeTextureThenDraw(unsigned int iterations,
405 GLsizei numElements,
406 GLuint texture1,
407 GLuint texture2)
408 {
409 for (unsigned int it = 0; it < iterations; it++)
410 {
411 glBindTexture(GL_TEXTURE_2D, texture1);
412 glDrawArrays(GL_TRIANGLES, 0, numElements);
413
414 glBindTexture(GL_TEXTURE_2D, texture2);
415 glDrawArrays(GL_TRIANGLES, 0, numElements);
416 }
417 }
418
ChangeProgramThenDraw(unsigned int iterations,GLsizei numElements,GLuint program1,GLuint program2)419 void ChangeProgramThenDraw(unsigned int iterations,
420 GLsizei numElements,
421 GLuint program1,
422 GLuint program2)
423 {
424 for (unsigned int it = 0; it < iterations; it++)
425 {
426 glUseProgram(program1);
427 glDrawArrays(GL_TRIANGLES, 0, numElements);
428
429 glUseProgram(program2);
430 glDrawArrays(GL_TRIANGLES, 0, numElements);
431 }
432 }
433
CycleVertexBufferThenDraw(unsigned int iterations,GLsizei numElements,const std::vector<GLuint> & vbos,size_t * currentVBO)434 void CycleVertexBufferThenDraw(unsigned int iterations,
435 GLsizei numElements,
436 const std::vector<GLuint> &vbos,
437 size_t *currentVBO)
438 {
439 for (unsigned int it = 0; it < iterations; it++)
440 {
441 GLuint vbo = vbos[*currentVBO];
442 glBindBuffer(GL_ARRAY_BUFFER, vbo);
443 glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, 0);
444 glDrawArrays(GL_TRIANGLES, 0, numElements);
445 *currentVBO = (*currentVBO + 1) % vbos.size();
446 }
447 }
448
ChangeScissorThenDraw(unsigned int iterations,GLsizei numElements,unsigned int windowWidth,unsigned int windowHeight)449 void ChangeScissorThenDraw(unsigned int iterations,
450 GLsizei numElements,
451 unsigned int windowWidth,
452 unsigned int windowHeight)
453 {
454 // Change scissor as such:
455 //
456 // - Start with a narrow vertical bar:
457 //
458 // Scissor
459 // |
460 // V
461 // +-----+-+-----+
462 // | | | | <-- Window
463 // | | | |
464 // | | | |
465 // | | | |
466 // | | | |
467 // | | | |
468 // +-----+-+-----+
469 //
470 // - Gradually reduce height and increase width, to end up with a narrow horizontal bar:
471 //
472 // +-------------+
473 // | |
474 // | |
475 // +-------------+ <-- Scissor
476 // +-------------+
477 // | |
478 // | |
479 // +-------------+
480 //
481 // - If more iterations left, restart, but shift the initial bar left to cover more area:
482 //
483 // +---+-+-------+ +-------------+
484 // | | | | | |
485 // | | | | +-------------+
486 // | | | | ---> | |
487 // | | | | | |
488 // | | | | +-------------+
489 // | | | | | |
490 // +---+-+-------+ +-------------+
491 //
492 // +-+-+---------+ +-------------+
493 // | | | | +-------------+
494 // | | | | | |
495 // | | | | ---> | |
496 // | | | | | |
497 // | | | | | |
498 // | | | | +-------------+
499 // +-+-+---------+ +-------------+
500
501 glEnable(GL_SCISSOR_TEST);
502
503 constexpr unsigned int kScissorStep = 2;
504 unsigned int scissorX = windowWidth / 2 - 1;
505 unsigned int scissorY = 0;
506 unsigned int scissorWidth = 2;
507 unsigned int scissorHeight = windowHeight;
508 unsigned int scissorPatternIteration = 0;
509
510 for (unsigned int it = 0; it < iterations; it++)
511 {
512 glScissor(scissorX, scissorY, scissorWidth, scissorHeight);
513 glDrawArrays(GL_TRIANGLES, 0, numElements);
514
515 if (scissorX < kScissorStep || scissorHeight < kScissorStep * 2)
516 {
517 ++scissorPatternIteration;
518 scissorX = windowWidth / 2 - 1 - scissorPatternIteration * 2;
519 scissorY = 0;
520 scissorWidth = 2;
521 scissorHeight = windowHeight;
522 }
523 else
524 {
525 scissorX -= kScissorStep;
526 scissorY += kScissorStep;
527 scissorWidth += kScissorStep * 2;
528 scissorHeight -= kScissorStep * 2;
529 }
530 }
531 }
532
DrawWithEightTextures(unsigned int iterations,GLsizei numElements,std::vector<GLuint> textures)533 void DrawWithEightTextures(unsigned int iterations,
534 GLsizei numElements,
535 std::vector<GLuint> textures)
536 {
537 for (unsigned int it = 0; it < iterations; it++)
538 {
539 for (size_t i = 0; i < textures.size(); ++i)
540 {
541 glActiveTexture(GL_TEXTURE0 + i);
542 size_t index = (it + i) % textures.size();
543 glBindTexture(GL_TEXTURE_2D, textures[index]);
544 }
545
546 glDrawArrays(GL_TRIANGLES, 0, numElements);
547 }
548 }
549
UpdateUniformThenDraw(unsigned int iterations,GLsizei numElements)550 void UpdateUniformThenDraw(unsigned int iterations, GLsizei numElements)
551 {
552 for (unsigned int it = 0; it < iterations; it++)
553 {
554 float f = static_cast<float>(it) / static_cast<float>(iterations);
555 glUniform4f(0, f, f + 0.1f, f + 0.2f, f + 0.3f);
556 glDrawArrays(GL_TRIANGLES, 0, numElements);
557 }
558 }
559
drawBenchmark()560 void DrawCallPerfBenchmark::drawBenchmark()
561 {
562 // This workaround fixes a huge queue of graphics commands accumulating on the GL
563 // back-end. The GL back-end doesn't have a proper NULL device at the moment.
564 // TODO(jmadill): Remove this when/if we ever get a proper OpenGL NULL device.
565 const auto &eglParams = GetParam().eglParameters;
566 const auto ¶ms = GetParam();
567 GLsizei numElements = static_cast<GLsizei>(3 * mNumTris);
568
569 switch (params.stateChange)
570 {
571 case StateChange::VertexAttrib:
572 ChangeVertexAttribThenDraw<1>(params.iterationsPerStep, numElements, mBuffer1);
573 break;
574 case StateChange::VertexBuffer:
575 ChangeArrayBuffersThenDraw<1>(params.iterationsPerStep, numElements, mBuffer1,
576 mBuffer2);
577 break;
578 case StateChange::ManyVertexBuffers:
579 ChangeArrayBuffersThenDraw<5>(params.iterationsPerStep, numElements, mBuffer1,
580 mBuffer2);
581 break;
582 case StateChange::Texture:
583 ChangeTextureThenDraw(params.iterationsPerStep, numElements, mTextures[0],
584 mTextures[1]);
585 break;
586 case StateChange::Program:
587 ChangeProgramThenDraw(params.iterationsPerStep, numElements, mProgram1, mProgram2);
588 break;
589 case StateChange::NoChange:
590 if (eglParams.deviceType != EGL_PLATFORM_ANGLE_DEVICE_TYPE_NULL_ANGLE ||
591 (eglParams.renderer != EGL_PLATFORM_ANGLE_TYPE_OPENGL_ANGLE &&
592 eglParams.renderer != EGL_PLATFORM_ANGLE_TYPE_OPENGLES_ANGLE))
593 {
594 ClearThenDraw(params.iterationsPerStep, numElements);
595 }
596 else
597 {
598 JustDraw(params.iterationsPerStep, numElements);
599 }
600 break;
601 case StateChange::VertexBufferCycle:
602 CycleVertexBufferThenDraw(params.iterationsPerStep, numElements, mVBOPool,
603 &mCurrentVBO);
604 break;
605 case StateChange::Scissor:
606 ChangeScissorThenDraw(params.iterationsPerStep, numElements, getWindow()->getWidth(),
607 getWindow()->getHeight());
608 break;
609 case StateChange::ManyTextureDraw:
610 glUseProgram(mProgram3);
611 DrawWithEightTextures(params.iterationsPerStep, numElements, mTextures);
612 break;
613 case StateChange::Uniform:
614 UpdateUniformThenDraw(params.iterationsPerStep, numElements);
615 break;
616 case StateChange::InvalidEnum:
617 ADD_FAILURE() << "Invalid state change.";
618 break;
619 }
620
621 ASSERT_GL_NO_ERROR();
622 }
623
TEST_P(DrawCallPerfBenchmark,Run)624 TEST_P(DrawCallPerfBenchmark, Run)
625 {
626 run();
627 }
628
629 using namespace params;
630
CombineStateChange(const DrawArraysPerfParams & in,StateChange stateChange)631 DrawArraysPerfParams CombineStateChange(const DrawArraysPerfParams &in, StateChange stateChange)
632 {
633 DrawArraysPerfParams out = in;
634 out.stateChange = stateChange;
635
636 // Crank up iteration count to ensure we cycle through all VBs before a swap.
637 if (stateChange == StateChange::VertexBufferCycle)
638 {
639 out.iterationsPerStep = kCycleVBOPoolSize * 2;
640 }
641
642 return out;
643 }
644
645 using P = DrawArraysPerfParams;
646
647 std::vector<P> gTestsWithStateChange =
648 CombineWithValues({P()}, angle::AllEnums<StateChange>(), CombineStateChange);
649 std::vector<P> gTestsWithRenderer =
650 CombineWithFuncs(gTestsWithStateChange, {D3D11<P>, GL<P>, Metal<P>, Vulkan<P>, WGL<P>});
651 std::vector<P> gTestsWithDevice =
652 CombineWithFuncs(gTestsWithRenderer, {Passthrough<P>, Offscreen<P>, NullDevice<P>});
653
654 ANGLE_INSTANTIATE_TEST_ARRAY(DrawCallPerfBenchmark, gTestsWithDevice);
655
656 } // anonymous namespace
657