From 9ec3f51d11d92c8b87da27811cc7ab577705ea3c Mon Sep 17 00:00:00 2001 From: Mohan Maiya Date: Fri, 19 Jul 2019 13:06:16 -0700 Subject: [PATCH] Reland "Vulkan: Implement OES_get_program_binary extension" Reason for revert: default uniform initialization was incomplete This change has the following fixes: 1. Add missing default uniform initialization when loading program binaries. 2. Re-enable OES Program Binary capabilities for Vulkan. 3. Added two angle end2end test, - ProgramBinaryES3Test.BinaryWithLargeUniformCount uses several uniforms across the vertex and fragment shaders. - ProgramBinaryES3Test.ActiveUniformShader tests the difference between uniform static and active use Bug: angleproject:3216 Bug: angleproject:3217 Bug: angleproject:3665 Tests: dEQP-GLES3.functional.shader_api.program_binary* angle_end2end_tests --gtest_filter=ProgramBinary* Change-Id: If6886f01241d65bb1e17a21cc3406533021072ee Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/1699069 Commit-Queue: Mohan Maiya Commit-Queue: Jamie Madill Reviewed-by: Jamie Madill Reviewed-by: Shahbaz Youssefi --- src/libANGLE/Program.cpp | 46 ++-- src/libANGLE/Program.h | 4 + src/libANGLE/renderer/vulkan/ProgramVk.cpp | 80 +++++- src/libANGLE/renderer/vulkan/ProgramVk.h | 5 + .../renderer/vulkan/vk_caps_utils.cpp | 4 + src/tests/gl_tests/ProgramBinaryTest.cpp | 232 ++++++++++++++++++ 6 files changed, 346 insertions(+), 25 deletions(-) diff --git a/src/libANGLE/Program.cpp b/src/libANGLE/Program.cpp index aaa21a1a7..6dc5e97a7 100644 --- a/src/libANGLE/Program.cpp +++ b/src/libANGLE/Program.cpp @@ -677,11 +677,7 @@ void WriteBufferVariable(BinaryOutputStream *stream, const BufferVariable &var) WriteShaderVar(stream, var); stream->writeInt(var.bufferIndex); - stream->writeInt(var.blockInfo.offset); - stream->writeInt(var.blockInfo.arrayStride); - stream->writeInt(var.blockInfo.matrixStride); - stream->writeInt(var.blockInfo.isRowMajorMatrix); - stream->writeInt(var.blockInfo.topLevelArrayStride); + WriteBlockMemberInfo(stream, var.blockInfo); stream->writeInt(var.topLevelArraySize); for (ShaderType shaderType : AllShaderTypes()) @@ -694,13 +690,9 @@ void LoadBufferVariable(BinaryInputStream *stream, BufferVariable *var) { LoadShaderVar(stream, var); - var->bufferIndex = stream->readInt(); - var->blockInfo.offset = stream->readInt(); - var->blockInfo.arrayStride = stream->readInt(); - var->blockInfo.matrixStride = stream->readInt(); - var->blockInfo.isRowMajorMatrix = stream->readBool(); - var->blockInfo.topLevelArrayStride = stream->readInt(); - var->topLevelArraySize = stream->readInt(); + var->bufferIndex = stream->readInt(); + LoadBlockMemberInfo(stream, &var->blockInfo); + var->topLevelArraySize = stream->readInt(); for (ShaderType shaderType : AllShaderTypes()) { @@ -862,6 +854,24 @@ bool IsActiveInterfaceBlock(const sh::InterfaceBlock &interfaceBlock) return interfaceBlock.active || interfaceBlock.layout != sh::BLOCKLAYOUT_PACKED; } +void WriteBlockMemberInfo(BinaryOutputStream *stream, const sh::BlockMemberInfo &var) +{ + stream->writeInt(var.arrayStride); + stream->writeInt(var.isRowMajorMatrix); + stream->writeInt(var.matrixStride); + stream->writeInt(var.offset); + stream->writeInt(var.topLevelArrayStride); +} + +void LoadBlockMemberInfo(BinaryInputStream *stream, sh::BlockMemberInfo *var) +{ + var->arrayStride = stream->readInt(); + var->isRowMajorMatrix = stream->readBool(); + var->matrixStride = stream->readInt(); + var->offset = stream->readInt(); + var->topLevelArrayStride = stream->readInt(); +} + // VariableLocation implementation. VariableLocation::VariableLocation() : arrayIndex(0), index(kUnused), ignored(false) {} @@ -4561,10 +4571,7 @@ void Program::serialize(const Context *context, angle::MemoryBuffer *binaryOut) // FIXME: referenced stream.writeInt(uniform.bufferIndex); - stream.writeInt(uniform.blockInfo.offset); - stream.writeInt(uniform.blockInfo.arrayStride); - stream.writeInt(uniform.blockInfo.matrixStride); - stream.writeInt(uniform.blockInfo.isRowMajorMatrix); + WriteBlockMemberInfo(&stream, uniform.blockInfo); // Active shader info for (ShaderType shaderType : gl::AllShaderTypes()) @@ -4759,11 +4766,8 @@ angle::Result Program::deserialize(const Context *context, LinkedUniform uniform; LoadShaderVar(&stream, &uniform); - uniform.bufferIndex = stream.readInt(); - uniform.blockInfo.offset = stream.readInt(); - uniform.blockInfo.arrayStride = stream.readInt(); - uniform.blockInfo.matrixStride = stream.readInt(); - uniform.blockInfo.isRowMajorMatrix = stream.readBool(); + uniform.bufferIndex = stream.readInt(); + LoadBlockMemberInfo(&stream, &uniform.blockInfo); uniform.typeInfo = &GetUniformTypeInfo(uniform.type); diff --git a/src/libANGLE/Program.h b/src/libANGLE/Program.h index f147b445b..c1191f215 100644 --- a/src/libANGLE/Program.h +++ b/src/libANGLE/Program.h @@ -43,6 +43,7 @@ namespace gl { class Buffer; class BinaryInputStream; +class BinaryOutputStream; struct Caps; class Context; struct Extensions; @@ -165,6 +166,9 @@ void LogLinkMismatch(InfoLog &infoLog, bool IsActiveInterfaceBlock(const sh::InterfaceBlock &interfaceBlock); +void WriteBlockMemberInfo(BinaryOutputStream *stream, const sh::BlockMemberInfo &var); +void LoadBlockMemberInfo(BinaryInputStream *stream, sh::BlockMemberInfo *var); + // Struct used for correlating uniforms/elements of uniform arrays to handles struct VariableLocation { diff --git a/src/libANGLE/renderer/vulkan/ProgramVk.cpp b/src/libANGLE/renderer/vulkan/ProgramVk.cpp index 3462ae31f..236d58f0f 100644 --- a/src/libANGLE/renderer/vulkan/ProgramVk.cpp +++ b/src/libANGLE/renderer/vulkan/ProgramVk.cpp @@ -305,12 +305,42 @@ std::unique_ptr ProgramVk::load(const gl::Context *context, gl::InfoLog &infoLog) { ContextVk *contextVk = vk::GetImpl(context); + gl::ShaderMap requiredBufferSize; + requiredBufferSize.fill(0); + angle::Result status = loadShaderSource(contextVk, stream); if (status != angle::Result::Continue) { return std::make_unique(status); } + // Deserializes the uniformLayout data of mDefaultUniformBlocks + for (gl::ShaderType shaderType : gl::AllShaderTypes()) + { + const size_t uniformCount = stream->readInt(); + for (unsigned int uniformIndex = 0; uniformIndex < uniformCount; ++uniformIndex) + { + sh::BlockMemberInfo blockInfo; + gl::LoadBlockMemberInfo(stream, &blockInfo); + mDefaultUniformBlocks[shaderType].uniformLayout.push_back(blockInfo); + } + } + + // Deserializes required uniform block memory sizes + for (gl::ShaderType shaderType : gl::AllShaderTypes()) + { + requiredBufferSize[shaderType] = stream->readInt(); + } + + reset(contextVk); + + // Initialize and resize the mDefaultUniformBlocks' memory + status = resizeUniformBlockMemory(contextVk, requiredBufferSize); + if (status != angle::Result::Continue) + { + return std::make_unique(status); + } + return std::make_unique(linkImpl(context, infoLog)); } @@ -319,6 +349,25 @@ void ProgramVk::save(const gl::Context *context, gl::BinaryOutputStream *stream) // (geofflang): Look into saving shader modules in ShaderInfo objects (keep in mind that we // compile shaders lazily) saveShaderSource(stream); + + // Serializes the uniformLayout data of mDefaultUniformBlocks + for (gl::ShaderType shaderType : gl::AllShaderTypes()) + { + const size_t uniformCount = mDefaultUniformBlocks[shaderType].uniformLayout.size(); + stream->writeInt(uniformCount); + for (unsigned int uniformIndex = 0; uniformIndex < uniformCount; ++uniformIndex) + { + sh::BlockMemberInfo &blockInfo = + mDefaultUniformBlocks[shaderType].uniformLayout[uniformIndex]; + gl::WriteBlockMemberInfo(stream, blockInfo); + } + } + + // Serializes required uniform block memory sizes + for (gl::ShaderType shaderType : gl::AllShaderTypes()) + { + stream->writeInt(mDefaultUniformBlocks[shaderType].uniformData.size()); + } } void ProgramVk::setBinaryRetrievableHint(bool retrievable) @@ -335,12 +384,21 @@ std::unique_ptr ProgramVk::link(const gl::Context *context, const gl::ProgramLinkedResources &resources, gl::InfoLog &infoLog) { + ContextVk *contextVk = vk::GetImpl(context); // Link resources before calling GetShaderSource to make sure they are ready for the set/binding // assignment done in that function. linkResources(resources); GlslangWrapper::GetShaderSource(mState, resources, &mShaderSources); + reset(contextVk); + + angle::Result status = initDefaultUniformBlocks(context); + if (status != angle::Result::Continue) + { + return std::make_unique(status); + } + // TODO(jie.a.chen@intel.com): Parallelize linking. // http://crbug.com/849576 return std::make_unique(linkImpl(context, infoLog)); @@ -353,11 +411,8 @@ angle::Result ProgramVk::linkImpl(const gl::Context *glContext, gl::InfoLog &inf RendererVk *renderer = contextVk->getRenderer(); gl::TransformFeedback *transformFeedback = glState.getCurrentTransformFeedback(); - reset(contextVk); updateBindingOffsets(); - ANGLE_TRY(initDefaultUniformBlocks(glContext)); - // Store a reference to the pipeline and descriptor set layouts. This will create them if they // don't already exist in the cache. @@ -495,13 +550,22 @@ void ProgramVk::linkResources(const gl::ProgramLinkedResources &resources) angle::Result ProgramVk::initDefaultUniformBlocks(const gl::Context *glContext) { ContextVk *contextVk = vk::GetImpl(glContext); - RendererVk *renderer = contextVk->getRenderer(); // Process vertex and fragment uniforms into std140 packing. gl::ShaderMap layoutMap; gl::ShaderMap requiredBufferSize; requiredBufferSize.fill(0); + generateUniformLayoutMapping(layoutMap, requiredBufferSize); + initDefaultUniformLayoutMapping(layoutMap); + + // All uniform initializations are complete, now resize the buffers accordingly and return + return resizeUniformBlockMemory(contextVk, requiredBufferSize); +} + +void ProgramVk::generateUniformLayoutMapping(gl::ShaderMap &layoutMap, + gl::ShaderMap &requiredBufferSize) +{ for (const gl::ShaderType shaderType : mState.getLinkedShaderStages()) { gl::Shader *shader = mState.getAttachedShader(shaderType); @@ -513,7 +577,10 @@ angle::Result ProgramVk::initDefaultUniformBlocks(const gl::Context *glContext) &requiredBufferSize[shaderType]); } } +} +void ProgramVk::initDefaultUniformLayoutMapping(gl::ShaderMap &layoutMap) +{ // Init the default block layout info. const auto &uniforms = mState.getUniforms(); for (const gl::VariableLocation &location : mState.getUniformLocations()) @@ -553,7 +620,12 @@ angle::Result ProgramVk::initDefaultUniformBlocks(const gl::Context *glContext) mDefaultUniformBlocks[shaderType].uniformLayout.push_back(layoutInfo[shaderType]); } } +} +angle::Result ProgramVk::resizeUniformBlockMemory(ContextVk *contextVk, + gl::ShaderMap &requiredBufferSize) +{ + RendererVk *renderer = contextVk->getRenderer(); for (const gl::ShaderType shaderType : mState.getLinkedShaderStages()) { if (requiredBufferSize[shaderType] > 0) diff --git a/src/libANGLE/renderer/vulkan/ProgramVk.h b/src/libANGLE/renderer/vulkan/ProgramVk.h index 885755e66..5a87446f1 100644 --- a/src/libANGLE/renderer/vulkan/ProgramVk.h +++ b/src/libANGLE/renderer/vulkan/ProgramVk.h @@ -173,6 +173,11 @@ class ProgramVk : public ProgramImpl uint32_t descriptorSetIndex, bool *newPoolAllocatedOut); angle::Result initDefaultUniformBlocks(const gl::Context *glContext); + void generateUniformLayoutMapping(gl::ShaderMap &layoutMap, + gl::ShaderMap &requiredBufferSize); + void initDefaultUniformLayoutMapping(gl::ShaderMap &layoutMap); + angle::Result resizeUniformBlockMemory(ContextVk *contextVk, + gl::ShaderMap &requiredBufferSize); void updateDefaultUniformsDescriptorSet(ContextVk *contextVk); void updateTransformFeedbackDescriptorSetImpl(ContextVk *contextVk); diff --git a/src/libANGLE/renderer/vulkan/vk_caps_utils.cpp b/src/libANGLE/renderer/vulkan/vk_caps_utils.cpp index d6038426b..a4d4d3dce 100644 --- a/src/libANGLE/renderer/vulkan/vk_caps_utils.cpp +++ b/src/libANGLE/renderer/vulkan/vk_caps_utils.cpp @@ -331,6 +331,10 @@ void RendererVk::ensureCapsInitialized() const mNativeCaps.maxSamples = vk_gl::GetMaxSampleCount(sampleCounts); mNativeCaps.subPixelBits = limitsVk.subPixelPrecisionBits; + + // Enable Program Binary extension. + mNativeExtensions.getProgramBinary = true; + mNativeCaps.programBinaryFormats.push_back(GL_PROGRAM_BINARY_ANGLE); } namespace egl_vk diff --git a/src/tests/gl_tests/ProgramBinaryTest.cpp b/src/tests/gl_tests/ProgramBinaryTest.cpp index d335dc961..49eb0b254 100644 --- a/src/tests/gl_tests/ProgramBinaryTest.cpp +++ b/src/tests/gl_tests/ProgramBinaryTest.cpp @@ -388,6 +388,238 @@ TEST_P(ProgramBinaryES3Test, UniformBlockBindingNoDraw) testBinaryAndUBOBlockIndexes(false); } +// Tests the difference between uniform static and active use +TEST_P(ProgramBinaryES3Test, ActiveUniformShader) +{ + // We can't run the test if no program binary formats are supported. + GLint binaryFormatCount = 0; + glGetIntegerv(GL_NUM_PROGRAM_BINARY_FORMATS, &binaryFormatCount); + ANGLE_SKIP_TEST_IF(!binaryFormatCount); + + constexpr char kVS[] = + "#version 300 es\n" + "in vec4 position;\n" + "void main() {\n" + " gl_Position = position;\n" + "}"; + + constexpr char kFS[] = + "#version 300 es\n" + "precision mediump float;\n" + "uniform float values[2];\n" + "out vec4 color;\n" + "bool isZero(float value) {\n" + " return value == 0.0f;\n" + "}\n" + "void main() {\n" + " color = isZero(values[1]) ? vec4(1.0f,0.0f,0.0f,1.0f) : vec4(0.0f,1.0f,0.0f,1.0f);\n" + "}"; + + // Init and draw with the program. + ANGLE_GL_PROGRAM(program, kVS, kFS); + + GLint valuesLoc = glGetUniformLocation(program.get(), "values"); + ASSERT_NE(-1, valuesLoc); + + glUseProgram(program.get()); + GLfloat values[2] = {0.5f, 1.0f}; + glUniform1fv(valuesLoc, 2, values); + ASSERT_GL_NO_ERROR(); + + glClearColor(1.0, 0.0, 0.0, 1.0); + glClear(GL_COLOR_BUFFER_BIT); + EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red); + + drawQuad(program.get(), "position", 0.5f); + ASSERT_GL_NO_ERROR(); + EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green); + + // Read back the binary. + GLint programLength = 0; + glGetProgramiv(program.get(), GL_PROGRAM_BINARY_LENGTH_OES, &programLength); + ASSERT_GL_NO_ERROR(); + + GLsizei readLength = 0; + GLenum binaryFormat = GL_NONE; + std::vector binary(programLength); + glGetProgramBinary(program.get(), programLength, &readLength, &binaryFormat, binary.data()); + ASSERT_GL_NO_ERROR(); + + EXPECT_EQ(static_cast(programLength), readLength); + + // Load a new program with the binary and draw. + ANGLE_GL_BINARY_ES3_PROGRAM(binaryProgram, binary, binaryFormat); + + valuesLoc = glGetUniformLocation(program.get(), "values"); + ASSERT_NE(-1, valuesLoc); + + glUseProgram(binaryProgram.get()); + GLfloat values2[2] = {0.1f, 1.0f}; + glUniform1fv(valuesLoc, 2, values2); + ASSERT_GL_NO_ERROR(); + + glClearColor(1.0, 0.0, 0.0, 1.0); + glClear(GL_COLOR_BUFFER_BIT); + EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red); + + drawQuad(binaryProgram.get(), "position", 0.5f); + ASSERT_GL_NO_ERROR(); + EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green); +} + +// Test that uses many uniforms in the shaders +TEST_P(ProgramBinaryES3Test, BinaryWithLargeUniformCount) +{ + // Suspecting AMD driver bug - failure seen on bots running on ATI GPU on Windows. + // http://anglebug.com/3721 + ANGLE_SKIP_TEST_IF(IsAMD() && IsOpenGL() && IsWindows()); + + // We can't run the test if no program binary formats are supported. + GLint binaryFormatCount = 0; + glGetIntegerv(GL_NUM_PROGRAM_BINARY_FORMATS, &binaryFormatCount); + ANGLE_SKIP_TEST_IF(!binaryFormatCount); + + constexpr char kVS[] = + "#version 300 es\n" + "uniform float redVS; \n" + "uniform block0 {\n" + " float val0;\n" + "};\n" + "uniform float greenVS; \n" + "uniform float blueVS; \n" + "in vec4 position;\n" + "out vec4 color;\n" + "void main() {\n" + " gl_Position = position;\n" + " color = vec4(redVS + val0, greenVS, blueVS, 1.0f);\n" + "}"; + + constexpr char kFS[] = + "#version 300 es\n" + "precision mediump float;\n" + "uniform float redFS; \n" + "uniform float greenFS; \n" + "uniform block1 {\n" + " float val1;\n" + " float val2;\n" + "};\n" + "uniform float blueFS; \n" + "in vec4 color;\n" + "out vec4 colorOut;\n" + "void main() {\n" + " colorOut = vec4(color.r + redFS,\n" + " color.g + greenFS + val1,\n" + " color.b + blueFS + val2, \n" + " color.a);\n" + "}"; + + // Init and draw with the program. + ANGLE_GL_PROGRAM(program, kVS, kFS); + + float block0Data[4] = {-0.7f, 1.0f, 1.0f, 1.0f}; + float block1Data[4] = {0.4f, -0.8f, 1.0f, 1.0f}; + GLuint bindIndex0 = 5; + GLuint bindIndex1 = 2; + + GLBuffer ubo0; + glBindBuffer(GL_UNIFORM_BUFFER, ubo0.get()); + glBufferData(GL_UNIFORM_BUFFER, sizeof(block0Data), &block0Data, GL_STATIC_DRAW); + glBindBufferRange(GL_UNIFORM_BUFFER, bindIndex0, ubo0.get(), 0, sizeof(block0Data)); + ASSERT_GL_NO_ERROR(); + + GLBuffer ubo1; + glBindBuffer(GL_UNIFORM_BUFFER, ubo1.get()); + glBufferData(GL_UNIFORM_BUFFER, sizeof(block1Data), &block1Data, GL_STATIC_DRAW); + glBindBufferRange(GL_UNIFORM_BUFFER, bindIndex1, ubo1.get(), 0, sizeof(block1Data)); + ASSERT_GL_NO_ERROR(); + + GLint block0Index = glGetUniformBlockIndex(program.get(), "block0"); + ASSERT_NE(-1, block0Index); + + GLint block1Index = glGetUniformBlockIndex(program.get(), "block1"); + ASSERT_NE(-1, block1Index); + + glUniformBlockBinding(program.get(), block0Index, bindIndex0); + glUniformBlockBinding(program.get(), block1Index, bindIndex1); + ASSERT_GL_NO_ERROR(); + + GLint redVSLoc = glGetUniformLocation(program.get(), "redVS"); + ASSERT_NE(-1, redVSLoc); + GLint greenVSLoc = glGetUniformLocation(program.get(), "greenVS"); + ASSERT_NE(-1, greenVSLoc); + GLint blueVSLoc = glGetUniformLocation(program.get(), "blueVS"); + ASSERT_NE(-1, blueVSLoc); + GLint redFSLoc = glGetUniformLocation(program.get(), "redFS"); + ASSERT_NE(-1, redFSLoc); + GLint greenFSLoc = glGetUniformLocation(program.get(), "greenFS"); + ASSERT_NE(-1, greenFSLoc); + GLint blueFSLoc = glGetUniformLocation(program.get(), "blueFS"); + ASSERT_NE(-1, blueFSLoc); + + glUseProgram(program.get()); + glUniform1f(redVSLoc, 0.6f); + glUniform1f(greenVSLoc, 0.2f); + glUniform1f(blueVSLoc, 1.1f); + glUniform1f(redFSLoc, 0.1f); + glUniform1f(greenFSLoc, 0.4f); + glUniform1f(blueFSLoc, 0.7f); + ASSERT_GL_NO_ERROR(); + + glClearColor(1.0, 0.0, 0.0, 1.0); + glClear(GL_COLOR_BUFFER_BIT); + EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red); + + drawQuad(program.get(), "position", 0.5f); + ASSERT_GL_NO_ERROR(); + EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::cyan); + + // Read back the binary. + GLint programLength = 0; + glGetProgramiv(program.get(), GL_PROGRAM_BINARY_LENGTH_OES, &programLength); + ASSERT_GL_NO_ERROR(); + + GLsizei readLength = 0; + GLenum binaryFormat = GL_NONE; + std::vector binary(programLength); + glGetProgramBinary(program.get(), programLength, &readLength, &binaryFormat, binary.data()); + ASSERT_GL_NO_ERROR(); + + EXPECT_EQ(static_cast(programLength), readLength); + + // Load a new program with the binary and draw. + ANGLE_GL_BINARY_ES3_PROGRAM(binaryProgram, binary, binaryFormat); + + redVSLoc = glGetUniformLocation(program.get(), "redVS"); + ASSERT_NE(-1, redVSLoc); + greenVSLoc = glGetUniformLocation(program.get(), "greenVS"); + ASSERT_NE(-1, greenVSLoc); + blueVSLoc = glGetUniformLocation(program.get(), "blueVS"); + ASSERT_NE(-1, blueVSLoc); + redFSLoc = glGetUniformLocation(program.get(), "redFS"); + ASSERT_NE(-1, redFSLoc); + greenFSLoc = glGetUniformLocation(program.get(), "greenFS"); + ASSERT_NE(-1, greenFSLoc); + blueFSLoc = glGetUniformLocation(program.get(), "blueFS"); + ASSERT_NE(-1, blueFSLoc); + + glUseProgram(binaryProgram.get()); + glUniform1f(redVSLoc, 0.2f); + glUniform1f(greenVSLoc, -0.6f); + glUniform1f(blueVSLoc, 1.0f); + glUniform1f(redFSLoc, 1.5f); + glUniform1f(greenFSLoc, 0.2f); + glUniform1f(blueFSLoc, 0.8f); + ASSERT_GL_NO_ERROR(); + + glClearColor(1.0, 0.0, 0.0, 1.0); + glClear(GL_COLOR_BUFFER_BIT); + EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red); + + drawQuad(binaryProgram.get(), "position", 0.5f); + ASSERT_GL_NO_ERROR(); + EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::magenta); +} + ANGLE_INSTANTIATE_TEST(ProgramBinaryES3Test, ES3_D3D11(), ES3_OPENGL(),