| // |
| // Copyright 2014 The ANGLE Project Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| // |
| // DrawCallPerf: |
| // Performance tests for ANGLE draw call overhead. |
| // |
| |
| #include "ANGLEPerfTest.h" |
| #include "DrawCallPerfParams.h" |
| #include "common/PackedEnums.h" |
| #include "test_utils/draw_call_perf_utils.h" |
| #include "util/shader_utils.h" |
| |
| namespace |
| { |
| enum class StateChange |
| { |
| NoChange, |
| VertexAttrib, |
| VertexBuffer, |
| ManyVertexBuffers, |
| Texture, |
| Program, |
| VertexBufferCycle, |
| Scissor, |
| ManyTextureDraw, |
| InvalidEnum, |
| EnumCount = InvalidEnum, |
| }; |
| |
| constexpr size_t kCycleVBOPoolSize = 200; |
| constexpr size_t kManyTexturesCount = 8; |
| |
| struct DrawArraysPerfParams : public DrawCallPerfParams |
| { |
| DrawArraysPerfParams() = default; |
| DrawArraysPerfParams(const DrawCallPerfParams &base) : DrawCallPerfParams(base) {} |
| |
| std::string story() const override; |
| |
| StateChange stateChange = StateChange::NoChange; |
| }; |
| |
| std::string DrawArraysPerfParams::story() const |
| { |
| std::stringstream strstr; |
| |
| strstr << DrawCallPerfParams::story(); |
| |
| switch (stateChange) |
| { |
| case StateChange::VertexAttrib: |
| strstr << "_attrib_change"; |
| break; |
| case StateChange::VertexBuffer: |
| strstr << "_vbo_change"; |
| break; |
| case StateChange::ManyVertexBuffers: |
| strstr << "_manyvbos_change"; |
| break; |
| case StateChange::Texture: |
| strstr << "_tex_change"; |
| break; |
| case StateChange::Program: |
| strstr << "_prog_change"; |
| break; |
| case StateChange::VertexBufferCycle: |
| strstr << "_vbo_cycle"; |
| break; |
| case StateChange::Scissor: |
| strstr << "_scissor_change"; |
| break; |
| case StateChange::ManyTextureDraw: |
| strstr << "_many_tex_draw"; |
| break; |
| default: |
| break; |
| } |
| |
| return strstr.str(); |
| } |
| |
| std::ostream &operator<<(std::ostream &os, const DrawArraysPerfParams ¶ms) |
| { |
| os << params.backendAndStory().substr(1); |
| return os; |
| } |
| |
| GLuint CreateSimpleTexture2D() |
| { |
| // Use tightly packed data |
| glPixelStorei(GL_UNPACK_ALIGNMENT, 1); |
| |
| // Generate a texture object |
| GLuint texture; |
| glGenTextures(1, &texture); |
| |
| // Bind the texture object |
| glBindTexture(GL_TEXTURE_2D, texture); |
| |
| // Load the texture: 2x2 Image, 3 bytes per pixel (R, G, B) |
| constexpr size_t width = 2; |
| constexpr size_t height = 2; |
| GLubyte pixels[width * height * 4] = { |
| 255, 0, 0, 0, // Red |
| 0, 255, 0, 0, // Green |
| 0, 0, 255, 0, // Blue |
| 255, 255, 0, 0 // Yellow |
| }; |
| glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels); |
| |
| // Set the filtering mode |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); |
| glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); |
| |
| return texture; |
| } |
| |
| class DrawCallPerfBenchmark : public ANGLERenderTest, |
| public ::testing::WithParamInterface<DrawArraysPerfParams> |
| { |
| public: |
| DrawCallPerfBenchmark(); |
| |
| void initializeBenchmark() override; |
| void destroyBenchmark() override; |
| void drawBenchmark() override; |
| |
| private: |
| GLuint mProgram1 = 0; |
| GLuint mProgram2 = 0; |
| GLuint mProgram3 = 0; |
| GLuint mBuffer1 = 0; |
| GLuint mBuffer2 = 0; |
| GLuint mFBO = 0; |
| GLuint mFBOTexture = 0; |
| std::vector<GLuint> mTextures; |
| int mNumTris = GetParam().numTris; |
| std::vector<GLuint> mVBOPool; |
| size_t mCurrentVBO = 0; |
| }; |
| |
| DrawCallPerfBenchmark::DrawCallPerfBenchmark() : ANGLERenderTest("DrawCallPerf", GetParam()) {} |
| |
| void DrawCallPerfBenchmark::initializeBenchmark() |
| { |
| const auto ¶ms = GetParam(); |
| |
| if (params.stateChange == StateChange::Texture) |
| { |
| mProgram1 = SetupSimpleTextureProgram(); |
| ASSERT_NE(0u, mProgram1); |
| } |
| else if (params.stateChange == StateChange::ManyTextureDraw) |
| { |
| mProgram3 = SetupEightTextureProgram(); |
| ASSERT_NE(0u, mProgram3); |
| } |
| else if (params.stateChange == StateChange::Program) |
| { |
| mProgram1 = SetupSimpleTextureProgram(); |
| mProgram2 = SetupDoubleTextureProgram(); |
| ASSERT_NE(0u, mProgram1); |
| ASSERT_NE(0u, mProgram2); |
| } |
| else if (params.stateChange == StateChange::ManyVertexBuffers) |
| { |
| constexpr char kVS[] = R"(attribute vec2 vPosition; |
| attribute vec2 v0; |
| attribute vec2 v1; |
| attribute vec2 v2; |
| attribute vec2 v3; |
| const float scale = 0.5; |
| const float offset = -0.5; |
| |
| varying vec2 v; |
| |
| void main() |
| { |
| gl_Position = vec4(vPosition * vec2(scale) + vec2(offset), 0, 1); |
| v = (v0 + v1 + v2 + v3) * 0.25; |
| })"; |
| |
| constexpr char kFS[] = R"(precision mediump float; |
| varying vec2 v; |
| void main() |
| { |
| gl_FragColor = vec4(v, 0, 1); |
| })"; |
| |
| mProgram1 = CompileProgram(kVS, kFS); |
| ASSERT_NE(0u, mProgram1); |
| glBindAttribLocation(mProgram1, 1, "v0"); |
| glBindAttribLocation(mProgram1, 2, "v1"); |
| glBindAttribLocation(mProgram1, 3, "v2"); |
| glBindAttribLocation(mProgram1, 4, "v3"); |
| glEnableVertexAttribArray(1); |
| glEnableVertexAttribArray(2); |
| glEnableVertexAttribArray(3); |
| glEnableVertexAttribArray(4); |
| } |
| else if (params.stateChange == StateChange::VertexBufferCycle) |
| { |
| mProgram1 = SetupSimpleDrawProgram(); |
| ASSERT_NE(0u, mProgram1); |
| |
| for (size_t bufferIndex = 0; bufferIndex < kCycleVBOPoolSize; ++bufferIndex) |
| { |
| GLuint buffer = Create2DTriangleBuffer(mNumTris, GL_STATIC_DRAW); |
| mVBOPool.push_back(buffer); |
| } |
| } |
| else |
| { |
| mProgram1 = SetupSimpleDrawProgram(); |
| ASSERT_NE(0u, mProgram1); |
| } |
| |
| // Re-link program to ensure the attrib bindings are used. |
| if (mProgram1) |
| { |
| glBindAttribLocation(mProgram1, 0, "vPosition"); |
| glLinkProgram(mProgram1); |
| glUseProgram(mProgram1); |
| } |
| |
| if (mProgram2) |
| { |
| glBindAttribLocation(mProgram2, 0, "vPosition"); |
| glLinkProgram(mProgram2); |
| } |
| |
| if (mProgram3) |
| { |
| glBindAttribLocation(mProgram3, 0, "vPosition"); |
| glLinkProgram(mProgram3); |
| } |
| |
| glClearColor(0.0f, 0.0f, 0.0f, 0.0f); |
| |
| mBuffer1 = Create2DTriangleBuffer(mNumTris, GL_STATIC_DRAW); |
| mBuffer2 = Create2DTriangleBuffer(mNumTris, GL_STATIC_DRAW); |
| |
| glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, 0); |
| glEnableVertexAttribArray(0); |
| |
| // Set the viewport |
| glViewport(0, 0, getWindow()->getWidth(), getWindow()->getHeight()); |
| |
| if (params.surfaceType == SurfaceType::Offscreen) |
| { |
| CreateColorFBO(getWindow()->getWidth(), getWindow()->getHeight(), &mFBOTexture, &mFBO); |
| } |
| |
| for (size_t i = 0; i < kManyTexturesCount; ++i) |
| { |
| mTextures.emplace_back(CreateSimpleTexture2D()); |
| } |
| |
| if (params.stateChange == StateChange::Program) |
| { |
| // Bind the textures as appropriate, they are not modified during the test. |
| GLint program1Tex1Loc = glGetUniformLocation(mProgram1, "tex"); |
| GLint program2Tex1Loc = glGetUniformLocation(mProgram2, "tex1"); |
| GLint program2Tex2Loc = glGetUniformLocation(mProgram2, "tex2"); |
| |
| glUseProgram(mProgram1); |
| glUniform1i(program1Tex1Loc, 0); |
| |
| glUseProgram(mProgram2); |
| glUniform1i(program2Tex1Loc, 0); |
| glUniform1i(program2Tex2Loc, 1); |
| } |
| |
| if (params.stateChange == StateChange::ManyTextureDraw) |
| { |
| GLint program3TexLocs[kManyTexturesCount]; |
| |
| for (size_t i = 0; i < mTextures.size(); ++i) |
| { |
| char stringBuffer[8]; |
| snprintf(stringBuffer, sizeof(stringBuffer), "tex%zu", i); |
| program3TexLocs[i] = glGetUniformLocation(mProgram3, stringBuffer); |
| } |
| |
| glUseProgram(mProgram3); |
| for (size_t i = 0; i < mTextures.size(); ++i) |
| { |
| glUniform1i(program3TexLocs[i], i); |
| } |
| |
| for (size_t i = 0; i < mTextures.size(); ++i) |
| { |
| glActiveTexture(GL_TEXTURE0 + i); |
| glBindTexture(GL_TEXTURE_2D, mTextures[i]); |
| } |
| } |
| |
| ASSERT_GL_NO_ERROR(); |
| } |
| |
| void DrawCallPerfBenchmark::destroyBenchmark() |
| { |
| glDeleteProgram(mProgram1); |
| glDeleteProgram(mProgram2); |
| glDeleteProgram(mProgram3); |
| glDeleteBuffers(1, &mBuffer1); |
| glDeleteBuffers(1, &mBuffer2); |
| glDeleteTextures(1, &mFBOTexture); |
| glDeleteTextures(mTextures.size(), mTextures.data()); |
| glDeleteFramebuffers(1, &mFBO); |
| |
| if (!mVBOPool.empty()) |
| { |
| glDeleteBuffers(mVBOPool.size(), mVBOPool.data()); |
| } |
| } |
| |
| void ClearThenDraw(unsigned int iterations, GLsizei numElements) |
| { |
| glClear(GL_COLOR_BUFFER_BIT); |
| |
| for (unsigned int it = 0; it < iterations; it++) |
| { |
| glDrawArrays(GL_TRIANGLES, 0, numElements); |
| } |
| } |
| |
| void JustDraw(unsigned int iterations, GLsizei numElements) |
| { |
| for (unsigned int it = 0; it < iterations; it++) |
| { |
| glDrawArrays(GL_TRIANGLES, 0, numElements); |
| } |
| } |
| |
| template <int kArrayBufferCount> |
| void ChangeVertexAttribThenDraw(unsigned int iterations, GLsizei numElements, GLuint buffer) |
| { |
| glBindBuffer(GL_ARRAY_BUFFER, buffer); |
| for (unsigned int it = 0; it < iterations; it++) |
| { |
| for (int arrayIndex = 0; arrayIndex < kArrayBufferCount; ++arrayIndex) |
| { |
| glVertexAttribPointer(arrayIndex, 2, GL_FLOAT, GL_FALSE, 0, 0); |
| } |
| glDrawArrays(GL_TRIANGLES, 0, numElements); |
| |
| for (int arrayIndex = 0; arrayIndex < kArrayBufferCount; ++arrayIndex) |
| { |
| glVertexAttribPointer(arrayIndex, 4, GL_UNSIGNED_BYTE, GL_TRUE, 0, 0); |
| } |
| glDrawArrays(GL_TRIANGLES, 0, numElements); |
| } |
| } |
| template <int kArrayBufferCount> |
| void ChangeArrayBuffersThenDraw(unsigned int iterations, |
| GLsizei numElements, |
| GLuint buffer1, |
| GLuint buffer2) |
| { |
| for (unsigned int it = 0; it < iterations; it++) |
| { |
| glBindBuffer(GL_ARRAY_BUFFER, buffer1); |
| for (int arrayIndex = 0; arrayIndex < kArrayBufferCount; ++arrayIndex) |
| { |
| glVertexAttribPointer(arrayIndex, 2, GL_FLOAT, GL_FALSE, 0, 0); |
| } |
| glDrawArrays(GL_TRIANGLES, 0, numElements); |
| |
| glBindBuffer(GL_ARRAY_BUFFER, buffer2); |
| for (int arrayIndex = 0; arrayIndex < kArrayBufferCount; ++arrayIndex) |
| { |
| glVertexAttribPointer(arrayIndex, 2, GL_FLOAT, GL_FALSE, 0, 0); |
| } |
| glDrawArrays(GL_TRIANGLES, 0, numElements); |
| } |
| } |
| |
| void ChangeTextureThenDraw(unsigned int iterations, |
| GLsizei numElements, |
| GLuint texture1, |
| GLuint texture2) |
| { |
| for (unsigned int it = 0; it < iterations; it++) |
| { |
| glBindTexture(GL_TEXTURE_2D, texture1); |
| glDrawArrays(GL_TRIANGLES, 0, numElements); |
| |
| glBindTexture(GL_TEXTURE_2D, texture2); |
| glDrawArrays(GL_TRIANGLES, 0, numElements); |
| } |
| } |
| |
| void ChangeProgramThenDraw(unsigned int iterations, |
| GLsizei numElements, |
| GLuint program1, |
| GLuint program2) |
| { |
| for (unsigned int it = 0; it < iterations; it++) |
| { |
| glUseProgram(program1); |
| glDrawArrays(GL_TRIANGLES, 0, numElements); |
| |
| glUseProgram(program2); |
| glDrawArrays(GL_TRIANGLES, 0, numElements); |
| } |
| } |
| |
| void CycleVertexBufferThenDraw(unsigned int iterations, |
| GLsizei numElements, |
| const std::vector<GLuint> &vbos, |
| size_t *currentVBO) |
| { |
| for (unsigned int it = 0; it < iterations; it++) |
| { |
| GLuint vbo = vbos[*currentVBO]; |
| glBindBuffer(GL_ARRAY_BUFFER, vbo); |
| glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, 0); |
| glDrawArrays(GL_TRIANGLES, 0, numElements); |
| *currentVBO = (*currentVBO + 1) % vbos.size(); |
| } |
| } |
| |
| void ChangeScissorThenDraw(unsigned int iterations, |
| GLsizei numElements, |
| unsigned int windowWidth, |
| unsigned int windowHeight) |
| { |
| // Change scissor as such: |
| // |
| // - Start with a narrow vertical bar: |
| // |
| // Scissor |
| // | |
| // V |
| // +-----+-+-----+ |
| // | | | | <-- Window |
| // | | | | |
| // | | | | |
| // | | | | |
| // | | | | |
| // | | | | |
| // +-----+-+-----+ |
| // |
| // - Gradually reduce height and increase width, to end up with a narrow horizontal bar: |
| // |
| // +-------------+ |
| // | | |
| // | | |
| // +-------------+ <-- Scissor |
| // +-------------+ |
| // | | |
| // | | |
| // +-------------+ |
| // |
| // - If more iterations left, restart, but shift the initial bar left to cover more area: |
| // |
| // +---+-+-------+ +-------------+ |
| // | | | | | | |
| // | | | | +-------------+ |
| // | | | | ---> | | |
| // | | | | | | |
| // | | | | +-------------+ |
| // | | | | | | |
| // +---+-+-------+ +-------------+ |
| // |
| // +-+-+---------+ +-------------+ |
| // | | | | +-------------+ |
| // | | | | | | |
| // | | | | ---> | | |
| // | | | | | | |
| // | | | | | | |
| // | | | | +-------------+ |
| // +-+-+---------+ +-------------+ |
| |
| glEnable(GL_SCISSOR_TEST); |
| |
| constexpr unsigned int kScissorStep = 2; |
| unsigned int scissorX = windowWidth / 2 - 1; |
| unsigned int scissorY = 0; |
| unsigned int scissorWidth = 2; |
| unsigned int scissorHeight = windowHeight; |
| unsigned int scissorPatternIteration = 0; |
| |
| for (unsigned int it = 0; it < iterations; it++) |
| { |
| glScissor(scissorX, scissorY, scissorWidth, scissorHeight); |
| glDrawArrays(GL_TRIANGLES, 0, numElements); |
| |
| if (scissorX < kScissorStep || scissorHeight < kScissorStep * 2) |
| { |
| ++scissorPatternIteration; |
| scissorX = windowWidth / 2 - 1 - scissorPatternIteration * 2; |
| scissorY = 0; |
| scissorWidth = 2; |
| scissorHeight = windowHeight; |
| } |
| else |
| { |
| scissorX -= kScissorStep; |
| scissorY += kScissorStep; |
| scissorWidth += kScissorStep * 2; |
| scissorHeight -= kScissorStep * 2; |
| } |
| } |
| } |
| |
| void DrawWithEightTextures(unsigned int iterations, |
| GLsizei numElements, |
| std::vector<GLuint> textures) |
| { |
| for (unsigned int it = 0; it < iterations; it++) |
| { |
| for (size_t i = 0; i < textures.size(); ++i) |
| { |
| glActiveTexture(GL_TEXTURE0 + i); |
| size_t index = (it + i) % textures.size(); |
| glBindTexture(GL_TEXTURE_2D, textures[index]); |
| } |
| |
| glDrawArrays(GL_TRIANGLES, 0, numElements); |
| } |
| } |
| |
| void DrawCallPerfBenchmark::drawBenchmark() |
| { |
| // This workaround fixes a huge queue of graphics commands accumulating on the GL |
| // back-end. The GL back-end doesn't have a proper NULL device at the moment. |
| // TODO(jmadill): Remove this when/if we ever get a proper OpenGL NULL device. |
| const auto &eglParams = GetParam().eglParameters; |
| const auto ¶ms = GetParam(); |
| GLsizei numElements = static_cast<GLsizei>(3 * mNumTris); |
| |
| switch (params.stateChange) |
| { |
| case StateChange::VertexAttrib: |
| ChangeVertexAttribThenDraw<1>(params.iterationsPerStep, numElements, mBuffer1); |
| break; |
| case StateChange::VertexBuffer: |
| ChangeArrayBuffersThenDraw<1>(params.iterationsPerStep, numElements, mBuffer1, |
| mBuffer2); |
| break; |
| case StateChange::ManyVertexBuffers: |
| ChangeArrayBuffersThenDraw<5>(params.iterationsPerStep, numElements, mBuffer1, |
| mBuffer2); |
| break; |
| case StateChange::Texture: |
| ChangeTextureThenDraw(params.iterationsPerStep, numElements, mTextures[0], |
| mTextures[1]); |
| break; |
| case StateChange::Program: |
| ChangeProgramThenDraw(params.iterationsPerStep, numElements, mProgram1, mProgram2); |
| break; |
| case StateChange::NoChange: |
| if (eglParams.deviceType != EGL_PLATFORM_ANGLE_DEVICE_TYPE_NULL_ANGLE || |
| (eglParams.renderer != EGL_PLATFORM_ANGLE_TYPE_OPENGL_ANGLE && |
| eglParams.renderer != EGL_PLATFORM_ANGLE_TYPE_OPENGLES_ANGLE)) |
| { |
| ClearThenDraw(params.iterationsPerStep, numElements); |
| } |
| else |
| { |
| JustDraw(params.iterationsPerStep, numElements); |
| } |
| break; |
| case StateChange::VertexBufferCycle: |
| CycleVertexBufferThenDraw(params.iterationsPerStep, numElements, mVBOPool, |
| &mCurrentVBO); |
| break; |
| case StateChange::Scissor: |
| ChangeScissorThenDraw(params.iterationsPerStep, numElements, getWindow()->getWidth(), |
| getWindow()->getHeight()); |
| break; |
| case StateChange::ManyTextureDraw: |
| glUseProgram(mProgram3); |
| DrawWithEightTextures(params.iterationsPerStep, numElements, mTextures); |
| break; |
| case StateChange::InvalidEnum: |
| ADD_FAILURE() << "Invalid state change."; |
| break; |
| } |
| |
| ASSERT_GL_NO_ERROR(); |
| } |
| |
| TEST_P(DrawCallPerfBenchmark, Run) |
| { |
| run(); |
| } |
| |
| using namespace params; |
| |
| DrawArraysPerfParams CombineStateChange(const DrawArraysPerfParams &in, StateChange stateChange) |
| { |
| DrawArraysPerfParams out = in; |
| out.stateChange = stateChange; |
| |
| // Crank up iteration count to ensure we cycle through all VBs before a swap. |
| if (stateChange == StateChange::VertexBufferCycle) |
| { |
| out.iterationsPerStep = kCycleVBOPoolSize * 2; |
| } |
| |
| return out; |
| } |
| |
| using P = DrawArraysPerfParams; |
| |
| std::vector<P> gTestsWithStateChange = |
| CombineWithValues({P()}, angle::AllEnums<StateChange>(), CombineStateChange); |
| std::vector<P> gTestsWithRenderer = |
| CombineWithFuncs(gTestsWithStateChange, {D3D11<P>, GL<P>, Vulkan<P>, WGL<P>}); |
| std::vector<P> gTestsWithDevice = |
| CombineWithFuncs(gTestsWithRenderer, {Passthrough<P>, Offscreen<P>, NullDevice<P>}); |
| |
| ANGLE_INSTANTIATE_TEST_ARRAY(DrawCallPerfBenchmark, gTestsWithDevice); |
| |
| } // anonymous namespace |