Fix fragment output variables validation

* Reject fragment shaders that assign
  out-of-range fragment locations

* Reject fragment shaders that assign
  output variables of different types
  to the same location

* Apply similar validation for fragment
  outputs bound via API calls

* Ensure that masks for active output
  variables and draw buffer types are
  set after processing all fragment
  output bindings

Bug: angleproject:1085
Change-Id: If29cbb58be1981279fc97c67739fe4136b0cdc98
Reviewed-on: https://chromium-review.googlesource.com/c/angle/angle/+/4813656
Commit-Queue: Alexey Knyazev <lexa.knyazev@gmail.com>
Reviewed-by: Shahbaz Youssefi <syoussefi@chromium.org>
diff --git a/src/compiler/translator/Compiler.cpp b/src/compiler/translator/Compiler.cpp
index 9ef54ed..eb4d249 100644
--- a/src/compiler/translator/Compiler.cpp
+++ b/src/compiler/translator/Compiler.cpp
@@ -886,8 +886,8 @@
     }
 
     if (mShaderVersion >= 300 && mShaderType == GL_FRAGMENT_SHADER &&
-        !ValidateOutputs(root, getExtensionBehavior(), mResources.MaxDrawBuffers,
-                         hasPixelLocalStorageUniforms(), &mDiagnostics))
+        !ValidateOutputs(root, getExtensionBehavior(), mResources, hasPixelLocalStorageUniforms(),
+                         &mDiagnostics))
     {
         return false;
     }
diff --git a/src/compiler/translator/ValidateOutputs.cpp b/src/compiler/translator/ValidateOutputs.cpp
index 66ab3f3..b42cca0 100644
--- a/src/compiler/translator/ValidateOutputs.cpp
+++ b/src/compiler/translator/ValidateOutputs.cpp
@@ -30,7 +30,7 @@
 {
   public:
     ValidateOutputsTraverser(const TExtensionBehavior &extBehavior,
-                             int maxDrawBuffers,
+                             const ShBuiltInResources &resources,
                              bool usesPixelLocalStorage);
 
     void validate(TDiagnostics *diagnostics) const;
@@ -39,7 +39,9 @@
 
   private:
     int mMaxDrawBuffers;
+    int mMaxDualSourceDrawBuffers;
     bool mEnablesBlendFuncExtended;
+    bool mUsesIndex1;
     bool mUsesPixelLocalStorage;
     bool mUsesFragDepth;
 
@@ -51,12 +53,14 @@
 };
 
 ValidateOutputsTraverser::ValidateOutputsTraverser(const TExtensionBehavior &extBehavior,
-                                                   int maxDrawBuffers,
+                                                   const ShBuiltInResources &resources,
                                                    bool usesPixelLocalStorage)
     : TIntermTraverser(true, false, false),
-      mMaxDrawBuffers(maxDrawBuffers),
+      mMaxDrawBuffers(resources.MaxDrawBuffers),
+      mMaxDualSourceDrawBuffers(resources.MaxDualSourceDrawBuffers),
       mEnablesBlendFuncExtended(
           IsExtensionEnabled(extBehavior, TExtension::EXT_blend_func_extended)),
+      mUsesIndex1(false),
       mUsesPixelLocalStorage(usesPixelLocalStorage),
       mUsesFragDepth(false)
 {}
@@ -74,11 +78,16 @@
     TQualifier qualifier = symbol->getQualifier();
     if (qualifier == EvqFragmentOut)
     {
-        if (symbol->getType().getLayoutQualifier().location != -1)
+        const TLayoutQualifier &layoutQualifier = symbol->getType().getLayoutQualifier();
+        if (layoutQualifier.location != -1)
         {
             mOutputs.push_back(symbol);
+            if (layoutQualifier.index == 1)
+            {
+                mUsesIndex1 = true;
+            }
         }
-        else if (symbol->getType().getLayoutQualifier().yuv == true)
+        else if (layoutQualifier.yuv == true)
         {
             mYuvOutputs.push_back(symbol);
         }
@@ -96,8 +105,8 @@
 void ValidateOutputsTraverser::validate(TDiagnostics *diagnostics) const
 {
     ASSERT(diagnostics);
-    OutputVector validOutputs(mMaxDrawBuffers, nullptr);
-    OutputVector validSecondaryOutputs(mMaxDrawBuffers, nullptr);
+    OutputVector validOutputs(mUsesIndex1 ? mMaxDualSourceDrawBuffers : mMaxDrawBuffers, nullptr);
+    OutputVector validSecondaryOutputs(mMaxDualSourceDrawBuffers, nullptr);
 
     for (const auto &symbol : mOutputs)
     {
@@ -110,11 +119,13 @@
         ASSERT(type.getLayoutQualifier().location != -1);
 
         OutputVector *validOutputsToUse = &validOutputs;
+        OutputVector *otherOutputsToUse = &validSecondaryOutputs;
         // The default index is 0, so we only assign the output to secondary outputs in case the
         // index is explicitly set to 1.
         if (type.getLayoutQualifier().index == 1)
         {
             validOutputsToUse = &validSecondaryOutputs;
+            otherOutputsToUse = &validOutputs;
         }
 
         if (location + elementCount <= validOutputsToUse->size())
@@ -132,6 +143,19 @@
                 else
                 {
                     (*validOutputsToUse)[offsetLocation] = symbol;
+                    if (offsetLocation < otherOutputsToUse->size())
+                    {
+                        TIntermSymbol *otherSymbol = (*otherOutputsToUse)[offsetLocation];
+                        if (otherSymbol && otherSymbol->getType().getBasicType() !=
+                                               symbol->getType().getBasicType())
+                        {
+                            std::stringstream strstr = sh::InitializeStream<std::stringstream>();
+                            strstr << "conflicting output types with previously defined output "
+                                   << "'" << (*otherOutputsToUse)[offsetLocation]->getName() << "'"
+                                   << " for location " << offsetLocation;
+                            error(*symbol, strstr.str().c_str(), diagnostics);
+                        }
+                    }
                 }
             }
         }
@@ -139,10 +163,11 @@
         {
             if (elementCount > 0)
             {
-                error(*symbol,
-                      elementCount > 1 ? "output array locations would exceed MAX_DRAW_BUFFERS"
-                                       : "output location must be < MAX_DRAW_BUFFERS",
-                      diagnostics);
+                std::stringstream strstr = sh::InitializeStream<std::stringstream>();
+                strstr << (elementCount > 1 ? "output array locations would exceed "
+                                            : "output location must be < ")
+                       << "MAX_" << (mUsesIndex1 ? "DUAL_SOURCE_" : "") << "DRAW_BUFFERS";
+                error(*symbol, strstr.str().c_str(), diagnostics);
             }
         }
     }
@@ -188,11 +213,11 @@
 
 bool ValidateOutputs(TIntermBlock *root,
                      const TExtensionBehavior &extBehavior,
-                     int maxDrawBuffers,
+                     const ShBuiltInResources &resources,
                      bool usesPixelLocalStorage,
                      TDiagnostics *diagnostics)
 {
-    ValidateOutputsTraverser validateOutputs(extBehavior, maxDrawBuffers, usesPixelLocalStorage);
+    ValidateOutputsTraverser validateOutputs(extBehavior, resources, usesPixelLocalStorage);
     root->traverse(&validateOutputs);
     int numErrorsBefore = diagnostics->numErrors();
     validateOutputs.validate(diagnostics);
diff --git a/src/compiler/translator/ValidateOutputs.h b/src/compiler/translator/ValidateOutputs.h
index 1957450..4201f7c 100644
--- a/src/compiler/translator/ValidateOutputs.h
+++ b/src/compiler/translator/ValidateOutputs.h
@@ -11,6 +11,8 @@
 #ifndef COMPILER_TRANSLATOR_VALIDATEOUTPUTS_H_
 #define COMPILER_TRANSLATOR_VALIDATEOUTPUTS_H_
 
+#include <GLSLANG/ShaderLang.h>
+
 #include "compiler/translator/ExtensionBehavior.h"
 
 namespace sh
@@ -23,7 +25,7 @@
 // Returns true if the shader has no conflicting or otherwise erroneous fragment outputs.
 bool ValidateOutputs(TIntermBlock *root,
                      const TExtensionBehavior &extBehavior,
-                     int maxDrawBuffers,
+                     const ShBuiltInResources &resources,
                      bool usesPixelLocalStorage,
                      TDiagnostics *diagnostics);
 
diff --git a/src/libANGLE/ProgramExecutable.cpp b/src/libANGLE/ProgramExecutable.cpp
index acd0c62..58df9e1 100644
--- a/src/libANGLE/ProgramExecutable.cpp
+++ b/src/libANGLE/ProgramExecutable.cpp
@@ -400,8 +400,6 @@
     mSamplerBindings.clear();
     mSamplerBoundTextureUnits.clear();
     mImageBindings.clear();
-
-    mOutputVariableTypes.clear();
 }
 
 void ProgramExecutable::load(bool isSeparable, gl::BinaryInputStream *stream)
@@ -485,13 +483,6 @@
         stream->readBool(&locationData.ignored);
     }
 
-    size_t outputTypeCount = stream->readInt<size_t>();
-    mOutputVariableTypes.resize(outputTypeCount);
-    for (size_t outputIndex = 0; outputIndex < outputTypeCount; ++outputIndex)
-    {
-        mOutputVariableTypes[outputIndex] = stream->readInt<GLenum>();
-    }
-
     size_t secondaryOutputVarCount = stream->readInt<size_t>();
     ASSERT(mSecondaryOutputLocations.empty());
     mSecondaryOutputLocations.resize(secondaryOutputVarCount);
@@ -608,12 +599,6 @@
         stream->writeBool(outputVar.ignored);
     }
 
-    stream->writeInt(mOutputVariableTypes.size());
-    for (const auto &outputVariableType : mOutputVariableTypes)
-    {
-        stream->writeInt(outputVariableType);
-    }
-
     stream->writeInt(getSecondaryOutputLocations().size());
     for (const auto &outputVar : getSecondaryOutputLocations())
     {
@@ -1141,73 +1126,15 @@
     const ProgramAliasedBindings &fragmentOutputLocations,
     const ProgramAliasedBindings &fragmentOutputIndices)
 {
-    ASSERT(mOutputVariableTypes.empty());
     ASSERT(mPODStruct.activeOutputVariablesMask.none());
     ASSERT(mPODStruct.drawBufferTypeMask.none());
     ASSERT(!mPODStruct.hasYUVOutput);
 
-    // Gather output variable types
-    for (const sh::ShaderVariable &outputVariable : outputVariables)
-    {
-        if (outputVariable.isBuiltIn() && outputVariable.name != "gl_FragColor" &&
-            outputVariable.name != "gl_FragData")
-        {
-            continue;
-        }
-
-        unsigned int baseLocation =
-            (outputVariable.location == -1 ? 0u
-                                           : static_cast<unsigned int>(outputVariable.location));
-
-        // GLSL ES 3.10 section 4.3.6: Output variables cannot be arrays of arrays or arrays of
-        // structures, so we may use getBasicTypeElementCount().
-        unsigned int elementCount = outputVariable.getBasicTypeElementCount();
-        for (unsigned int elementIndex = 0; elementIndex < elementCount; elementIndex++)
-        {
-            const unsigned int location = baseLocation + elementIndex;
-            if (location >= mOutputVariableTypes.size())
-            {
-                mOutputVariableTypes.resize(location + 1, GL_NONE);
-            }
-            ASSERT(location < mPODStruct.activeOutputVariablesMask.size());
-            mPODStruct.activeOutputVariablesMask.set(location);
-            mOutputVariableTypes[location] = VariableComponentType(outputVariable.type);
-            ComponentType componentType    = GLenumToComponentType(mOutputVariableTypes[location]);
-            SetComponentTypeMask(componentType, location, &mPODStruct.drawBufferTypeMask);
-        }
-
-        if (outputVariable.yuv)
-        {
-            ASSERT(outputVariables.size() == 1);
-            mPODStruct.hasYUVOutput = true;
-        }
-    }
-
-    if (version >= ES_3_1)
-    {
-        // [OpenGL ES 3.1] Chapter 8.22 Page 203:
-        // A link error will be generated if the sum of the number of active image uniforms used in
-        // all shaders, the number of active shader storage blocks, and the number of active
-        // fragment shader outputs exceeds the implementation-dependent value of
-        // MAX_COMBINED_SHADER_OUTPUT_RESOURCES.
-        if (combinedImageUniformsCount + combinedShaderStorageBlocksCount +
-                mPODStruct.activeOutputVariablesMask.count() >
-            static_cast<GLuint>(caps.maxCombinedShaderOutputResources))
-        {
-            mInfoLog
-                << "The sum of the number of active image uniforms, active shader storage blocks "
-                   "and active fragment shader outputs exceeds "
-                   "MAX_COMBINED_SHADER_OUTPUT_RESOURCES ("
-                << caps.maxCombinedShaderOutputResources << ")";
-            return false;
-        }
-    }
-
     mOutputVariables = outputVariables;
 
     if (fragmentShaderVersion == 100)
     {
-        return true;
+        return gatherOutputTypes();
     }
 
     // EXT_blend_func_extended doesn't specify anything related to binding specific elements of an
@@ -1385,6 +1312,80 @@
         }
     }
 
+    if (!gatherOutputTypes())
+    {
+        return false;
+    }
+
+    if (version >= ES_3_1)
+    {
+        // [OpenGL ES 3.1] Chapter 8.22 Page 203:
+        // A link error will be generated if the sum of the number of active image uniforms used in
+        // all shaders, the number of active shader storage blocks, and the number of active
+        // fragment shader outputs exceeds the implementation-dependent value of
+        // MAX_COMBINED_SHADER_OUTPUT_RESOURCES.
+        if (combinedImageUniformsCount + combinedShaderStorageBlocksCount +
+                mPODStruct.activeOutputVariablesMask.count() >
+            static_cast<GLuint>(caps.maxCombinedShaderOutputResources))
+        {
+            mInfoLog
+                << "The sum of the number of active image uniforms, active shader storage blocks "
+                   "and active fragment shader outputs exceeds "
+                   "MAX_COMBINED_SHADER_OUTPUT_RESOURCES ("
+                << caps.maxCombinedShaderOutputResources << ")";
+            return false;
+        }
+    }
+
+    return true;
+}
+
+bool ProgramExecutable::gatherOutputTypes()
+{
+    for (const sh::ShaderVariable &outputVariable : mOutputVariables)
+    {
+        if (outputVariable.isBuiltIn() && outputVariable.name != "gl_FragColor" &&
+            outputVariable.name != "gl_FragData")
+        {
+            continue;
+        }
+
+        unsigned int baseLocation =
+            (outputVariable.location == -1 ? 0u
+                                           : static_cast<unsigned int>(outputVariable.location));
+
+        const ComponentType componentType =
+            GLenumToComponentType(VariableComponentType(outputVariable.type));
+
+        // GLSL ES 3.10 section 4.3.6: Output variables cannot be arrays of arrays or arrays of
+        // structures, so we may use getBasicTypeElementCount().
+        unsigned int elementCount = outputVariable.getBasicTypeElementCount();
+        for (unsigned int elementIndex = 0; elementIndex < elementCount; elementIndex++)
+        {
+            const unsigned int location = baseLocation + elementIndex;
+            ASSERT(location < mPODStruct.activeOutputVariablesMask.size());
+            mPODStruct.activeOutputVariablesMask.set(location);
+            const ComponentType storedComponentType =
+                gl::GetComponentTypeMask(mPODStruct.drawBufferTypeMask, location);
+            if (storedComponentType == ComponentType::InvalidEnum)
+            {
+                SetComponentTypeMask(componentType, location, &mPODStruct.drawBufferTypeMask);
+            }
+            else if (storedComponentType != componentType)
+            {
+                mInfoLog << "Inconsistent component types for fragment outputs at location "
+                         << location;
+                return false;
+            }
+        }
+
+        if (outputVariable.yuv)
+        {
+            ASSERT(mOutputVariables.size() == 1);
+            mPODStruct.hasYUVOutput = true;
+        }
+    }
+
     return true;
 }
 
diff --git a/src/libANGLE/ProgramExecutable.h b/src/libANGLE/ProgramExecutable.h
index 077e9d5..732e9a3 100644
--- a/src/libANGLE/ProgramExecutable.h
+++ b/src/libANGLE/ProgramExecutable.h
@@ -533,6 +533,8 @@
                                      const ProgramAliasedBindings &fragmentOutputLocations,
                                      const ProgramAliasedBindings &fragmentOutputIndices);
 
+    bool gatherOutputTypes();
+
     void linkSamplerAndImageBindings(GLuint *combinedImageUniformsCount);
     bool linkAtomicCounterBuffers(const Caps &caps, InfoLog &infoLog);
 
@@ -646,9 +648,6 @@
     ShaderMap<std::vector<sh::ShaderVariable>> mLinkedUniforms;
     ShaderMap<std::vector<sh::InterfaceBlock>> mLinkedUniformBlocks;
 
-    // Fragment output variable base types: FLOAT, INT, or UINT.  Ordered by location.
-    std::vector<GLenum> mOutputVariableTypes;
-
     // Cache for sampler validation
     mutable Optional<bool> mCachedValidateSamplersResult;
 };
diff --git a/src/tests/compiler_tests/EXT_blend_func_extended_test.cpp b/src/tests/compiler_tests/EXT_blend_func_extended_test.cpp
index 8746454..b86f855 100644
--- a/src/tests/compiler_tests/EXT_blend_func_extended_test.cpp
+++ b/src/tests/compiler_tests/EXT_blend_func_extended_test.cpp
@@ -114,6 +114,36 @@
     fragColor = vec4(1.0);
 })";
 
+// Shader that specifies an output with out-of-bounds location
+// for index 0 when another output uses index 1 is invalid.
+const char ESSL300_Index0OutOfBoundsFailureShader[] =
+    R"(precision mediump float;
+layout(location = 1, index = 0) out mediump vec4 fragColor;
+layout(location = 0, index = 1) out mediump vec4 secondaryFragColor;
+void main() {
+    fragColor = vec4(1);
+    secondaryFragColor = vec4(1);
+})";
+
+// Shader that specifies an output with out-of-bounds location for index 1 is invalid.
+const char ESSL300_Index1OutOfBoundsFailureShader[] =
+    R"(precision mediump float;
+layout(location = 1, index = 1) out mediump vec4 secondaryFragColor;
+void main() {
+    secondaryFragColor = vec4(1);
+})";
+
+// Shader that specifies two outputs with the same location
+// but different indices and different base types is invalid.
+const char ESSL300_IndexTypeMismatchFailureShader[] =
+    R"(precision mediump float;
+layout(location = 0, index = 0) out mediump vec4 fragColor;
+layout(location = 0, index = 1) out mediump ivec4 secondaryFragColor;
+void main() {
+    fragColor = vec4(1);
+    secondaryFragColor = ivec4(1);
+})";
+
 // Global index layout qualifier fails.
 const char ESSL300_GlobalIndexFailureShader[] =
     R"(precision mediump float;
@@ -260,6 +290,9 @@
                                  Values(sh::ESSLVersion300, sh::ESSLVersion310),
                                  Values(ESSL300_LocationIndexFailureShader,
                                         ESSL300_DoubleIndexFailureShader,
+                                        ESSL300_Index0OutOfBoundsFailureShader,
+                                        ESSL300_Index1OutOfBoundsFailureShader,
+                                        ESSL300_IndexTypeMismatchFailureShader,
                                         ESSL300_GlobalIndexFailureShader,
                                         ESSL300_IndexOnUniformVariableFailureShader,
                                         ESSL300_IndexOnStructFailureShader,
diff --git a/src/tests/gl_tests/BlendFuncExtendedTest.cpp b/src/tests/gl_tests/BlendFuncExtendedTest.cpp
index 945a6e5..868e4d1 100644
--- a/src/tests/gl_tests/BlendFuncExtendedTest.cpp
+++ b/src/tests/gl_tests/BlendFuncExtendedTest.cpp
@@ -862,6 +862,179 @@
     glDeleteProgram(program);
 }
 
+// Test that a secondary blending source limits the number of primary outputs.
+TEST_P(EXTBlendFuncExtendedTestES3, TooManyFragmentOutputsForDualSourceBlending)
+{
+    ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_blend_func_extended"));
+
+    GLint maxDualSourceDrawBuffers;
+    glGetIntegerv(GL_MAX_DUAL_SOURCE_DRAW_BUFFERS_EXT, &maxDualSourceDrawBuffers);
+    ASSERT_GE(maxDualSourceDrawBuffers, 1);
+
+    constexpr char kFS[] = R"(#version 300 es
+#extension GL_EXT_blend_func_extended : require
+precision mediump float;
+out vec4 outSrc0;
+out vec4 outSrc1;
+void main() {
+    outSrc0 = vec4(0.5);
+    outSrc1 = vec4(1.0);
+})";
+
+    GLuint vs = CompileShader(GL_VERTEX_SHADER, essl3_shaders::vs::Simple());
+    GLuint fs = CompileShader(GL_FRAGMENT_SHADER, kFS);
+    ASSERT_NE(0u, vs);
+    ASSERT_NE(0u, fs);
+
+    GLuint program = glCreateProgram();
+    glAttachShader(program, vs);
+    glDeleteShader(vs);
+    glAttachShader(program, fs);
+    glDeleteShader(fs);
+
+    glBindFragDataLocationIndexedEXT(program, maxDualSourceDrawBuffers, 0, "outSrc0");
+    glBindFragDataLocationIndexedEXT(program, 0, 1, "outSrc1");
+    ASSERT_GL_NO_ERROR();
+
+    GLint linkStatus;
+    glLinkProgram(program);
+    glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
+    EXPECT_EQ(0, linkStatus);
+
+    glDeleteProgram(program);
+}
+
+// Test that fragment outputs bound to the same location must have the same type.
+TEST_P(EXTBlendFuncExtendedTestES3, InconsistentTypesForLocationAPI)
+{
+    ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_blend_func_extended"));
+
+    constexpr char kFS[] = R"(#version 300 es
+#extension GL_EXT_blend_func_extended : require
+precision mediump float;
+out vec4 outSrc0;
+out ivec4 outSrc1;
+void main() {
+    outSrc0 = vec4(0.5);
+    outSrc1 = ivec4(1.0);
+})";
+
+    GLuint vs = CompileShader(GL_VERTEX_SHADER, essl3_shaders::vs::Simple());
+    GLuint fs = CompileShader(GL_FRAGMENT_SHADER, kFS);
+    ASSERT_NE(0u, vs);
+    ASSERT_NE(0u, fs);
+
+    GLuint program = glCreateProgram();
+    glAttachShader(program, vs);
+    glDeleteShader(vs);
+    glAttachShader(program, fs);
+    glDeleteShader(fs);
+
+    glBindFragDataLocationIndexedEXT(program, 0, 0, "outSrc0");
+    glBindFragDataLocationIndexedEXT(program, 0, 1, "outSrc1");
+    ASSERT_GL_NO_ERROR();
+
+    GLint linkStatus;
+    glLinkProgram(program);
+    glGetProgramiv(program, GL_LINK_STATUS, &linkStatus);
+    EXPECT_EQ(0, linkStatus);
+
+    glDeleteProgram(program);
+}
+
+// Test that rendering to multiple fragment outputs bound via API works.
+TEST_P(EXTBlendFuncExtendedDrawTestES3, MultipleDrawBuffersAPI)
+{
+    ANGLE_SKIP_TEST_IF(!IsGLExtensionEnabled("GL_EXT_blend_func_extended"));
+
+    constexpr char kFS[] = R"(#version 300 es
+#extension GL_EXT_blend_func_extended : require
+precision mediump float;
+out vec4 outSrc0;
+out ivec4 outSrc1;
+void main() {
+    outSrc0 = vec4(0.0, 1.0, 0.0, 1.0);
+    outSrc1 = ivec4(1, 2, 3, 4);
+})";
+
+    mProgram = CompileProgram(essl3_shaders::vs::Simple(), kFS, [](GLuint program) {
+        glBindFragDataLocationEXT(program, 0, "outSrc0");
+        glBindFragDataLocationEXT(program, 1, "outSrc1");
+    });
+
+    ASSERT_NE(0u, mProgram);
+
+    GLRenderbuffer rb0;
+    glBindRenderbuffer(GL_RENDERBUFFER, rb0);
+    glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8, 1, 1);
+
+    GLRenderbuffer rb1;
+    glBindRenderbuffer(GL_RENDERBUFFER, rb1);
+    glRenderbufferStorage(GL_RENDERBUFFER, GL_RGBA8I, 1, 1);
+
+    GLFramebuffer fbo;
+    glBindFramebuffer(GL_FRAMEBUFFER, fbo);
+
+    const GLenum bufs[] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1};
+    glDrawBuffers(2, bufs);
+
+    GLfloat clearF[] = {0.0, 0.0, 0.0, 0.0};
+    GLint clearI[]   = {0, 0, 0, 0};
+
+    // FBO: rb0 (rgba8), rb1 (rgba8i)
+    glBindRenderbuffer(GL_RENDERBUFFER, rb0);
+    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rb0);
+    glBindRenderbuffer(GL_RENDERBUFFER, rb1);
+    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_RENDERBUFFER, rb1);
+    ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    glClearBufferfv(GL_COLOR, 0, clearF);
+    glClearBufferiv(GL_COLOR, 1, clearI);
+    ASSERT_GL_NO_ERROR();
+
+    glReadBuffer(GL_COLOR_ATTACHMENT0);
+    EXPECT_PIXEL_EQ(0, 0, 0, 0, 0, 0);
+    glReadBuffer(GL_COLOR_ATTACHMENT1);
+    EXPECT_PIXEL_8I(0, 0, 0, 0, 0, 0);
+
+    drawQuad(mProgram, essl3_shaders::PositionAttrib(), 0.0);
+    ASSERT_GL_NO_ERROR();
+
+    glReadBuffer(GL_COLOR_ATTACHMENT0);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+    glReadBuffer(GL_COLOR_ATTACHMENT1);
+    EXPECT_PIXEL_8I(0, 0, 1, 2, 3, 4);
+
+    // FBO: rb1 (rgba8i), rb0 (rgba8)
+    glBindRenderbuffer(GL_RENDERBUFFER, rb0);
+    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_RENDERBUFFER, rb0);
+    glBindRenderbuffer(GL_RENDERBUFFER, rb1);
+    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, rb1);
+    ASSERT_GL_FRAMEBUFFER_COMPLETE(GL_FRAMEBUFFER);
+
+    // Rebind fragment outputs
+    glBindFragDataLocationEXT(mProgram, 0, "outSrc1");
+    glBindFragDataLocationEXT(mProgram, 1, "outSrc0");
+    glLinkProgram(mProgram);
+
+    glClearBufferfv(GL_COLOR, 1, clearF);
+    glClearBufferiv(GL_COLOR, 0, clearI);
+    ASSERT_GL_NO_ERROR();
+
+    glReadBuffer(GL_COLOR_ATTACHMENT1);
+    EXPECT_PIXEL_EQ(0, 0, 0, 0, 0, 0);
+    glReadBuffer(GL_COLOR_ATTACHMENT0);
+    EXPECT_PIXEL_8I(0, 0, 0, 0, 0, 0);
+
+    drawQuad(mProgram, essl3_shaders::PositionAttrib(), 0.0);
+    ASSERT_GL_NO_ERROR();
+
+    glReadBuffer(GL_COLOR_ATTACHMENT1);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+    glReadBuffer(GL_COLOR_ATTACHMENT0);
+    EXPECT_PIXEL_8I(0, 0, 1, 2, 3, 4);
+}
+
 // Use a program pipeline with EXT_blend_func_extended
 TEST_P(EXTBlendFuncExtendedDrawTestES31, UseProgramPipeline)
 {